snapshot of https://github.com/casbin/casbin.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Build / test (1.21) (push) Successful in 2m21s
Build / benchmark (1.21) (push) Failing after 2m28s
golangci-lint / golangci (push) Successful in 19m40s

This commit is contained in:
upstream-mirror
2026-09-15 21:46:19 +05:00
commit d8971e16fb
234 changed files with 28137 additions and 0 deletions

29
.asf.yaml Normal file
View File

@@ -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

21
.gitattributes vendored Normal file
View File

@@ -0,0 +1,21 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Repository-only development infrastructure and contributor guidance are not
# part of the source release.
.github export-ignore
CONTRIBUTING.md export-ignore

248
.github/scripts/benchmark_formatter.py vendored Normal file
View File

@@ -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)

32
.github/scripts/download_artifact.js vendored Normal file
View File

@@ -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}`);
}
};

59
.github/scripts/post_comment.js vendored Normal file
View File

@@ -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 = '<sub>🤖 This comment will be automatically updated with the latest benchmark results.</sub>';
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
});
}
};

2
.github/semantic.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
# Always validate the PR title AND all the commits
titleAndCommits: true

37
.github/workflows/comment.yml vendored Normal file
View File

@@ -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})

44
.github/workflows/default.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Build
on:
push:
branches:
- master
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go: ['1.21']
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Run go test
run: make test
benchmark:
runs-on: ubuntu-latest
strategy:
matrix:
go: ['1.21']
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Run go test bench
run: make benchmark

26
.github/workflows/golangci-lint.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: golangci-lint
on:
push:
branches:
- master
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
golangci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: golangci-lint
uses: golangci/golangci-lint-action@v8

97
.github/workflows/performance-pr.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: Performance Comparison for Pull Requests
on:
pull_request:
branches: [master]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
benchmark-pr:
name: Performance benchmark comparison
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 'stable'
# Save commit SHAs for display
- name: Save commit info
id: commits
run: |
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
echo "base_short=${BASE_SHA:0:7}" >> $GITHUB_OUTPUT
echo "head_short=${HEAD_SHA:0:7}" >> $GITHUB_OUTPUT
# Run benchmark on PR branch
- name: Run benchmark on PR branch
run: |
go test -bench '.' -benchtime=2s -benchmem ./... | tee pr-bench.txt
# Checkout base branch and run benchmark
- name: Checkout base branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
clean: false
path: base
- name: Run benchmark on base branch
working-directory: base
run: |
go test -bench '.' -benchtime=2s -benchmem ./... | \
tee ../base-bench.txt
# Install benchstat for comparison
- name: Install benchstat
run: go install golang.org/x/perf/cmd/benchstat@latest
# Compare benchmarks using benchstat
- name: Compare benchmarks with benchstat
id: benchstat
run: |
cat > comparison.md << 'EOF'
## Benchmark Comparison
Comparing base branch (`${{ steps.commits.outputs.base_short }}`)
vs PR branch (`${{ steps.commits.outputs.head_short }}`)
```
EOF
benchstat base-bench.txt pr-bench.txt >> comparison.md || true
echo '```' >> comparison.md
# Post-process to append percentage + emoji column (🚀 faster < -10%, 🐌 slower > +10%, otherwise ➡️)
if [ ! -f comparison.md ]; then
echo "comparison.md not found after benchstat." >&2
exit 1
fi
python3 .github/scripts/benchmark_formatter.py
# Save PR number
- name: Save PR number
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
if [ -z "$PR_NUMBER" ]; then
echo "Error: Pull request number is not available in event payload." >&2
exit 1
fi
echo "$PR_NUMBER" > pr_number.txt
# Upload benchmark results
- name: Upload benchmark results
uses: actions/upload-artifact@v4
with:
name: benchmark-results
path: |
comparison.md
pr_number.txt

204
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,204 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Releases are driven by manually pushed tags only. Nothing is published
# automatically when a pull request is merged.
#
# v<X>.<Y>.<Z>-rc<N> GitHub pre-release only.
# v<X>.<Y>.<Z> GitHub release plus the Go module proxy.
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
release:
if: ${{ github.repository == 'apache/casbin' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Compute release metadata
id: release
run: |
set -euo pipefail
TAG_NAME="${GITHUB_REF_NAME}"
if [[ "${TAG_NAME}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
IS_RC="false"
elif [[ "${TAG_NAME}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)-rc[0-9]+$ ]]; then
IS_RC="true"
else
echo "::error::Unsupported release tag: ${TAG_NAME}. Expected vX.Y.Z or vX.Y.Z-rcN."
exit 1
fi
VERSION="${BASH_REMATCH[1]}"
BASENAME="apache-casbin-incubating-${VERSION}-src"
# Previous release, used as the start of the change log range.
PREVIOUS_TAG="$(
{
git tag --list 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true
echo "v${VERSION}"
} |
sort -V -u |
awk -v current="v${VERSION}" '
$0 == current {
print previous
exit
}
{
previous = $0
}
'
)"
{
echo "tag=${TAG_NAME}"
echo "version=${VERSION}"
echo "is_rc=${IS_RC}"
echo "basename=${BASENAME}"
echo "previous_tag=${PREVIOUS_TAG}"
} >> "${GITHUB_OUTPUT}"
- name: Generate release notes
run: |
set -euo pipefail
CURRENT_TAG="${{ steps.release.outputs.tag }}"
CURRENT_VERSION="${CURRENT_TAG#v}"
PREVIOUS_TAG="${{ steps.release.outputs.previous_tag }}"
RELEASE_DATE="$(date -u +%F)"
RANGE="${CURRENT_TAG}"
if [ -n "${PREVIOUS_TAG}" ]; then
RANGE="${PREVIOUS_TAG}..${CURRENT_TAG}"
fi
FEATURES="$(git log --pretty=format:'%s (%h)' "${RANGE}" | grep -Ei '^(feat)(\(.+\))?: ' || true)"
FIXES="$(git log --pretty=format:'%s (%h)' "${RANGE}" | grep -Ei '^(fix)(\(.+\))?: ' || true)"
DOCS="$(git log --pretty=format:'%s (%h)' "${RANGE}" | grep -Ei '^(docs?|doc)(\(.+\))?: ' || true)"
{
if [ -n "${PREVIOUS_TAG}" ]; then
echo "# [${CURRENT_VERSION}](https://github.com/${GITHUB_REPOSITORY}/compare/${PREVIOUS_TAG}...${CURRENT_TAG}) (${RELEASE_DATE})"
else
echo "# ${CURRENT_VERSION} (${RELEASE_DATE})"
fi
echo
if [ "${{ steps.release.outputs.is_rc }}" = "true" ]; then
echo "Release candidate for ${{ steps.release.outputs.version }}. This is not an official release."
echo
fi
if [ -n "${FEATURES}" ]; then
echo "## Features"
echo
printf '%s\n' "${FEATURES}" | sed 's/^/- /'
echo
fi
if [ -n "${FIXES}" ]; then
echo "## Fixes"
echo
printf '%s\n' "${FIXES}" | sed 's/^/- /'
echo
fi
if [ -n "${DOCS}" ]; then
echo "## Docs"
echo
printf '%s\n' "${DOCS}" | sed 's/^/- /'
echo
fi
if [ -z "${FEATURES}${FIXES}${DOCS}" ]; then
echo "## Notes"
echo
echo "- No feat/fix/doc commits were found in this release range."
fi
} > RELEASE_NOTES.md
- name: Create source archives
run: |
set -euo pipefail
BASENAME="${{ steps.release.outputs.basename }}"
mkdir -p dist
git archive --format=tar.gz --prefix="${BASENAME}/" -o "dist/${BASENAME}.tar.gz" "${{ steps.release.outputs.tag }}"
git archive --format=zip --prefix="${BASENAME}/" -o "dist/${BASENAME}.zip" "${{ steps.release.outputs.tag }}"
cd dist
for archive in "${BASENAME}.tar.gz" "${BASENAME}.zip"; do
sha512sum "${archive}" > "${archive}.sha512"
done
sha512sum --check ./*.sha512
- name: Publish GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if [ "${{ steps.release.outputs.is_rc }}" = "true" ]; then
TITLE="Apache Casbin (Incubating) ${{ steps.release.outputs.version }} (release candidate ${{ steps.release.outputs.tag }})"
FLAGS=(--prerelease)
else
TITLE="Apache Casbin (Incubating) ${{ steps.release.outputs.version }}"
FLAGS=(--latest)
fi
gh release create "${{ steps.release.outputs.tag }}" \
--verify-tag \
--title "${TITLE}" \
--notes-file RELEASE_NOTES.md \
"${FLAGS[@]}" \
dist/*
- name: Publish to the Go module proxy
if: ${{ steps.release.outputs.is_rc == 'false' }}
run: |
set -euo pipefail
MODULE_PATH="$(awk '/^module /{print $2; exit}' go.mod)"
TAG="${{ steps.release.outputs.tag }}"
# Requesting the version makes the proxy fetch and cache it, which is
# what publishes the release to the Go module ecosystem.
for attempt in 1 2 3; do
if curl --fail --silent --show-error --location \
"https://proxy.golang.org/${MODULE_PATH}/@v/${TAG}.info"; then
echo
echo "Published ${MODULE_PATH}@${TAG} to proxy.golang.org."
exit 0
fi
echo "Attempt ${attempt} failed, retrying in 30s..."
sleep 30
done
echo "::error::Failed to publish ${MODULE_PATH}@${TAG} to proxy.golang.org."
exit 1

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
.idea/
*.iml
# vendor files
vendor

198
.golangci.yml Normal file
View File

@@ -0,0 +1,198 @@
# Based on https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322
# This code is licensed under the terms of the MIT license https://opensource.org/license/mit
# Copyright (c) 2021 Marat Reymers
version: "2"
linters:
default: none
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- cyclop
- dupl
- durationcheck
- errname
- exhaustive
- funlen
- gocheckcompilerdirectives
- gochecknoinits
- gochecksumtype
- gocyclo
- godot
- gomoddirectives
- gomodguard_v2
- goprintffuncname
- gosec
- govet
- ineffassign
- loggercheck
- makezero
- mirror
- musttag
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- nosprintfhostport
- predeclared
- promlinter
- protogetter
- reassign
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- testableexamples
- testifylint
- tparallel
- unconvert
- unused
- usestdlibvars
- wastedassign
- whitespace
settings:
cyclop:
max-complexity: 30
package-average: 10
errcheck:
check-type-assertions: true
exhaustive:
check:
- switch
- map
exhaustruct:
exclude:
- ^net/http.Client$
- ^net/http.Cookie$
- ^net/http.Request$
- ^net/http.Response$
- ^net/http.Server$
- ^net/http.Transport$
- ^net/url.URL$
- ^os/exec.Cmd$
- ^reflect.StructField$
- ^github.com/Shopify/sarama.Config$
- ^github.com/Shopify/sarama.ProducerMessage$
- ^github.com/mitchellh/mapstructure.DecoderConfig$
- ^github.com/prometheus/client_golang/.+Opts$
- ^github.com/spf13/cobra.Command$
- ^github.com/spf13/cobra.CompletionOptions$
- ^github.com/stretchr/testify/mock.Mock$
- ^github.com/testcontainers/testcontainers-go.+Request$
- ^github.com/testcontainers/testcontainers-go.FromDockerfile$
- ^golang.org/x/tools/go/analysis.Analyzer$
- ^google.golang.org/protobuf/.+Options$
- ^gopkg.in/yaml.v3.Node$
funlen:
lines: 100
statements: 50
ignore-comments: true
gocognit:
min-complexity: 20
gocritic:
settings:
captLocal:
paramsOnly: false
underef:
skipRecvDeref: false
gomodguard_v2:
blocked:
- module: github.com/golang/protobuf
recommendations:
- google.golang.org/protobuf
reason: see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules
- module: github.com/satori/go.uuid
recommendations:
- github.com/google/uuid
reason: satori's package is not maintained
- module: github.com/gofrs/uuid
recommendations:
- github.com/gofrs/uuid/v5
reason: gofrs' package was not go module before v5
govet:
disable:
- fieldalignment
enable-all: true
inamedparam:
skip-single-param: true
mnd:
ignored-functions:
- flag.Arg
- flag.Duration.*
- flag.Float.*
- flag.Int.*
- flag.Uint.*
- os.Chmod
- os.Mkdir.*
- os.OpenFile
- os.WriteFile
- prometheus.ExponentialBuckets.*
- prometheus.LinearBuckets
nakedret:
max-func-lines: 0
nolintlint:
require-explanation: true
require-specific: true
allow-no-explanation:
- funlen
- gocognit
- lll
perfsprint:
strconcat: false
revive:
rules:
- name: unused-parameter
disabled: true
rowserrcheck:
packages:
- github.com/jmoiron/sqlx
staticcheck:
checks:
- all
- -ST1003
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- godot
source: (noinspection|TODO)
- linters:
- gocritic
source: //noinspection
- linters:
- bodyclose
- dupl
- funlen
- goconst
- gosec
- noctx
- wrapcheck
path: _test\.go
- linters:
- perfsprint
text: fmt.Sprintf can be replaced with string addition
paths:
- third_party$
- builtin$
- examples$
issues:
max-same-issues: 50
formatters:
enable:
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

31
.rat-excludes Normal file
View File

@@ -0,0 +1,31 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Parser and test fixtures whose contents must remain unchanged.
examples/**
config/testdata/**
# Go module metadata. go.sum is generated by the Go toolchain.
go.mod
go.sum
# Source-control metadata.
**/.gitignore
# Third-party MIT-licensed golangci-lint configuration. Its license and
# attribution are recorded in the root LICENSE file.
**/.golangci.yml

35
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,35 @@
# How to contribute
The following is a set of guidelines for contributing to Apache Casbin and its libraries, which are hosted at [Apache Casbin organization at Github](https://github.com/apache).
This project adheres to the [Contributor Covenant 1.2.](https://www.contributor-covenant.org/version/1/2/0/code-of-conduct.html) By participating, you are expected to uphold this code. Please report unacceptable behavior to info@casbin.com.
## Questions
- We do our best to have an [up-to-date documentation](https://casbin.apache.org/docs/overview)
- [Stack Overflow](https://stackoverflow.com) is the best place to start if you have a question. Please use the [casbin tag](https://stackoverflow.com/tags/casbin/info) we are actively monitoring. We encourage you to use Stack Overflow specially for Modeling Access Control Problems, in order to build a shared knowledge base.
- You can also join our [Discord](https://discord.gg/S5UjpzGZjN).
## Reporting issues
Reporting issues are a great way to contribute to the project. We are perpetually grateful about a well-written, through bug report.
Before raising a new issue, check our [issue list](https://github.com/apache/casbin/issues) to determine if it already contains the problem that you are facing.
A good bug report shouldn't leave others needing to chase you for more information. Please be as detailed as possible. The following questions might serve as a template for writing a detailed report:
What were you trying to achieve?
What are the expected results?
What are the received results?
What are the steps to reproduce the issue?
In what environment did you encounter the issue?
Feature requests can also be submitted as issues.
## Pull requests
Good pull requests (e.g. patches, improvements, new features) are a fantastic help. They should remain focused in scope and avoid unrelated commits.
Please ask first before embarking on any significant pull request (e.g. implementing new features, refactoring code etc.), otherwise you risk spending a lot of time working on something that the maintainers might not want to merge into the project.
First add an issue to the project to discuss the improvement. Please adhere to the coding conventions used throughout the project. If in doubt, consult the [Effective Go style guide](https://golang.org/doc/effective_go.html).

5
DISCLAIMER Normal file
View File

@@ -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.

229
LICENSE Normal file
View File

@@ -0,0 +1,229 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
THIRD-PARTY COMPONENTS
The .golangci.yml configuration is based on
https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322
and is licensed under the MIT License.
MIT License
Copyright (c) 2021 Marat Reymers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

29
Makefile Normal file
View File

@@ -0,0 +1,29 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
SHELL = /bin/bash
default: lint test
test:
go test -race -v ./...
benchmark:
go test -bench=.
lint:
golangci-lint run --verbose

8
NOTICE Normal file
View File

@@ -0,0 +1,8 @@
Apache Casbin (Incubating)
Copyright 2026 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (https://www.apache.org/).
Portions of this software were originally developed by the Casbin project.
Copyright 2017-2026 The casbin Authors.

278
README.md Normal file
View File

@@ -0,0 +1,278 @@
<!--
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.
-->
Apache Casbin
====
[![Lint](https://github.com/apache/casbin/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/apache/casbin/actions/workflows/golangci-lint.yml)
[![Build](https://github.com/apache/casbin/actions/workflows/default.yml/badge.svg)](https://github.com/apache/casbin/actions/workflows/default.yml)
[![Coverage Status](https://coveralls.io/repos/github/apache/casbin/badge.svg?branch=master)](https://coveralls.io/github/apache/casbin?branch=master)
[![Godoc](https://godoc.org/github.com/apache/casbin?status.svg)](https://pkg.go.dev/github.com/casbin/casbin/v2)
[![Release](https://img.shields.io/github/release/apache/casbin.svg)](https://github.com/apache/casbin/releases/latest)
[![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN)
**News**: still worry about how to write the correct Apache Casbin policy? ``Apache Casbin online editor`` is coming to help! Try it at: https://casbin.apache.org/editor/
![casbin Logo](casbin-logo.png)
Apache Casbin is a powerful and efficient open-source access control library for Golang projects. It provides support for enforcing authorization based on various [access control models](https://en.wikipedia.org/wiki/Computer_security_model).
## All the languages supported by Apache Casbin:
| [![golang](https://casbin.apache.org/img/langs/golang.png)](https://github.com/apache/casbin) | [![java](https://casbin.apache.org/img/langs/java.png)](https://github.com/casbin/jcasbin) | [![nodejs](https://casbin.apache.org/img/langs/nodejs.png)](https://github.com/casbin/node-casbin) | [![php](https://casbin.apache.org/img/langs/php.png)](https://github.com/php-casbin/php-casbin) |
|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
| [Casbin](https://github.com/apache/casbin) | [jCasbin](https://github.com/casbin/jcasbin) | [node-Casbin](https://github.com/casbin/node-casbin) | [PHP-Casbin](https://github.com/php-casbin/php-casbin) |
| production-ready | production-ready | production-ready | production-ready |
| [![python](https://casbin.apache.org/img/langs/python.png)](https://github.com/casbin/pycasbin) | [![dotnet](https://casbin.apache.org/img/langs/dotnet.png)](https://github.com/casbin-net/Casbin.NET) | [![c++](https://casbin.apache.org/img/langs/cpp.png)](https://github.com/casbin/casbin-cpp) | [![rust](https://casbin.apache.org/img/langs/rust.png)](https://github.com/casbin/casbin-rs) |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| [PyCasbin](https://github.com/casbin/pycasbin) | [Casbin.NET](https://github.com/casbin-net/Casbin.NET) | [Casbin-CPP](https://github.com/casbin/casbin-cpp) | [Casbin-RS](https://github.com/casbin/casbin-rs) |
| production-ready | production-ready | production-ready | production-ready |
## Table of contents
- [Supported models](#supported-models)
- [How it works?](#how-it-works)
- [Features](#features)
- [Installation](#installation)
- [Documentation](#documentation)
- [Online editor](#online-editor)
- [Tutorials](#tutorials)
- [Get started](#get-started)
- [Policy management](#policy-management)
- [Policy persistence](#policy-persistence)
- [Policy consistence between multiple nodes](#policy-consistence-between-multiple-nodes)
- [Role manager](#role-manager)
- [Benchmarks](#benchmarks)
- [Examples](#examples)
- [Middlewares](#middlewares)
- [Our adopters](#our-adopters)
## Supported models
1. [**ACL (Access Control List)**](https://en.wikipedia.org/wiki/Access_control_list)
2. **ACL with [superuser](https://en.wikipedia.org/wiki/Superuser)**
3. **ACL without users**: especially useful for systems that don't have authentication or user log-ins.
3. **ACL without resources**: some scenarios may target for a type of resources instead of an individual resource by using permissions like ``write-article``, ``read-log``. It doesn't control the access to a specific article or log.
4. **[RBAC (Role-Based Access Control)](https://en.wikipedia.org/wiki/Role-based_access_control)**
5. **RBAC with resource roles**: both users and resources can have roles (or groups) at the same time.
6. **RBAC with domains/tenants**: users can have different role sets for different domains/tenants.
7. **[ABAC (Attribute-Based Access Control)](https://en.wikipedia.org/wiki/Attribute-Based_Access_Control)**: syntax sugar like ``resource.Owner`` can be used to get the attribute for a resource.
8. **[RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer)**: supports paths like ``/res/*``, ``/res/:id`` and HTTP methods like ``GET``, ``POST``, ``PUT``, ``DELETE``.
9. **Deny-override**: both allow and deny authorizations are supported, deny overrides the allow.
10. **Priority**: the policy rules can be prioritized like firewall rules.
## How it works?
In Casbin, an access control model is abstracted into a CONF file based on the **PERM metamodel (Policy, Effect, Request, Matchers)**. So switching or upgrading the authorization mechanism for a project is just as simple as modifying a configuration. You can customize your own access control model by combining the available models. For example, you can get RBAC roles and ABAC attributes together inside one model and share one set of policy rules.
The most basic and simplest model in Casbin is ACL. ACL's model CONF is:
```ini
# Request definition
[request_definition]
r = sub, obj, act
# Policy definition
[policy_definition]
p = sub, obj, act
# Policy effect
[policy_effect]
e = some(where (p.eft == allow))
# Matchers
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
```
An example policy for ACL model is like:
```
p, alice, data1, read
p, bob, data2, write
```
It means:
- alice can read data1
- bob can write data2
We also support multi-line mode by appending '\\' in the end:
```ini
# Matchers
[matchers]
m = r.sub == p.sub && r.obj == p.obj \
&& r.act == p.act
```
Further more, if you are using ABAC, you can try operator `in` like following in Casbin **golang** edition (jCasbin and Node-Casbin are not supported yet):
```ini
# Matchers
[matchers]
m = r.obj == p.obj && r.act == p.act || r.obj in ('data2', 'data3')
```
But you **SHOULD** make sure that the length of the array is **MORE** than **1**, otherwise there will cause it to panic.
For more operators, you may take a look at [govaluate](https://github.com/casbin/govaluate)
## Features
What Apache Casbin does:
1. enforce the policy in the classic ``{subject, object, action}`` form or a customized form as you defined, both allow and deny authorizations are supported.
2. handle the storage of the access control model and its policy.
3. manage the role-user mappings and role-role mappings (aka role hierarchy in RBAC).
4. support built-in superuser like ``root`` or ``administrator``. A superuser can do anything without explicit permissions.
5. multiple built-in operators to support the rule matching. For example, ``keyMatch`` can map a resource key ``/foo/bar`` to the pattern ``/foo*``.
What Apache Casbin does NOT do:
1. authentication (aka verify ``username`` and ``password`` when a user logs in)
2. manage the list of users or roles. I believe it's more convenient for the project itself to manage these entities. Users usually have their passwords, and Apache Casbin is not designed as a password container. However, Apache Casbin stores the user-role mapping for the RBAC scenario.
## Installation
```
go get github.com/casbin/casbin/v3
```
## Documentation
https://casbin.apache.org/docs/overview
## Online editor
You can also use the online editor (https://casbin.apache.org/editor/) to write your Casbin model and policy in your web browser. It provides functionality such as ``syntax highlighting`` and ``code completion``, just like an IDE for a programming language.
## Tutorials
https://casbin.apache.org/docs/tutorials
## Get started
1. New a Casbin enforcer with a model file and a policy file:
```go
e, _ := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")
```
Note: you can also initialize an enforcer with policy in DB instead of file, see [Policy-persistence](#policy-persistence) section for details.
2. Add an enforcement hook into your code right before the access happens:
```go
sub := "alice" // the user that wants to access a resource.
obj := "data1" // the resource that is going to be accessed.
act := "read" // the operation that the user performs on the resource.
if res, _ := e.Enforce(sub, obj, act); res {
// permit alice to read data1
} else {
// deny the request, show an error
}
```
3. Besides the static policy file, Apache Casbin also provides API for permission management at run-time. For example, You can get all the roles assigned to a user as below:
```go
roles, _ := e.GetImplicitRolesForUser(sub)
```
See [Policy management APIs](#policy-management) for more usage.
## Policy management
Apache Casbin provides two sets of APIs to manage permissions:
- [Management API](https://casbin.apache.org/docs/management-api): the primitive API that provides full support for Apache Casbin policy management.
- [RBAC API](https://casbin.apache.org/docs/rbac-api): a more friendly API for RBAC. This API is a subset of Management API. The RBAC users could use this API to simplify the code.
We also provide a [web-based UI](https://casbin.apache.org/docs/admin-portal) for model management and policy management:
![model editor](https://hsluoyz.github.io/casbin/ui_model_editor.png)
![policy editor](https://hsluoyz.github.io/casbin/ui_policy_editor.png)
## Policy persistence
https://casbin.apache.org/docs/adapters
## Policy consistence between multiple nodes
https://casbin.apache.org/docs/watchers
## Role manager
https://casbin.apache.org/docs/role-managers
## Benchmarks
https://casbin.apache.org/docs/benchmark
## Examples
| Model | Model file | Policy file |
|---------------------------|----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| ACL | [basic_model.conf](https://github.com/apache/casbin/blob/master/examples/basic_model.conf) | [basic_policy.csv](https://github.com/apache/casbin/blob/master/examples/basic_policy.csv) |
| ACL with superuser | [basic_model_with_root.conf](https://github.com/apache/casbin/blob/master/examples/basic_with_root_model.conf) | [basic_policy.csv](https://github.com/apache/casbin/blob/master/examples/basic_policy.csv) |
| ACL without users | [basic_model_without_users.conf](https://github.com/apache/casbin/blob/master/examples/basic_without_users_model.conf) | [basic_policy_without_users.csv](https://github.com/apache/casbin/blob/master/examples/basic_without_users_policy.csv) |
| ACL without resources | [basic_model_without_resources.conf](https://github.com/apache/casbin/blob/master/examples/basic_without_resources_model.conf) | [basic_policy_without_resources.csv](https://github.com/apache/casbin/blob/master/examples/basic_without_resources_policy.csv) |
| RBAC | [rbac_model.conf](https://github.com/apache/casbin/blob/master/examples/rbac_model.conf) | [rbac_policy.csv](https://github.com/apache/casbin/blob/master/examples/rbac_policy.csv) |
| RBAC with resource roles | [rbac_model_with_resource_roles.conf](https://github.com/apache/casbin/blob/master/examples/rbac_with_resource_roles_model.conf) | [rbac_policy_with_resource_roles.csv](https://github.com/apache/casbin/blob/master/examples/rbac_with_resource_roles_policy.csv) |
| RBAC with domains/tenants | [rbac_model_with_domains.conf](https://github.com/apache/casbin/blob/master/examples/rbac_with_domains_model.conf) | [rbac_policy_with_domains.csv](https://github.com/apache/casbin/blob/master/examples/rbac_with_domains_policy.csv) |
| ABAC | [abac_model.conf](https://github.com/apache/casbin/blob/master/examples/abac_model.conf) | N/A |
| RESTful | [keymatch_model.conf](https://github.com/apache/casbin/blob/master/examples/keymatch_model.conf) | [keymatch_policy.csv](https://github.com/apache/casbin/blob/master/examples/keymatch_policy.csv) |
| Deny-override | [rbac_model_with_deny.conf](https://github.com/apache/casbin/blob/master/examples/rbac_with_deny_model.conf) | [rbac_policy_with_deny.csv](https://github.com/apache/casbin/blob/master/examples/rbac_with_deny_policy.csv) |
| Priority | [priority_model.conf](https://github.com/apache/casbin/blob/master/examples/priority_model.conf) | [priority_policy.csv](https://github.com/apache/casbin/blob/master/examples/priority_policy.csv) |
## Middlewares
Authz middlewares for web frameworks: https://casbin.apache.org/docs/middlewares
## Our adopters
https://casbin.apache.org/docs/adopters
## How to Contribute
Please read the [contributing guide](CONTRIBUTING.md).
## Contributors
This project exists thanks to all the people who contribute.
<a href="https://github.com/apache/casbin/graphs/contributors"><img src="https://opencollective.com/casbin/contributors.svg?width=890&button=false" /></a>
## Star History
[![Star History Chart](https://star-history.dera.page/svg?repos=apache/casbin&type=Date)](https://star-history.dera.page/#apache/casbin&Date)
## License
This project is licensed under the [Apache 2.0 license](LICENSE).
## Contact
If you have any issues or feature requests, please contact us. PR is welcomed.
- https://github.com/apache/casbin/issues
- https://discord.gg/S5UjpzGZjN

201
abac_test.go Normal file
View File

@@ -0,0 +1,201 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"encoding/json"
"fmt"
"testing"
"github.com/casbin/casbin/v3/util"
)
type testResource struct {
Name string
Owner string
}
func newTestResource(name string, owner string) testResource {
r := testResource{}
r.Name = name
r.Owner = owner
return r
}
func TestABACModel(t *testing.T) {
e, _ := NewEnforcer("examples/abac_model.conf")
data1 := newTestResource("data1", "alice")
data2 := newTestResource("data2", "bob")
testEnforce(t, e, "alice", data1, "read", true)
testEnforce(t, e, "alice", data1, "write", true)
testEnforce(t, e, "alice", data2, "read", false)
testEnforce(t, e, "alice", data2, "write", false)
testEnforce(t, e, "bob", data1, "read", false)
testEnforce(t, e, "bob", data1, "write", false)
testEnforce(t, e, "bob", data2, "read", true)
testEnforce(t, e, "bob", data2, "write", true)
}
func TestABACMapRequest(t *testing.T) {
e, _ := NewEnforcer("examples/abac_model.conf")
data1 := map[string]interface{}{
"Name": "data1",
"Owner": "alice",
}
data2 := map[string]interface{}{
"Name": "data2",
"Owner": "bob",
}
testEnforce(t, e, "alice", data1, "read", true)
testEnforce(t, e, "alice", data1, "write", true)
testEnforce(t, e, "alice", data2, "read", false)
testEnforce(t, e, "alice", data2, "write", false)
testEnforce(t, e, "bob", data1, "read", false)
testEnforce(t, e, "bob", data1, "write", false)
testEnforce(t, e, "bob", data2, "read", true)
testEnforce(t, e, "bob", data2, "write", true)
}
func TestABACTypes(t *testing.T) {
e, _ := NewEnforcer("examples/abac_model.conf")
matcher := `"moderator" IN r.sub.Roles && r.sub.Enabled == true && r.sub.Age >= 21 && r.sub.Name != "foo"`
e.GetModel()["m"]["m"].Value = util.RemoveComments(util.EscapeAssertion(matcher))
structRequest := struct {
Roles []interface{}
Enabled bool
Age int
Name string
}{
Roles: []interface{}{"user", "moderator"},
Enabled: true,
Age: 30,
Name: "alice",
}
testEnforce(t, e, structRequest, "", "", true)
mapRequest := map[string]interface{}{
"Roles": []interface{}{"user", "moderator"},
"Enabled": true,
"Age": 30,
"Name": "alice",
}
testEnforce(t, e, mapRequest, nil, "", true)
e.EnableAcceptJsonRequest(true)
jsonRequest, _ := json.Marshal(mapRequest)
testEnforce(t, e, string(jsonRequest), "", "", true)
}
func TestABACJsonRequest(t *testing.T) {
e, _ := NewEnforcer("examples/abac_model.conf")
e.EnableAcceptJsonRequest(true)
data1Json := `{ "Name": "data1", "Owner": "alice"}`
data2Json := `{ "Name": "data2", "Owner": "bob"}`
testEnforce(t, e, "alice", data1Json, "read", true)
testEnforce(t, e, "alice", data1Json, "write", true)
testEnforce(t, e, "alice", data2Json, "read", false)
testEnforce(t, e, "alice", data2Json, "write", false)
testEnforce(t, e, "bob", data1Json, "read", false)
testEnforce(t, e, "bob", data1Json, "write", false)
testEnforce(t, e, "bob", data2Json, "read", true)
testEnforce(t, e, "bob", data2Json, "write", true)
e, _ = NewEnforcer("examples/abac_not_using_policy_model.conf", "examples/abac_rule_effect_policy.csv")
e.EnableAcceptJsonRequest(true)
testEnforce(t, e, "alice", data1Json, "read", true)
testEnforce(t, e, "alice", data1Json, "write", true)
testEnforce(t, e, "alice", data2Json, "read", false)
testEnforce(t, e, "alice", data2Json, "write", false)
e, _ = NewEnforcer("examples/abac_rule_model.conf", "examples/abac_rule_policy.csv")
e.EnableAcceptJsonRequest(true)
sub1Json := `{"Name": "alice", "Age": 16}`
sub2Json := `{"Name": "alice", "Age": 20}`
sub3Json := `{"Name": "alice", "Age": 65}`
testEnforce(t, e, sub1Json, "/data1", "read", false)
testEnforce(t, e, sub1Json, "/data2", "read", false)
testEnforce(t, e, sub1Json, "/data1", "write", false)
testEnforce(t, e, sub1Json, "/data2", "write", true)
testEnforce(t, e, sub2Json, "/data1", "read", true)
testEnforce(t, e, sub2Json, "/data2", "read", false)
testEnforce(t, e, sub2Json, "/data1", "write", false)
testEnforce(t, e, sub2Json, "/data2", "write", true)
testEnforce(t, e, sub3Json, "/data1", "read", true)
testEnforce(t, e, sub3Json, "/data2", "read", false)
testEnforce(t, e, sub3Json, "/data1", "write", false)
testEnforce(t, e, sub3Json, "/data2", "write", false)
}
type testSub struct {
Name string
Age int
}
func newTestSubject(name string, age int) testSub {
s := testSub{}
s.Name = name
s.Age = age
return s
}
func TestABACNotUsingPolicy(t *testing.T) {
e, _ := NewEnforcer("examples/abac_not_using_policy_model.conf", "examples/abac_rule_effect_policy.csv")
data1 := newTestResource("data1", "alice")
data2 := newTestResource("data2", "bob")
testEnforce(t, e, "alice", data1, "read", true)
testEnforce(t, e, "alice", data1, "write", true)
testEnforce(t, e, "alice", data2, "read", false)
testEnforce(t, e, "alice", data2, "write", false)
}
func TestABACPolicy(t *testing.T) {
e, _ := NewEnforcer("examples/abac_rule_model.conf", "examples/abac_rule_policy.csv")
m := e.GetModel()
for sec, ast := range m {
fmt.Println(sec)
for ptype, p := range ast {
fmt.Println(ptype, p)
}
}
sub1 := newTestSubject("alice", 16)
sub2 := newTestSubject("alice", 20)
sub3 := newTestSubject("alice", 65)
testEnforce(t, e, sub1, "/data1", "read", false)
testEnforce(t, e, sub1, "/data2", "read", false)
testEnforce(t, e, sub1, "/data1", "write", false)
testEnforce(t, e, sub1, "/data2", "write", true)
testEnforce(t, e, sub2, "/data1", "read", true)
testEnforce(t, e, sub2, "/data2", "read", false)
testEnforce(t, e, sub2, "/data1", "write", false)
testEnforce(t, e, sub2, "/data2", "write", true)
testEnforce(t, e, sub3, "/data1", "read", true)
testEnforce(t, e, sub3, "/data2", "read", false)
testEnforce(t, e, sub3, "/data1", "write", false)
testEnforce(t, e, sub3, "/data2", "write", false)
}

224
ai_api.go Normal file
View File

@@ -0,0 +1,224 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// AIConfig contains configuration for AI API calls.
type AIConfig struct {
// Endpoint is the API endpoint (e.g., "https://api.openai.com/v1/chat/completions")
Endpoint string
// APIKey is the authentication key for the API
APIKey string
// Model is the model to use (e.g., "gpt-3.5-turbo", "gpt-4")
Model string
// Timeout for API requests (default: 30s)
Timeout time.Duration
}
// aiMessage represents a message in the OpenAI chat format.
type aiMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
// aiChatRequest represents the request to OpenAI chat completions API.
type aiChatRequest struct {
Model string `json:"model"`
Messages []aiMessage `json:"messages"`
}
// aiChatResponse represents the response from OpenAI chat completions API.
type aiChatResponse struct {
Choices []struct {
Message aiMessage `json:"message"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
// SetAIConfig sets the configuration for AI API calls.
func (e *Enforcer) SetAIConfig(config AIConfig) {
if config.Timeout == 0 {
config.Timeout = 30 * time.Second
}
e.aiConfig = config
}
// Explain returns an AI-generated explanation of why Enforce returned a particular result.
// It calls the configured OpenAI-compatible API to generate a natural language explanation.
func (e *Enforcer) Explain(rvals ...interface{}) (string, error) {
if e.aiConfig.Endpoint == "" {
return "", errors.New("AI config not set, use SetAIConfig first")
}
// Get enforcement result and matched rules
result, matchedRules, err := e.EnforceEx(rvals...)
if err != nil {
return "", fmt.Errorf("failed to enforce: %w", err)
}
// Build context for AI
explainContext := e.buildExplainContext(rvals, result, matchedRules)
// Call AI API
explanation, err := e.callAIAPI(explainContext)
if err != nil {
return "", fmt.Errorf("failed to get AI explanation: %w", err)
}
return explanation, nil
}
// buildExplainContext builds the context string for AI explanation.
func (e *Enforcer) buildExplainContext(rvals []interface{}, result bool, matchedRules []string) string {
var sb strings.Builder
// Add request information
sb.WriteString("Authorization Request:\n")
fmt.Fprintf(&sb, "Subject: %v\n", rvals[0])
if len(rvals) > 1 {
fmt.Fprintf(&sb, "Object: %v\n", rvals[1])
}
if len(rvals) > 2 {
fmt.Fprintf(&sb, "Action: %v\n", rvals[2])
}
fmt.Fprintf(&sb, "\nEnforcement Result: %v\n", result)
// Add matched rules
if len(matchedRules) > 0 {
sb.WriteString("\nMatched Policy Rules:\n")
for _, rule := range matchedRules {
fmt.Fprintf(&sb, "- %s\n", rule)
}
} else {
sb.WriteString("\nNo policy rules matched.\n")
}
// Add model information
sb.WriteString("\nAccess Control Model:\n")
if m, ok := e.model["m"]; ok {
for key, ast := range m {
fmt.Fprintf(&sb, "Matcher (%s): %s\n", key, ast.Value)
}
}
if eff, ok := e.model["e"]; ok {
for key, ast := range eff {
fmt.Fprintf(&sb, "Effect (%s): %s\n", key, ast.Value)
}
}
// Add all policies
policies, _ := e.GetPolicy()
if len(policies) > 0 {
sb.WriteString("\nAll Policy Rules:\n")
for _, policy := range policies {
fmt.Fprintf(&sb, "- %s\n", strings.Join(policy, ", "))
}
}
return sb.String()
}
// callAIAPI calls the configured AI API to get an explanation.
func (e *Enforcer) callAIAPI(explainContext string) (string, error) {
// Prepare the request
messages := []aiMessage{
{
Role: "system",
Content: "You are an expert in access control and authorization systems. " +
"Explain why an authorization request was allowed or denied based on the " +
"provided access control model, policies, and enforcement result. " +
"Be clear, concise, and educational.",
},
{
Role: "user",
Content: fmt.Sprintf("Please explain the following authorization decision:\n\n%s", explainContext),
},
}
reqBody := aiChatRequest{
Model: e.aiConfig.Model,
Messages: messages,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
// Create HTTP request with context
reqCtx, cancel := context.WithTimeout(context.Background(), e.aiConfig.Timeout)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, e.aiConfig.Endpoint, bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+e.aiConfig.APIKey)
// Execute request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
// Parse response
var chatResp aiChatResponse
if err := json.Unmarshal(body, &chatResp); err != nil {
return "", fmt.Errorf("failed to parse response: %w", err)
}
// Check for API errors
if chatResp.Error != nil {
return "", fmt.Errorf("API error: %s", chatResp.Error.Message)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
// Extract explanation
if len(chatResp.Choices) == 0 {
return "", errors.New("no response from AI")
}
return chatResp.Choices[0].Message.Content, nil
}

253
ai_api_test.go Normal file
View File

@@ -0,0 +1,253 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// TestExplainWithoutConfig tests that Explain returns error when config is not set.
func TestExplainWithoutConfig(t *testing.T) {
e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
if err != nil {
t.Fatal(err)
}
_, err = e.Explain("alice", "data1", "read")
if err == nil {
t.Error("Expected error when AI config is not set")
}
if !strings.Contains(err.Error(), "AI config not set") {
t.Errorf("Expected 'AI config not set' error, got: %v", err)
}
}
// TestExplainWithMockAPI tests Explain with a mock OpenAI-compatible API.
func TestExplainWithMockAPI(t *testing.T) {
// Create a mock server that simulates OpenAI API
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != http.MethodPost {
t.Errorf("Expected POST request, got %s", r.Method)
}
if r.Header.Get("Content-Type") != "application/json" {
t.Errorf("Expected Content-Type: application/json, got %s", r.Header.Get("Content-Type"))
}
if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
t.Errorf("Expected Bearer token in Authorization header, got %s", r.Header.Get("Authorization"))
}
// Parse request to verify structure
var req aiChatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Errorf("Failed to decode request: %v", err)
}
if req.Model != "gpt-3.5-turbo" {
t.Errorf("Expected model gpt-3.5-turbo, got %s", req.Model)
}
if len(req.Messages) != 2 {
t.Errorf("Expected 2 messages, got %d", len(req.Messages))
}
// Send mock response
resp := aiChatResponse{
Choices: []struct {
Message aiMessage `json:"message"`
}{
{
Message: aiMessage{
Role: "assistant",
Content: "The request was allowed because alice has read permission on data1 according to the policy rule.",
},
},
},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}))
defer mockServer.Close()
// Create enforcer
e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
if err != nil {
t.Fatal(err)
}
// Set AI config with mock server
e.SetAIConfig(AIConfig{
Endpoint: mockServer.URL,
APIKey: "test-api-key",
Model: "gpt-3.5-turbo",
Timeout: 5 * time.Second,
})
// Test explanation for allowed request
explanation, err := e.Explain("alice", "data1", "read")
if err != nil {
t.Fatalf("Failed to get explanation: %v", err)
}
if explanation == "" {
t.Error("Expected non-empty explanation")
}
if !strings.Contains(explanation, "allowed") {
t.Errorf("Expected explanation to mention 'allowed', got: %s", explanation)
}
}
// TestExplainDenied tests Explain for a denied request.
func TestExplainDenied(t *testing.T) {
// Create a mock server
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := aiChatResponse{
Choices: []struct {
Message aiMessage `json:"message"`
}{
{
Message: aiMessage{
Role: "assistant",
Content: "The request was denied because there is no policy rule that allows alice to write to data1.",
},
},
},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}))
defer mockServer.Close()
// Create enforcer
e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
if err != nil {
t.Fatal(err)
}
// Set AI config
e.SetAIConfig(AIConfig{
Endpoint: mockServer.URL,
APIKey: "test-api-key",
Model: "gpt-3.5-turbo",
Timeout: 5 * time.Second,
})
// Test explanation for denied request
explanation, err := e.Explain("alice", "data1", "write")
if err != nil {
t.Fatalf("Failed to get explanation: %v", err)
}
if explanation == "" {
t.Error("Expected non-empty explanation")
}
if !strings.Contains(explanation, "denied") {
t.Errorf("Expected explanation to mention 'denied', got: %s", explanation)
}
}
// TestExplainAPIError tests handling of API errors.
func TestExplainAPIError(t *testing.T) {
// Create a mock server that returns an error
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := aiChatResponse{
Error: &struct {
Message string `json:"message"`
}{
Message: "Invalid API key",
},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(resp)
}))
defer mockServer.Close()
// Create enforcer
e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
if err != nil {
t.Fatal(err)
}
// Set AI config
e.SetAIConfig(AIConfig{
Endpoint: mockServer.URL,
APIKey: "invalid-key",
Model: "gpt-3.5-turbo",
Timeout: 5 * time.Second,
})
// Test that API error is properly handled
_, err = e.Explain("alice", "data1", "read")
if err == nil {
t.Error("Expected error for API failure")
}
if !strings.Contains(err.Error(), "Invalid API key") {
t.Errorf("Expected API error message, got: %v", err)
}
}
// TestBuildExplainContext tests the context building function.
func TestBuildExplainContext(t *testing.T) {
e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
if err != nil {
t.Fatal(err)
}
// Test with matched rules
rvals := []interface{}{"alice", "data1", "read"}
result := true
matchedRules := []string{"alice, data1, read"}
context := e.buildExplainContext(rvals, result, matchedRules)
// Verify context contains expected elements
if !strings.Contains(context, "alice") {
t.Error("Context should contain subject 'alice'")
}
if !strings.Contains(context, "data1") {
t.Error("Context should contain object 'data1'")
}
if !strings.Contains(context, "read") {
t.Error("Context should contain action 'read'")
}
if !strings.Contains(context, "true") {
t.Error("Context should contain result 'true'")
}
if !strings.Contains(context, "alice, data1, read") {
t.Error("Context should contain matched rule")
}
// Test with no matched rules
context2 := e.buildExplainContext(rvals, false, []string{})
if !strings.Contains(context2, "No policy rules matched") {
t.Error("Context should indicate no matched rules")
}
}

47
biba_test.go Normal file
View File

@@ -0,0 +1,47 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"testing"
)
func testEnforceBiba(t *testing.T, e *Enforcer, sub string, subLevel float64, obj string, objLevel float64, act string, res bool) {
t.Helper()
if myRes, err := e.Enforce(sub, subLevel, obj, objLevel, act); err != nil {
t.Errorf("Enforce Error: %s", err)
} else if myRes != res {
t.Errorf("%s, %v, %s, %v, %s: %t, supposed to be %t", sub, subLevel, obj, objLevel, act, myRes, res)
}
}
func TestBibaModel(t *testing.T) {
e, _ := NewEnforcer("examples/biba_model.conf")
testEnforceBiba(t, e, "alice", 3, "data1", 1, "read", false)
testEnforceBiba(t, e, "bob", 2, "data2", 2, "read", true)
testEnforceBiba(t, e, "charlie", 1, "data1", 1, "read", true)
testEnforceBiba(t, e, "bob", 2, "data3", 3, "read", true)
testEnforceBiba(t, e, "charlie", 1, "data2", 2, "read", true)
testEnforceBiba(t, e, "alice", 3, "data3", 3, "write", true)
testEnforceBiba(t, e, "bob", 2, "data3", 3, "write", false)
testEnforceBiba(t, e, "charlie", 1, "data2", 2, "write", false)
testEnforceBiba(t, e, "alice", 3, "data1", 1, "write", true)
testEnforceBiba(t, e, "bob", 2, "data1", 1, "write", true)
}

53
blp_test.go Normal file
View File

@@ -0,0 +1,53 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"testing"
)
func testEnforceBLP(t *testing.T, e *Enforcer, sub string, subLevel float64, obj string, objLevel float64, act string, res bool) {
t.Helper()
if myRes, err := e.Enforce(sub, subLevel, obj, objLevel, act); err != nil {
t.Errorf("Enforce Error: %s", err)
} else if myRes != res {
t.Errorf("%s, %v, %s, %v, %s: %t, supposed to be %t", sub, subLevel, obj, objLevel, act, myRes, res)
}
}
func TestBLPModel(t *testing.T) {
e, _ := NewEnforcer("examples/blp_model.conf")
// Read operations: subject level >= object level
testEnforceBLP(t, e, "alice", 3, "data1", 1, "read", true)
testEnforceBLP(t, e, "bob", 2, "data2", 2, "read", true)
testEnforceBLP(t, e, "charlie", 1, "data1", 1, "read", true)
// Read violations: subject level < object level
testEnforceBLP(t, e, "bob", 2, "data3", 3, "read", false)
testEnforceBLP(t, e, "charlie", 1, "data2", 2, "read", false)
// Write operations: subject level <= object level
testEnforceBLP(t, e, "alice", 3, "data3", 3, "write", true)
testEnforceBLP(t, e, "bob", 2, "data3", 3, "write", true)
testEnforceBLP(t, e, "charlie", 1, "data2", 2, "write", true)
// Write violations: subject level > object level
testEnforceBLP(t, e, "alice", 3, "data1", 1, "write", false)
testEnforceBLP(t, e, "bob", 2, "data1", 1, "write", false)
}

BIN
casbin-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

270
config/config.go Normal file
View File

@@ -0,0 +1,270 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package config
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
)
var (
// DEFAULT_SECTION specifies the name of a section if no name provided.
DEFAULT_SECTION = "default"
// DEFAULT_COMMENT defines what character(s) indicate a comment `#`.
DEFAULT_COMMENT = []byte{'#'}
// DEFAULT_COMMENT_SEM defines what alternate character(s) indicate a comment `;`.
DEFAULT_COMMENT_SEM = []byte{';'}
// DEFAULT_MULTI_LINE_SEPARATOR defines what character indicates a multi-line content.
DEFAULT_MULTI_LINE_SEPARATOR = []byte{'\\'}
)
// ConfigInterface defines the behavior of a Config implementation.
type ConfigInterface interface {
String(key string) string
Strings(key string) []string
Bool(key string) (bool, error)
Int(key string) (int, error)
Int64(key string) (int64, error)
Float64(key string) (float64, error)
Set(key string, value string) error
}
// Config represents an implementation of the ConfigInterface.
type Config struct {
// Section:key=value
data map[string]map[string]string
}
// NewConfig create an empty configuration representation from file.
func NewConfig(confName string) (ConfigInterface, error) {
c := &Config{
data: make(map[string]map[string]string),
}
err := c.parse(confName)
return c, err
}
// NewConfigFromText create an empty configuration representation from text.
func NewConfigFromText(text string) (ConfigInterface, error) {
c := &Config{
data: make(map[string]map[string]string),
}
err := c.parseBuffer(bufio.NewReader(strings.NewReader(text)))
return c, err
}
// AddConfig adds a new section->key:value to the configuration.
func (c *Config) AddConfig(section string, option string, value string) bool {
if section == "" {
section = DEFAULT_SECTION
}
if _, ok := c.data[section]; !ok {
c.data[section] = make(map[string]string)
}
_, ok := c.data[section][option]
c.data[section][option] = value
return !ok
}
func (c *Config) parse(fname string) (err error) {
f, err := os.Open(fname)
if err != nil {
return err
}
defer f.Close()
buf := bufio.NewReader(f)
return c.parseBuffer(buf)
}
func (c *Config) parseBuffer(buf *bufio.Reader) error {
var section string
var lineNum int
var buffer bytes.Buffer
var canWrite bool
for {
if canWrite {
if err := c.write(section, lineNum, &buffer); err != nil {
return err
} else {
canWrite = false
}
}
lineNum++
line, _, err := buf.ReadLine()
if err == io.EOF {
// force write when buffer is not flushed yet
if buffer.Len() > 0 {
if err = c.write(section, lineNum, &buffer); err != nil {
return err
}
}
break
} else if err != nil {
return err
}
line = bytes.TrimSpace(line)
switch {
case bytes.Equal(line, []byte{}), bytes.HasPrefix(line, DEFAULT_COMMENT_SEM),
bytes.HasPrefix(line, DEFAULT_COMMENT):
canWrite = true
continue
case bytes.HasPrefix(line, []byte{'['}) && bytes.HasSuffix(line, []byte{']'}):
// force write when buffer is not flushed yet
if buffer.Len() > 0 {
if err := c.write(section, lineNum, &buffer); err != nil {
return err
}
canWrite = false
}
section = string(line[1 : len(line)-1])
default:
var p []byte
if bytes.HasSuffix(line, DEFAULT_MULTI_LINE_SEPARATOR) {
p = bytes.TrimSpace(line[:len(line)-1])
p = append(p, " "...)
} else {
p = line
canWrite = true
}
end := len(p)
for i, value := range p {
if value == DEFAULT_COMMENT[0] || value == DEFAULT_COMMENT_SEM[0] {
end = i
break
}
}
if _, err := buffer.Write(p[:end]); err != nil {
return err
}
}
}
return nil
}
func (c *Config) write(section string, lineNum int, b *bytes.Buffer) error {
if b.Len() <= 0 {
return nil
}
optionVal := bytes.SplitN(b.Bytes(), []byte{'='}, 2)
if len(optionVal) != 2 {
return fmt.Errorf("parse the content error : line %d , %s = ? ", lineNum, optionVal[0])
}
option := bytes.TrimSpace(optionVal[0])
value := bytes.TrimSpace(optionVal[1])
c.AddConfig(section, string(option), string(value))
// flush buffer after adding
b.Reset()
return nil
}
// Bool lookups up the value using the provided key and converts the value to a bool.
func (c *Config) Bool(key string) (bool, error) {
return strconv.ParseBool(c.get(key))
}
// Int lookups up the value using the provided key and converts the value to a int.
func (c *Config) Int(key string) (int, error) {
return strconv.Atoi(c.get(key))
}
// Int64 lookups up the value using the provided key and converts the value to a int64.
func (c *Config) Int64(key string) (int64, error) {
return strconv.ParseInt(c.get(key), 10, 64)
}
// Float64 lookups up the value using the provided key and converts the value to a float64.
func (c *Config) Float64(key string) (float64, error) {
return strconv.ParseFloat(c.get(key), 64)
}
// String lookups up the value using the provided key and converts the value to a string.
func (c *Config) String(key string) string {
return c.get(key)
}
// Strings lookups up the value using the provided key and converts the value to an array of string
// by splitting the string by comma.
func (c *Config) Strings(key string) []string {
v := c.get(key)
if v == "" {
return nil
}
return strings.Split(v, ",")
}
// Set sets the value for the specific key in the Config.
func (c *Config) Set(key string, value string) error {
if len(key) == 0 {
return errors.New("key is empty")
}
var (
section string
option string
)
keys := strings.Split(strings.ToLower(key), "::")
if len(keys) >= 2 {
section = keys[0]
option = keys[1]
} else {
option = keys[0]
}
c.AddConfig(section, option, value)
return nil
}
// section.key or key.
func (c *Config) get(key string) string {
var (
section string
option string
)
keys := strings.Split(strings.ToLower(key), "::")
if len(keys) >= 2 {
section = keys[0]
option = keys[1]
} else {
section = DEFAULT_SECTION
option = keys[0]
}
if value, ok := c.data[section][option]; ok {
return value
}
return ""
}

93
config/config_test.go Normal file
View File

@@ -0,0 +1,93 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package config
import (
"testing"
)
func TestGet(t *testing.T) {
config, cerr := NewConfig("testdata/testini.ini")
if cerr != nil {
t.Errorf("Configuration file loading failed, err:%v", cerr.Error())
t.Fatalf("err: %v", cerr)
}
// default::key test
if v, err := config.Bool("debug"); err != nil || !v {
t.Errorf("Get failure: expected different value for debug (expected: [%#v] got: [%#v])", true, v)
t.Fatalf("err: %v", err)
}
if v := config.String("url"); v != "act.wiki" {
t.Errorf("Get failure: expected different value for url (expected: [%#v] got: [%#v])", "act.wiki", v)
}
// redis::key test
if v := config.Strings("redis::redis.key"); len(v) != 2 || v[0] != "push1" || v[1] != "push2" {
t.Errorf("Get failure: expected different value for redis::redis.key (expected: [%#v] got: [%#v])", "[]string{push1,push2}", v)
}
if v := config.String("mysql::mysql.dev.host"); v != "127.0.0.1" {
t.Errorf("Get failure: expected different value for mysql::mysql.dev.host (expected: [%#v] got: [%#v])", "127.0.0.1", v)
}
if v := config.String("mysql::mysql.master.host"); v != "10.0.0.1" {
t.Errorf("Get failure: expected different value for mysql::mysql.master.host (expected: [%#v] got: [%#v])", "10.0.0.1", v)
}
if v := config.String("mysql::mysql.master.user"); v != "root" {
t.Errorf("Get failure: expected different value for mysql::mysql.master.user (expected: [%#v] got: [%#v])", "root", v)
}
if v := config.String("mysql::mysql.master.pass"); v != "89dds)2$" {
t.Errorf("Get failure: expected different value for mysql::mysql.master.pass (expected: [%#v] got: [%#v])", "89dds)2$", v)
}
// math::key test
if v, err := config.Int64("math::math.i64"); err != nil || v != 64 {
t.Errorf("Get failure: expected different value for math::math.i64 (expected: [%#v] got: [%#v])", 64, v)
t.Fatalf("err: %v", err)
}
if v, err := config.Float64("math::math.f64"); err != nil || v != 64.1 {
t.Errorf("Get failure: expected different value for math::math.f64 (expected: [%#v] got: [%#v])", 64.1, v)
t.Fatalf("err: %v", err)
}
_ = config.Set("other::key1", "new test key")
if v := config.String("other::key1"); v != "new test key" {
t.Errorf("Get failure: expected different value for other::key1 (expected: [%#v] got: [%#v])", "new test key", v)
}
_ = config.Set("other::key1", "test key")
if v := config.String("multi1::name"); v != "r.sub==p.sub && r.obj==p.obj" {
t.Errorf("Get failure: expected different value for multi1::name (expected: [%#v] got: [%#v])", "r.sub==p.sub&&r.obj==p.obj", v)
}
if v := config.String("multi2::name"); v != "r.sub==p.sub && r.obj==p.obj" {
t.Errorf("Get failure: expected different value for multi2::name (expected: [%#v] got: [%#v])", "r.sub==p.sub&&r.obj==p.obj", v)
}
if v := config.String("multi3::name"); v != "r.sub==p.sub && r.obj==p.obj" {
t.Errorf("Get failure: expected different value for multi3::name (expected: [%#v] got: [%#v])", "r.sub==p.sub&&r.obj==p.obj", v)
}
if v := config.String("multi4::name"); v != "" {
t.Errorf("Get failure: expected different value for multi4::name (expected: [%#v] got: [%#v])", "", v)
}
if v := config.String("multi5::name"); v != "r.sub==p.sub && r.obj==p.obj" {
t.Errorf("Get failure: expected different value for multi5::name (expected: [%#v] got: [%#v])", "r.sub==p.sub&&r.obj==p.obj", v)
}
}

47
config/testdata/testini.ini vendored Normal file
View File

@@ -0,0 +1,47 @@
# test config
debug = true
url = act.wiki
; redis config
[redis]
redis.key = push1,push2
; mysql config
[mysql]
mysql.dev.host = 127.0.0.1
mysql.dev.user = root
mysql.dev.pass = 123456
mysql.dev.db = test
mysql.master.host = 10.0.0.1 # host # 10.0.0.1
mysql.master.user = root ; user name
mysql.master.pass = 89dds)2$#d
mysql.master.db = act
; math config
[math]
math.i64 = 64
math.f64 = 64.1
# multi-line test
[multi1]
name = r.sub==p.sub \
&& r.obj==p.obj\
\
[multi2]
name = r.sub==p.sub \
&& r.obj==p.obj
[multi3]
name = r.sub==p.sub \
&& r.obj==p.obj
[multi4]
name = \
\
\
[multi5]
name = r.sub==p.sub \
&& r.obj==p.obj\
\

34
constant/constants.go Normal file
View File

@@ -0,0 +1,34 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package constant
const (
ActionIndex = "act"
DomainIndex = "dom"
SubjectIndex = "sub"
ObjectIndex = "obj"
PriorityIndex = "priority"
)
const (
AllowOverrideEffect = "some(where (p_eft == allow))"
DenyOverrideEffect = "!some(where (p_eft == deny))"
AllowAndDenyEffect = "some(where (p_eft == allow)) && !some(where (p_eft == deny))"
PriorityEffect = "priority(p_eft) || deny"
SubjectPriorityEffect = "subjectPriority(p_eft) || deny"
)

336
constraint_test.go Normal file
View File

@@ -0,0 +1,336 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"strings"
"testing"
"github.com/casbin/casbin/v3/errors"
"github.com/casbin/casbin/v3/model"
)
func TestConstraintSOD(t *testing.T) {
modelText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[constraint_definition]
c = sod("role1", "role2")
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`
m, err := model.NewModelFromString(modelText)
if err != nil {
t.Fatalf("Failed to create model: %v", err)
}
e, err := NewEnforcer(m)
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}
// Add a user to role1 should succeed
_, err = e.AddRoleForUser("alice", "role1")
if err != nil {
t.Fatalf("Failed to add role1 to alice: %v", err)
}
// Add a different user to role2 should succeed
_, err = e.AddRoleForUser("bob", "role2")
if err != nil {
t.Fatalf("Failed to add role2 to bob: %v", err)
}
// Try to add role2 to alice should fail (SOD violation)
_, err = e.AddRoleForUser("alice", "role2")
if err == nil {
t.Fatal("Expected constraint violation error, got nil")
}
if !strings.Contains(err.Error(), "constraint violation") {
t.Fatalf("Expected constraint violation error, got: %v", err)
}
}
func TestConstraintSODMax(t *testing.T) {
modelText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[constraint_definition]
c = sodMax(["role1", "role2", "role3"], 1)
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`
m, err := model.NewModelFromString(modelText)
if err != nil {
t.Fatalf("Failed to create model: %v", err)
}
e, err := NewEnforcer(m)
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}
// Add user to one role should succeed
_, err = e.AddRoleForUser("alice", "role1")
if err != nil {
t.Fatalf("Failed to add role1 to alice: %v", err)
}
// Try to add user to another role from the set should fail
_, err = e.AddRoleForUser("alice", "role2")
if err == nil {
t.Fatal("Expected constraint violation error, got nil")
}
if !strings.Contains(err.Error(), "constraint violation") {
t.Fatalf("Expected constraint violation error, got: %v", err)
}
}
func TestConstraintRoleMax(t *testing.T) {
modelText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[constraint_definition]
c = roleMax("admin", 2)
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`
m, err := model.NewModelFromString(modelText)
if err != nil {
t.Fatalf("Failed to create model: %v", err)
}
e, err := NewEnforcer(m)
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}
// Add first user to admin role should succeed
_, err = e.AddRoleForUser("alice", "admin")
if err != nil {
t.Fatalf("Failed to add admin to alice: %v", err)
}
// Add second user to admin role should succeed
_, err = e.AddRoleForUser("bob", "admin")
if err != nil {
t.Fatalf("Failed to add admin to bob: %v", err)
}
// Try to add third user to admin role should fail (exceeds max)
_, err = e.AddRoleForUser("charlie", "admin")
if err == nil {
t.Fatal("Expected constraint violation error, got nil")
}
if !strings.Contains(err.Error(), "constraint violation") {
t.Fatalf("Expected constraint violation error, got: %v", err)
}
}
func TestConstraintRolePre(t *testing.T) {
modelText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[constraint_definition]
c = rolePre("db_admin", "security_trained")
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`
m, err := model.NewModelFromString(modelText)
if err != nil {
t.Fatalf("Failed to create model: %v", err)
}
e, err := NewEnforcer(m)
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}
// Try to add db_admin without prerequisite should fail
_, err = e.AddRoleForUser("alice", "db_admin")
if err == nil {
t.Fatal("Expected constraint violation error, got nil")
}
if !strings.Contains(err.Error(), "constraint violation") {
t.Fatalf("Expected constraint violation error, got: %v", err)
}
// Add prerequisite role first
_, err = e.AddRoleForUser("alice", "security_trained")
if err != nil {
t.Fatalf("Failed to add security_trained to alice: %v", err)
}
// Now adding db_admin should succeed
_, err = e.AddRoleForUser("alice", "db_admin")
if err != nil {
t.Fatalf("Failed to add db_admin to alice: %v", err)
}
}
func TestConstraintWithoutRBAC(t *testing.T) {
modelText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[constraint_definition]
c = sod("role1", "role2")
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
`
_, err := model.NewModelFromString(modelText)
if err == nil {
t.Fatal("Expected error for constraints without RBAC, got nil")
}
if err != errors.ErrConstraintRequiresRBAC {
t.Fatalf("Expected ErrConstraintRequiresRBAC, got: %v", err)
}
}
func TestConstraintParsingError(t *testing.T) {
modelText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[constraint_definition]
c = invalidFunction("role1", "role2")
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`
_, err := model.NewModelFromString(modelText)
if err == nil {
t.Fatal("Expected parsing error for invalid constraint, got nil")
}
if !strings.Contains(err.Error(), "constraint parsing error") {
t.Fatalf("Expected constraint parsing error, got: %v", err)
}
}
func TestConstraintRollback(t *testing.T) {
modelText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[constraint_definition]
c = sod("role1", "role2")
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`
m, err := model.NewModelFromString(modelText)
if err != nil {
t.Fatalf("Failed to create model: %v", err)
}
e, err := NewEnforcer(m)
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}
// Add alice to role1
_, err = e.AddRoleForUser("alice", "role1")
if err != nil {
t.Fatalf("Failed to add role1 to alice: %v", err)
}
// Try to add alice to role2 (should fail with constraint violation)
_, err = e.AddRoleForUser("alice", "role2")
if err == nil {
t.Fatal("Expected constraint violation error, got nil")
}
if !strings.Contains(err.Error(), "constraint violation") {
t.Fatalf("Expected constraint violation error, got: %v", err)
}
}

View File

@@ -0,0 +1,153 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package detector
import (
"fmt"
"strings"
"github.com/casbin/casbin/v3/rbac"
)
// rangeableRM is an interface for role managers that support iterating over all role links.
// This is used to build the adjacency graph for cycle detection.
type rangeableRM interface {
Range(func(name1, name2 string, domain ...string) bool)
}
// DefaultDetector is the default implementation of the Detector interface.
// It uses depth-first search (DFS) to detect cycles in role inheritance.
type DefaultDetector struct{}
// NewDefaultDetector creates a new instance of DefaultDetector.
func NewDefaultDetector() *DefaultDetector {
return &DefaultDetector{}
}
// Check checks whether the current status of the passed-in RoleManager contains logical errors (e.g., cycles in role inheritance).
// It uses DFS to traverse the role graph and detect cycles.
// Returns nil if no cycle is found, otherwise returns an error with a description of the cycle.
func (d *DefaultDetector) Check(rm rbac.RoleManager) error {
// Defensive nil check to prevent runtime panics
if rm == nil {
return fmt.Errorf("role manager cannot be nil")
}
// Build the adjacency graph by exploring all roles
graph, err := d.buildGraph(rm)
if err != nil {
return err
}
// Run DFS to detect cycles
visited := make(map[string]bool)
recursionStack := make(map[string]bool)
for role := range graph {
if !visited[role] {
if cycle := d.detectCycle(role, graph, visited, recursionStack, []string{}); cycle != nil {
return fmt.Errorf("cycle detected: %s", strings.Join(cycle, " -> "))
}
}
}
return nil
}
// buildGraph builds an adjacency list representation of the role inheritance graph.
// It uses the Range method (via type assertion) to iterate through all role links.
func (d *DefaultDetector) buildGraph(rm rbac.RoleManager) (graph map[string][]string, err error) {
graph = make(map[string][]string)
// Recover from any panics during Range iteration (e.g., nil pointer dereferences)
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("RoleManager is not properly initialized: %v", r)
}
}()
// Try to cast to a RoleManager implementation that supports Range
// This works with RoleManagerImpl and similar implementations
rrm, ok := rm.(rangeableRM)
if !ok {
// Return an error if the RoleManager doesn't support Range iteration
return nil, fmt.Errorf("RoleManager does not support Range iteration, cannot detect cycles")
}
// Use Range method to build the graph directly
rrm.Range(func(name1, name2 string, domain ...string) bool {
// Initialize empty slice for name1 if it doesn't exist
if graph[name1] == nil {
graph[name1] = []string{}
}
// Add the link: name1 -> name2
graph[name1] = append(graph[name1], name2)
// Ensure name2 exists in graph even if it has no outgoing edges
if graph[name2] == nil {
graph[name2] = []string{}
}
return true
})
return graph, nil
}
// detectCycle performs DFS to detect cycles in the role graph.
// Returns a slice representing the cycle path if found, nil otherwise.
func (d *DefaultDetector) detectCycle(
role string,
graph map[string][]string,
visited map[string]bool,
recursionStack map[string]bool,
path []string,
) []string {
// Mark the current role as visited and add to recursion stack
visited[role] = true
recursionStack[role] = true
path = append(path, role)
// Visit all neighbors (parent roles)
for _, neighbor := range graph[role] {
if !visited[neighbor] {
// Recursively visit unvisited neighbor
if cycle := d.detectCycle(neighbor, graph, visited, recursionStack, path); cycle != nil {
return cycle
}
} else if recursionStack[neighbor] {
// Back edge found - cycle detected
// Find where the cycle starts in the path
cycleStart := -1
for i, p := range path {
if p == neighbor {
cycleStart = i
break
}
}
if cycleStart >= 0 {
// Build the cycle path
cyclePath := append([]string{}, path[cycleStart:]...)
cyclePath = append(cyclePath, neighbor)
return cyclePath
}
}
}
// Remove from recursion stack before returning
recursionStack[role] = false
return nil
}

View File

@@ -0,0 +1,341 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package detector
import (
"fmt"
"strings"
"testing"
defaultrolemanager "github.com/casbin/casbin/v3/rbac/default-role-manager"
)
func TestDefaultDetector_NilRoleManager(t *testing.T) {
detector := NewDefaultDetector()
err := detector.Check(nil)
if err == nil {
t.Error("Expected error for nil role manager, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "role manager cannot be nil") {
t.Errorf("Expected error message to contain 'role manager cannot be nil', got: %s", errMsg)
}
}
}
func TestDefaultDetector_NoCycle(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
_ = rm.AddLink("alice", "admin")
_ = rm.AddLink("bob", "user")
_ = rm.AddLink("admin", "superuser")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err != nil {
t.Errorf("Expected no cycle, but got error: %v", err)
}
}
func TestDefaultDetector_SimpleCycle(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
_ = rm.AddLink("A", "B")
_ = rm.AddLink("B", "A")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err == nil {
t.Error("Expected cycle detection error, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "cycle detected") {
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
}
// Should contain both A and B in the cycle
if !strings.Contains(errMsg, "A") || !strings.Contains(errMsg, "B") {
t.Errorf("Expected error message to contain both A and B, got: %s", errMsg)
}
}
}
func TestDefaultDetector_ComplexCycle(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
_ = rm.AddLink("A", "B")
_ = rm.AddLink("B", "C")
_ = rm.AddLink("C", "A")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err == nil {
t.Error("Expected cycle detection error, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "cycle detected") {
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
}
// Should contain A, B, and C in the cycle
if !strings.Contains(errMsg, "A") || !strings.Contains(errMsg, "B") || !strings.Contains(errMsg, "C") {
t.Errorf("Expected error message to contain A, B, and C, got: %s", errMsg)
}
}
}
func TestDefaultDetector_SelfLoop(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
_ = rm.AddLink("A", "A")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err == nil {
t.Error("Expected cycle detection error for self-loop, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "cycle detected") {
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
}
}
}
func TestDefaultDetector_MultipleCycles(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
// First cycle: A -> B -> A
_ = rm.AddLink("A", "B")
_ = rm.AddLink("B", "A")
// Second cycle: C -> D -> C
_ = rm.AddLink("C", "D")
_ = rm.AddLink("D", "C")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err == nil {
t.Error("Expected cycle detection error, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "cycle detected") {
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
}
}
}
func TestDefaultDetector_DisconnectedComponents(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
// Component 1: alice -> admin -> superuser
_ = rm.AddLink("alice", "admin")
_ = rm.AddLink("admin", "superuser")
// Component 2: bob -> user
_ = rm.AddLink("bob", "user")
// Component 3: carol -> moderator
_ = rm.AddLink("carol", "moderator")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err != nil {
t.Errorf("Expected no cycle in disconnected components, but got error: %v", err)
}
}
func TestDefaultDetector_ComplexGraphWithCycle(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
// Build a complex graph with one cycle
_ = rm.AddLink("u1", "g1")
_ = rm.AddLink("u2", "g1")
_ = rm.AddLink("g1", "g2")
_ = rm.AddLink("g2", "g3")
_ = rm.AddLink("g3", "g1") // Creates cycle: g1 -> g2 -> g3 -> g1
_ = rm.AddLink("u3", "g4")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err == nil {
t.Error("Expected cycle detection error, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "cycle detected") {
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
}
}
}
func TestDefaultDetector_LongCycle(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(20)
// Create a long cycle: A -> B -> C -> D -> E -> A
_ = rm.AddLink("A", "B")
_ = rm.AddLink("B", "C")
_ = rm.AddLink("C", "D")
_ = rm.AddLink("D", "E")
_ = rm.AddLink("E", "A")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err == nil {
t.Error("Expected cycle detection error, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "cycle detected") {
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
}
}
}
func TestDefaultDetector_EmptyRoleManager(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
detector := NewDefaultDetector()
err := detector.Check(rm)
if err != nil {
t.Errorf("Expected no error for empty role manager, but got: %v", err)
}
}
func TestDefaultDetector_LargeGraphNoCycle(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(100)
// Build a large graph with no cycles: a tree structure
// Create 100 levels: u0 -> u1 -> u2 -> ... -> u99
for i := 0; i < 99; i++ {
user := fmt.Sprintf("u%d", i)
role := fmt.Sprintf("u%d", i+1)
_ = rm.AddLink(user, role)
}
detector := NewDefaultDetector()
err := detector.Check(rm)
if err != nil {
t.Errorf("Expected no cycle in large graph, but got error: %v", err)
}
}
func TestDefaultDetector_LargeGraphWithCycle(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(100)
// Build a large graph with a cycle at the end
// Create a chain: u0 -> u1 -> u2 -> ... -> u99 -> u0
for i := 0; i < 99; i++ {
user := fmt.Sprintf("u%d", i)
role := fmt.Sprintf("u%d", i+1)
_ = rm.AddLink(user, role)
}
// Add the cycle
_ = rm.AddLink("u99", "u0")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err == nil {
t.Error("Expected cycle detection error in large graph, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "cycle detected") {
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
}
}
}
// Performance test with 10,000 roles.
func TestDefaultDetector_PerformanceLargeGraph(t *testing.T) {
if testing.Short() {
t.Skip("Skipping performance test in short mode")
}
// Use a higher maxHierarchyLevel to support deep hierarchies
rm := defaultrolemanager.NewRoleManagerImpl(10000)
// Build a large tree structure with 10,000 roles
// Each role has up to 3 children
numRoles := 10000
for i := 0; i < numRoles; i++ {
role := fmt.Sprintf("r%d", i)
// Add links to create a tree structure
child1 := (i * 3) + 1
child2 := (i * 3) + 2
child3 := (i * 3) + 3
if child1 < numRoles {
_ = rm.AddLink(fmt.Sprintf("r%d", child1), role)
}
if child2 < numRoles {
_ = rm.AddLink(fmt.Sprintf("r%d", child2), role)
}
if child3 < numRoles {
_ = rm.AddLink(fmt.Sprintf("r%d", child3), role)
}
}
detector := NewDefaultDetector()
err := detector.Check(rm)
if err != nil {
t.Errorf("Expected no cycle in large performance test, but got error: %v", err)
}
}
func TestDefaultDetector_MultipleInheritance(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
// User inherits from multiple roles
_ = rm.AddLink("alice", "admin")
_ = rm.AddLink("alice", "moderator")
_ = rm.AddLink("admin", "superuser")
_ = rm.AddLink("moderator", "user")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err != nil {
t.Errorf("Expected no cycle with multiple inheritance, but got error: %v", err)
}
}
func TestDefaultDetector_DiamondPattern(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
// Diamond pattern: alice -> admin, alice -> moderator, admin -> superuser, moderator -> superuser
_ = rm.AddLink("alice", "admin")
_ = rm.AddLink("alice", "moderator")
_ = rm.AddLink("admin", "superuser")
_ = rm.AddLink("moderator", "superuser")
detector := NewDefaultDetector()
err := detector.Check(rm)
if err != nil {
t.Errorf("Expected no cycle in diamond pattern, but got error: %v", err)
}
}
func TestDefaultDetector_DiamondPatternWithCycle(t *testing.T) {
rm := defaultrolemanager.NewRoleManagerImpl(10)
// Diamond pattern with cycle: alice -> admin, alice -> moderator, admin -> superuser, moderator -> superuser, superuser -> alice
_ = rm.AddLink("alice", "admin")
_ = rm.AddLink("alice", "moderator")
_ = rm.AddLink("admin", "superuser")
_ = rm.AddLink("moderator", "superuser")
_ = rm.AddLink("superuser", "alice") // Creates cycle
detector := NewDefaultDetector()
err := detector.Check(rm)
if err == nil {
t.Error("Expected cycle detection error in diamond pattern with cycle, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "cycle detected") {
t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg)
}
}
}

28
detector/detector.go Normal file
View File

@@ -0,0 +1,28 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package detector
import "github.com/casbin/casbin/v3/rbac"
// Detector defines the interface of a policy consistency checker, currently used to detect RBAC inheritance cycles.
type Detector interface {
// Check checks whether the current status of the passed-in RoleManager contains logical errors (e.g., cycles in role inheritance).
// param: rm RoleManager instance
// return: If an error is found, return a descriptive error; otherwise return nil.
Check(rm rbac.RoleManager) error
}

View File

@@ -0,0 +1,111 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package effector
import (
"errors"
"github.com/casbin/casbin/v3/constant"
)
// DefaultEffector is default effector for Casbin.
type DefaultEffector struct{}
// NewDefaultEffector is the constructor for DefaultEffector.
func NewDefaultEffector() *DefaultEffector {
e := DefaultEffector{}
return &e
}
// MergeEffects merges all matching results collected by the enforcer into a single decision.
func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches []float64, policyIndex int, policyLength int) (Effect, int, error) {
result := Indeterminate
explainIndex := -1
switch expr {
case constant.AllowOverrideEffect:
if matches[policyIndex] == 0 {
break
}
// only check the current policyIndex
if effects[policyIndex] == Allow {
result = Allow
explainIndex = policyIndex
break
}
case constant.DenyOverrideEffect:
// only check the current policyIndex
if matches[policyIndex] != 0 && effects[policyIndex] == Deny {
result = Deny
explainIndex = policyIndex
break
}
// if no deny rules are matched at last, then allow
if policyIndex == policyLength-1 {
result = Allow
}
case constant.AllowAndDenyEffect:
// short-circuit if matched deny rule
if matches[policyIndex] != 0 && effects[policyIndex] == Deny {
result = Deny
// set hit rule to the (first) matched deny rule
explainIndex = policyIndex
break
}
// short-circuit some effects in the middle
if policyIndex < policyLength-1 {
// choose not to short-circuit
return result, explainIndex, nil
}
// merge all effects at last
for i, eft := range effects {
if matches[i] == 0 {
continue
}
if eft == Allow {
result = Allow
// set hit rule to first matched allow rule
explainIndex = i
break
}
}
case constant.PriorityEffect, constant.SubjectPriorityEffect:
// reverse merge, short-circuit may be earlier
for i := len(effects) - 1; i >= 0; i-- {
if matches[i] == 0 {
continue
}
if effects[i] != Indeterminate {
if effects[i] == Allow {
result = Allow
} else {
result = Deny
}
explainIndex = i
break
}
}
default:
return Deny, -1, errors.New("unsupported effect")
}
return result, explainIndex, nil
}

34
effector/effector.go Normal file
View File

@@ -0,0 +1,34 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package effector //nolint:cyclop // TODO
// Effect is the result for a policy rule.
type Effect int
// Values for policy effect.
const (
Allow Effect = iota
Indeterminate
Deny
)
// Effector is the interface for Casbin effectors.
type Effector interface {
// MergeEffects merges all matching results collected by the enforcer into a single decision.
MergeEffects(expr string, effects []Effect, matches []float64, policyIndex int, policyLength int) (Effect, int, error)
}

1102
enforcer.go Normal file

File diff suppressed because it is too large Load Diff

180
enforcer_backslash_test.go Normal file
View File

@@ -0,0 +1,180 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"testing"
"github.com/casbin/casbin/v3/model"
fileadapter "github.com/casbin/casbin/v3/persist/file-adapter"
)
// TestBackslashHandlingConsistency tests that backslashes in string literals
// within matcher expressions are handled consistently with CSV-parsed values.
// This addresses the issue where govaluate interprets escape sequences in
// string literals, but CSV parsing treats backslashes as literal characters.
func TestBackslashHandlingConsistency(t *testing.T) {
// Test case 1: Literal string in matcher should match CSV-parsed request
t.Run("LiteralInMatcher", func(t *testing.T) {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("e", "e", "some(where (p.eft == allow))")
// User writes '\1\2' in matcher - should be treated as literal backslashes
m.AddDef("m", "m", "regexMatch('\\1\\2', p.obj)")
e, err := NewEnforcer(m, fileadapter.NewAdapter("examples/basic_policy.csv"))
if err != nil {
t.Fatal(err)
}
// Add a policy with a regex pattern containing backslashes
// CSV format: "\\[0-9]+\\" means literal string with 4 backslashes
_, err = e.AddPolicy("filename", "\\\\[0-9]+\\\\", "read")
if err != nil {
t.Fatal(err)
}
// This should match because '\1\2' after escaping becomes \1\2,
// and the pattern \\[0-9]+\\ matches strings like \1\ (which is a substring of \1\2)
result, err := e.Enforce("filename", "dummy", "read")
if err != nil {
t.Fatal(err)
}
if !result {
t.Errorf("Expected true, got false - literal '\\1\\2' should match after escape processing")
}
})
// Test case 2: Request parameter should match policy with same backslash content
t.Run("RequestParameterVsPolicy", func(t *testing.T) {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", "regexMatch(r.obj, p.obj)")
e, err := NewEnforcer(m, fileadapter.NewAdapter("examples/basic_policy.csv"))
if err != nil {
t.Fatal(err)
}
// Add policy with regex pattern
_, err = e.AddPolicy("filename", "\\\\[0-9]+\\\\", "read")
if err != nil {
t.Fatal(err)
}
// Request with backslashes - simulating CSV input "\1\2" which becomes \1\2
result, err := e.Enforce("filename", "\\1\\2", "read")
if err != nil {
t.Fatal(err)
}
if !result {
t.Errorf("Expected true, got false - request \\1\\2 should match regex pattern")
}
})
// Test case 3: Both approaches should give the same result
t.Run("ConsistencyBetweenLiteralAndParameter", func(t *testing.T) {
// Create two enforcers with different matchers
m1 := model.NewModel()
m1.AddDef("r", "r", "sub, obj, act")
m1.AddDef("p", "p", "sub, obj, act")
m1.AddDef("e", "e", "some(where (p.eft == allow))")
m1.AddDef("m", "m", "regexMatch('\\1\\2', p.obj)")
m2 := model.NewModel()
m2.AddDef("r", "r", "sub, obj, act")
m2.AddDef("p", "p", "sub, obj, act")
m2.AddDef("e", "e", "some(where (p.eft == allow))")
m2.AddDef("m", "m", "regexMatch(r.obj, p.obj)")
e1, err := NewEnforcer(m1, fileadapter.NewAdapter("examples/basic_policy.csv"))
if err != nil {
t.Fatal(err)
}
e2, err := NewEnforcer(m2, fileadapter.NewAdapter("examples/basic_policy.csv"))
if err != nil {
t.Fatal(err)
}
// Add same policy to both
pattern := "\\\\[0-9]+\\\\"
_, err = e1.AddPolicy("filename", pattern, "read")
if err != nil {
t.Fatal(err)
}
_, err = e2.AddPolicy("filename", pattern, "read")
if err != nil {
t.Fatal(err)
}
// Test with the same request
result1, err := e1.Enforce("filename", "dummy", "read")
if err != nil {
t.Fatal(err)
}
result2, err := e2.Enforce("filename", "\\1\\2", "read")
if err != nil {
t.Fatal(err)
}
if result1 != result2 {
t.Errorf("Inconsistent results: literal in matcher gave %v, parameter gave %v", result1, result2)
}
})
// Test case 4: Simple equality check with backslashes
t.Run("SimpleEqualityWithBackslashes", func(t *testing.T) {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("e", "e", "some(where (p.eft == allow))")
// In Go source, '\test' (one backslash in the actual string) represents
// what would be typed in a web form. After escape processing, it will match
// the CSV-parsed value "\test" (one backslash).
// Note: In Go source, we write "r.obj == '\\test'" which is a Go string
// containing the text: r.obj == '\test' (with ONE backslash in the string content)
m.AddDef("m", "m", "r.obj == '\\test' && p.sub == r.sub")
e, err := NewEnforcer(m, fileadapter.NewAdapter("examples/basic_policy.csv"))
if err != nil {
t.Fatal(err)
}
_, err = e.AddPolicy("alice", "any", "read")
if err != nil {
t.Fatal(err)
}
// Request with literal backslash from CSV would be "\test"
// In Go source, we write "\\test" which represents the string \test (one backslash)
result, err := e.Enforce("alice", "\\test", "read")
if err != nil {
t.Fatal(err)
}
if !result {
t.Errorf("Expected true - literal '\\test' should equal request parameter \\test")
}
})
}

188
enforcer_cached.go Normal file
View File

@@ -0,0 +1,188 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"strings"
"sync"
"sync/atomic"
"time"
"github.com/casbin/casbin/v3/persist/cache"
)
// CachedEnforcer wraps Enforcer and provides decision cache.
type CachedEnforcer struct {
*Enforcer
expireTime time.Duration
cache cache.Cache
enableCache int32
locker *sync.RWMutex
}
type CacheableParam interface {
GetCacheKey() string
}
// NewCachedEnforcer creates a cached enforcer via file or DB.
func NewCachedEnforcer(params ...interface{}) (*CachedEnforcer, error) {
e := &CachedEnforcer{}
var err error
e.Enforcer, err = NewEnforcer(params...)
if err != nil {
return nil, err
}
e.enableCache = 1
e.cache, _ = cache.NewDefaultCache()
e.locker = new(sync.RWMutex)
return e, nil
}
// EnableCache determines whether to enable cache on Enforce(). When enableCache is enabled, cached result (true | false) will be returned for previous decisions.
func (e *CachedEnforcer) EnableCache(enableCache bool) {
var enabled int32
if enableCache {
enabled = 1
}
atomic.StoreInt32(&e.enableCache, enabled)
}
// Enforce decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (sub, obj, act).
// if rvals is not string , ignore the cache.
func (e *CachedEnforcer) Enforce(rvals ...interface{}) (bool, error) {
if atomic.LoadInt32(&e.enableCache) == 0 {
return e.Enforcer.Enforce(rvals...)
}
key, ok := e.getKey(rvals...)
if !ok {
return e.Enforcer.Enforce(rvals...)
}
if res, err := e.getCachedResult(key); err == nil {
return res, nil
} else if err != cache.ErrNoSuchKey {
return res, err
}
res, err := e.Enforcer.Enforce(rvals...)
if err != nil {
return false, err
}
err = e.setCachedResult(key, res, e.expireTime)
return res, err
}
func (e *CachedEnforcer) LoadPolicy() error {
if atomic.LoadInt32(&e.enableCache) != 0 {
if err := e.cache.Clear(); err != nil {
return err
}
}
return e.Enforcer.LoadPolicy()
}
func (e *CachedEnforcer) RemovePolicy(params ...interface{}) (bool, error) {
if atomic.LoadInt32(&e.enableCache) != 0 {
key, ok := e.getKey(params...)
if ok {
if err := e.cache.Delete(key); err != nil && err != cache.ErrNoSuchKey {
return false, err
}
}
}
return e.Enforcer.RemovePolicy(params...)
}
func (e *CachedEnforcer) RemovePolicies(rules [][]string) (bool, error) {
if len(rules) != 0 {
if atomic.LoadInt32(&e.enableCache) != 0 {
for _, rule := range rules {
irule := make([]interface{}, len(rule))
for i, param := range rule {
irule[i] = param
}
key, _ := e.getKey(irule...)
if err := e.cache.Delete(key); err != nil && err != cache.ErrNoSuchKey {
return false, err
}
}
}
}
return e.Enforcer.RemovePolicies(rules)
}
func (e *CachedEnforcer) getCachedResult(key string) (res bool, err error) {
e.locker.Lock()
defer e.locker.Unlock()
return e.cache.Get(key)
}
func (e *CachedEnforcer) SetExpireTime(expireTime time.Duration) {
e.expireTime = expireTime
}
func (e *CachedEnforcer) SetCache(c cache.Cache) {
e.cache = c
}
func (e *CachedEnforcer) setCachedResult(key string, res bool, extra ...interface{}) error {
e.locker.Lock()
defer e.locker.Unlock()
return e.cache.Set(key, res, extra...)
}
func (e *CachedEnforcer) getKey(params ...interface{}) (string, bool) {
return GetCacheKey(params...)
}
// InvalidateCache deletes all the existing cached decisions.
func (e *CachedEnforcer) InvalidateCache() error {
e.locker.Lock()
defer e.locker.Unlock()
return e.cache.Clear()
}
func GetCacheKey(params ...interface{}) (string, bool) {
key := strings.Builder{}
for _, param := range params {
switch typedParam := param.(type) {
case string:
key.WriteString(typedParam)
case CacheableParam:
key.WriteString(typedParam.GetCacheKey())
default:
return "", false
}
key.WriteString("$$")
}
return key.String(), true
}
// ClearPolicy clears all policy.
func (e *CachedEnforcer) ClearPolicy() {
if atomic.LoadInt32(&e.enableCache) != 0 {
if err := e.cache.Clear(); err != nil {
// Logger has been removed - error is ignored
return
}
}
e.Enforcer.ClearPolicy()
}

224
enforcer_cached_b_test.go Normal file
View File

@@ -0,0 +1,224 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"fmt"
"testing"
)
func BenchmarkCachedRaw(b *testing.B) {
for i := 0; i < b.N; i++ {
rawEnforce("alice", "data1", "read")
}
}
func BenchmarkCachedBasicModel(b *testing.B) {
e, _ := NewCachedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("alice", "data1", "read")
}
}
func BenchmarkCachedRBACModel(b *testing.B) {
e, _ := NewCachedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("alice", "data2", "read")
}
}
func BenchmarkCachedRBACModelSmall(b *testing.B) {
e, _ := NewCachedEnforcer("examples/rbac_model.conf")
// 100 roles, 10 resources.
for i := 0; i < 100; i++ {
_, err := e.AddPolicy(fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read")
if err != nil {
b.Fatal(err)
}
}
// 1000 users.
for i := 0; i < 1000; i++ {
_, err := e.AddGroupingPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10))
if err != nil {
b.Fatal(err)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("user501", "data9", "read")
}
}
func BenchmarkCachedRBACModelMedium(b *testing.B) {
e, _ := NewCachedEnforcer("examples/rbac_model.conf")
// 1000 roles, 100 resources.
pPolicies := make([][]string, 0)
for i := 0; i < 1000; i++ {
pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"})
}
_, err := e.AddPolicies(pPolicies)
if err != nil {
b.Fatal(err)
}
// 10000 users.
gPolicies := make([][]string, 0)
for i := 0; i < 10000; i++ {
gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)})
}
_, err = e.AddGroupingPolicies(gPolicies)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("user5001", "data150", "read")
}
}
func BenchmarkCachedRBACModelLarge(b *testing.B) {
e, _ := NewCachedEnforcer("examples/rbac_model.conf")
// 10000 roles, 1000 resources.
pPolicies := make([][]string, 0)
for i := 0; i < 10000; i++ {
pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"})
}
_, err := e.AddPolicies(pPolicies)
if err != nil {
b.Fatal(err)
}
// 100000 users.
gPolicies := make([][]string, 0)
for i := 0; i < 100000; i++ {
gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)})
}
_, err = e.AddGroupingPolicies(gPolicies)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("user50001", "data1500", "read")
}
}
func BenchmarkCachedRBACModelWithResourceRoles(b *testing.B) {
e, _ := NewCachedEnforcer("examples/rbac_with_resource_roles_model.conf", "examples/rbac_with_resource_roles_policy.csv")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("alice", "data1", "read")
}
}
func BenchmarkCachedRBACModelWithDomains(b *testing.B) {
e, _ := NewCachedEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("alice", "domain1", "data1", "read")
}
}
func BenchmarkCachedABACModel(b *testing.B) {
e, _ := NewCachedEnforcer("examples/abac_model.conf")
data1 := newTestResource("data1", "alice")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("alice", data1, "read")
}
}
func BenchmarkCachedKeyMatchModel(b *testing.B) {
e, _ := NewCachedEnforcer("examples/keymatch_model.conf", "examples/keymatch_policy.csv")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("alice", "/alice_data/resource1", "GET")
}
}
func BenchmarkCachedRBACModelWithDeny(b *testing.B) {
e, _ := NewCachedEnforcer("examples/rbac_with_deny_model.conf", "examples/rbac_with_deny_policy.csv")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("alice", "data1", "read")
}
}
func BenchmarkCachedPriorityModel(b *testing.B) {
e, _ := NewCachedEnforcer("examples/priority_model.conf", "examples/priority_policy.csv")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce("alice", "data1", "read")
}
}
func BenchmarkCachedWithEnforceContext(b *testing.B) {
e, _ := NewCachedEnforcer("examples/priority_model_enforce_context.conf", "examples/priority_policy_enforce_context.csv")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Enforce(EnforceContext{RType: "r2", PType: "p", EType: "e", MType: "m2"}, "alice", "data1")
}
}
func BenchmarkCachedRBACModelMediumParallel(b *testing.B) {
e, _ := NewCachedEnforcer("examples/rbac_model.conf")
// 10000 roles, 1000 resources.
pPolicies := make([][]string, 0)
for i := 0; i < 10000; i++ {
pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"})
}
_, err := e.AddPolicies(pPolicies)
if err != nil {
b.Fatal(err)
}
// 100000 users.
gPolicies := make([][]string, 0)
for i := 0; i < 100000; i++ {
gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)})
}
_, err = e.AddGroupingPolicies(gPolicies)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = e.Enforce("user5001", "data150", "read")
}
})
}

View File

@@ -0,0 +1,204 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import "testing"
// TestCachedGFunctionAfterAddingGroupingPolicy tests that adding new grouping policies
// at runtime correctly updates permission evaluation without requiring a restart or manual cache clearing.
// This is a regression test for the issue where GenerateGFunction's memoization cache
// caused stale permission results after adding new grouping policies.
func TestCachedGFunctionAfterAddingGroupingPolicy(t *testing.T) {
e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}
// Initial state: bob should not have access to data2 (read)
// bob has no roles initially
ok, err := e.Enforce("bob", "data2", "read")
if err != nil {
t.Fatalf("Enforce failed: %v", err)
}
if ok {
t.Error("bob should not have read access to data2 initially")
}
// Add a new grouping policy: bob becomes data2_admin
// data2_admin has read and write access to data2
_, err = e.AddGroupingPolicy("bob", "data2_admin")
if err != nil {
t.Fatalf("Failed to add grouping policy: %v", err)
}
// Now bob should have read access to data2 through the data2_admin role
// This should work immediately without needing to clear cache or restart
ok, err = e.Enforce("bob", "data2", "read")
if err != nil {
t.Fatalf("Enforce failed after adding grouping policy: %v", err)
}
if !ok {
t.Error("bob should have read access to data2 after being added to data2_admin role")
}
// Also verify write access works
ok, err = e.Enforce("bob", "data2", "write")
if err != nil {
t.Fatalf("Enforce failed: %v", err)
}
if !ok {
t.Error("bob should have write access to data2 after being added to data2_admin role")
}
}
// TestCachedGFunctionWithMultipleEnforceCalls tests that the g() function cache
// properly invalidates when grouping policies change, even after multiple enforce calls.
func TestCachedGFunctionWithMultipleEnforceCalls(t *testing.T) {
e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}
// Make multiple enforce calls to ensure the g() function closure is cached
for i := 0; i < 5; i++ {
ok, enforceErr := e.Enforce("charlie", "data2", "read")
if enforceErr != nil {
t.Fatalf("Enforce failed on iteration %d: %v", i, enforceErr)
}
if ok {
t.Errorf("charlie should not have read access to data2 on iteration %d", i)
}
}
// Add grouping policy
_, err = e.AddGroupingPolicy("charlie", "data2_admin")
if err != nil {
t.Fatalf("Failed to add grouping policy: %v", err)
}
// Immediately verify the change took effect
ok, err := e.Enforce("charlie", "data2", "read")
if err != nil {
t.Fatalf("Enforce failed after adding grouping policy: %v", err)
}
if !ok {
t.Error("charlie should have read access to data2 immediately after being added to data2_admin role")
}
// Make multiple calls to ensure it stays consistent
for i := 0; i < 5; i++ {
ok, enforceErr := e.Enforce("charlie", "data2", "read")
if enforceErr != nil {
t.Fatalf("Enforce failed on iteration %d after policy change: %v", i, enforceErr)
}
if !ok {
t.Errorf("charlie should have read access to data2 on iteration %d after policy change", i)
}
}
}
// TestCachedGFunctionAfterRemovingGroupingPolicy tests that removing grouping policies
// also properly invalidates the g() function cache.
func TestCachedGFunctionAfterRemovingGroupingPolicy(t *testing.T) {
e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}
// alice initially has the data2_admin role
ok, err := e.Enforce("alice", "data2", "read")
if err != nil {
t.Fatalf("Enforce failed: %v", err)
}
if !ok {
t.Error("alice should have read access to data2 initially")
}
// Remove alice from data2_admin role
_, err = e.RemoveGroupingPolicy("alice", "data2_admin")
if err != nil {
t.Fatalf("Failed to remove grouping policy: %v", err)
}
// Now alice should not have access to data2 (she only has access to data1)
ok, err = e.Enforce("alice", "data2", "read")
if err != nil {
t.Fatalf("Enforce failed after removing grouping policy: %v", err)
}
if ok {
t.Error("alice should not have read access to data2 after being removed from data2_admin role")
}
// Verify alice still has access to data1 (direct policy)
ok, err = e.Enforce("alice", "data1", "read")
if err != nil {
t.Fatalf("Enforce failed: %v", err)
}
if !ok {
t.Error("alice should still have read access to data1 (direct policy)")
}
}
// TestCachedGFunctionAfterBuildRoleLinks tests the specific scenario mentioned in the bug report:
// adding grouping policies and calling BuildRoleLinks() manually should properly invalidate the cache.
func TestCachedGFunctionAfterBuildRoleLinks(t *testing.T) {
e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}
// First, make some enforce calls to ensure the g() function closure is created and cached
// This will cache "bob" NOT having data2_admin role in the g() function's sync.Map
for i := 0; i < 3; i++ {
ok, enforceErr := e.Enforce("bob", "data2", "read")
if enforceErr != nil {
t.Fatalf("Enforce failed on iteration %d: %v", i, enforceErr)
}
if ok {
t.Errorf("bob should not have read access to data2 on iteration %d (before adding role)", i)
}
}
// Disable autoBuildRoleLinks to manually control when role links are rebuilt
e.EnableAutoBuildRoleLinks(false)
// Manually add the grouping policy to the model (bypassing BuildIncrementalRoleLinks)
// This simulates the scenario where policies are loaded from database
err = e.model.AddPolicy("g", "g", []string{"bob", "data2_admin"})
if err != nil {
t.Fatalf("Failed to add grouping policy to model: %v", err)
}
// Manually build role links as mentioned in the issue
// This is the key part - BuildRoleLinks() should invalidate the matcher map cache
err = e.BuildRoleLinks()
if err != nil {
t.Fatalf("Failed to build role links: %v", err)
}
// Now bob should have read access to data2 through the data2_admin role
// This is where the bug would manifest - if BuildRoleLinks() doesn't invalidate the cache,
// the old g() function closure with "bob->data2_admin = false" cached will still be used
ok, err := e.Enforce("bob", "data2", "read")
if err != nil {
t.Fatalf("Enforce failed after BuildRoleLinks: %v", err)
}
if !ok {
t.Error("bob should have read access to data2 after BuildRoleLinks() - this indicates the g() function cache was not properly invalidated")
}
}

183
enforcer_cached_synced.go Normal file
View File

@@ -0,0 +1,183 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"sync"
"sync/atomic"
"time"
"github.com/casbin/casbin/v3/persist/cache"
)
// SyncedCachedEnforcer wraps Enforcer and provides decision sync cache.
type SyncedCachedEnforcer struct {
*SyncedEnforcer
expireTime time.Duration
cache cache.Cache
enableCache int32
locker *sync.RWMutex
}
// NewSyncedCachedEnforcer creates a sync cached enforcer via file or DB.
func NewSyncedCachedEnforcer(params ...interface{}) (*SyncedCachedEnforcer, error) {
e := &SyncedCachedEnforcer{}
var err error
e.SyncedEnforcer, err = NewSyncedEnforcer(params...)
if err != nil {
return nil, err
}
e.enableCache = 1
e.cache, _ = cache.NewSyncCache()
e.locker = new(sync.RWMutex)
return e, nil
}
// EnableCache determines whether to enable cache on Enforce(). When enableCache is enabled, cached result (true | false) will be returned for previous decisions.
func (e *SyncedCachedEnforcer) EnableCache(enableCache bool) {
var enabled int32
if enableCache {
enabled = 1
}
atomic.StoreInt32(&e.enableCache, enabled)
}
// Enforce decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (sub, obj, act).
// if rvals is not string , ignore the cache.
func (e *SyncedCachedEnforcer) Enforce(rvals ...interface{}) (bool, error) {
if atomic.LoadInt32(&e.enableCache) == 0 {
return e.SyncedEnforcer.Enforce(rvals...)
}
key, ok := e.getKey(rvals...)
if !ok {
return e.SyncedEnforcer.Enforce(rvals...)
}
if res, err := e.getCachedResult(key); err == nil {
return res, nil
} else if err != cache.ErrNoSuchKey {
return res, err
}
res, err := e.SyncedEnforcer.Enforce(rvals...)
if err != nil {
return false, err
}
err = e.setCachedResult(key, res, e.expireTime)
return res, err
}
func (e *SyncedCachedEnforcer) LoadPolicy() error {
if atomic.LoadInt32(&e.enableCache) != 0 {
if err := e.cache.Clear(); err != nil {
return err
}
}
return e.SyncedEnforcer.LoadPolicy()
}
func (e *SyncedCachedEnforcer) AddPolicy(params ...interface{}) (bool, error) {
if ok, err := e.checkOneAndRemoveCache(params...); !ok {
return ok, err
}
return e.SyncedEnforcer.AddPolicy(params...)
}
func (e *SyncedCachedEnforcer) AddPolicies(rules [][]string) (bool, error) {
if ok, err := e.checkManyAndRemoveCache(rules); !ok {
return ok, err
}
return e.SyncedEnforcer.AddPolicies(rules)
}
func (e *SyncedCachedEnforcer) RemovePolicy(params ...interface{}) (bool, error) {
if ok, err := e.checkOneAndRemoveCache(params...); !ok {
return ok, err
}
return e.SyncedEnforcer.RemovePolicy(params...)
}
func (e *SyncedCachedEnforcer) RemovePolicies(rules [][]string) (bool, error) {
if ok, err := e.checkManyAndRemoveCache(rules); !ok {
return ok, err
}
return e.SyncedEnforcer.RemovePolicies(rules)
}
func (e *SyncedCachedEnforcer) getCachedResult(key string) (res bool, err error) {
return e.cache.Get(key)
}
func (e *SyncedCachedEnforcer) SetExpireTime(expireTime time.Duration) {
e.locker.Lock()
defer e.locker.Unlock()
e.expireTime = expireTime
}
// SetCache need to be sync cache.
func (e *SyncedCachedEnforcer) SetCache(c cache.Cache) {
e.locker.Lock()
defer e.locker.Unlock()
e.cache = c
}
func (e *SyncedCachedEnforcer) setCachedResult(key string, res bool, extra ...interface{}) error {
return e.cache.Set(key, res, extra...)
}
func (e *SyncedCachedEnforcer) getKey(params ...interface{}) (string, bool) {
return GetCacheKey(params...)
}
// InvalidateCache deletes all the existing cached decisions.
func (e *SyncedCachedEnforcer) InvalidateCache() error {
return e.cache.Clear()
}
func (e *SyncedCachedEnforcer) checkOneAndRemoveCache(params ...interface{}) (bool, error) {
if atomic.LoadInt32(&e.enableCache) != 0 {
key, ok := e.getKey(params...)
if ok {
if err := e.cache.Delete(key); err != nil && err != cache.ErrNoSuchKey {
return false, err
}
}
}
return true, nil
}
func (e *SyncedCachedEnforcer) checkManyAndRemoveCache(rules [][]string) (bool, error) {
if len(rules) != 0 {
if atomic.LoadInt32(&e.enableCache) != 0 {
for _, rule := range rules {
irule := make([]interface{}, len(rule))
for i, param := range rule {
irule[i] = param
}
key, _ := e.getKey(irule...)
if err := e.cache.Delete(key); err != nil && err != cache.ErrNoSuchKey {
return false, err
}
}
}
}
return true, nil
}

View File

@@ -0,0 +1,85 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"sync"
"testing"
"time"
)
func testSyncEnforceCache(t *testing.T, e *SyncedCachedEnforcer, sub string, obj interface{}, act string, res bool) {
t.Helper()
if myRes, _ := e.Enforce(sub, obj, act); myRes != res {
t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, myRes, res)
}
}
func TestSyncCache(t *testing.T) {
e, _ := NewSyncedCachedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
e.expireTime = time.Millisecond
// The cache is enabled by default for NewCachedEnforcer.
g := sync.WaitGroup{}
goThread := 1000
g.Add(goThread)
for i := 0; i < goThread; i++ {
go func() {
_, _ = e.AddPolicy("alice", "data2", "read")
testSyncEnforceCache(t, e, "alice", "data2", "read", true)
if e.InvalidateCache() != nil {
panic("never reached")
}
g.Done()
}()
}
g.Wait()
_, _ = e.RemovePolicy("alice", "data2", "read")
testSyncEnforceCache(t, e, "alice", "data1", "read", true)
time.Sleep(time.Millisecond * 2) // coverage for expire
testSyncEnforceCache(t, e, "alice", "data1", "read", true)
testSyncEnforceCache(t, e, "alice", "data1", "write", false)
testSyncEnforceCache(t, e, "alice", "data2", "read", false)
testSyncEnforceCache(t, e, "alice", "data2", "write", false)
// The cache is enabled, calling RemovePolicy, LoadPolicy or RemovePolicies will
// also operate cached items.
_, _ = e.RemovePolicy("alice", "data1", "read")
testSyncEnforceCache(t, e, "alice", "data1", "read", false)
testSyncEnforceCache(t, e, "alice", "data1", "write", false)
testSyncEnforceCache(t, e, "alice", "data2", "read", false)
testSyncEnforceCache(t, e, "alice", "data2", "write", false)
e, _ = NewSyncedCachedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
testSyncEnforceCache(t, e, "alice", "data1", "read", true)
testSyncEnforceCache(t, e, "bob", "data2", "write", true)
testSyncEnforceCache(t, e, "alice", "data2", "read", true)
testSyncEnforceCache(t, e, "alice", "data2", "write", true)
_, _ = e.RemovePolicies([][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
})
testSyncEnforceCache(t, e, "alice", "data1", "read", false)
testSyncEnforceCache(t, e, "bob", "data2", "write", false)
testSyncEnforceCache(t, e, "alice", "data2", "read", true)
testSyncEnforceCache(t, e, "alice", "data2", "write", true)
}

76
enforcer_cached_test.go Normal file
View File

@@ -0,0 +1,76 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import "testing"
func testEnforceCache(t *testing.T, e *CachedEnforcer, sub string, obj interface{}, act string, res bool) {
t.Helper()
if myRes, _ := e.Enforce(sub, obj, act); myRes != res {
t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, myRes, res)
}
}
func TestCache(t *testing.T) {
e, _ := NewCachedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
// The cache is enabled by default for NewCachedEnforcer.
testEnforceCache(t, e, "alice", "data1", "read", true)
testEnforceCache(t, e, "alice", "data1", "write", false)
testEnforceCache(t, e, "alice", "data2", "read", false)
testEnforceCache(t, e, "alice", "data2", "write", false)
// The cache is enabled, calling RemovePolicy, LoadPolicy or RemovePolicies will
// also operate cached items.
_, _ = e.RemovePolicy("alice", "data1", "read")
testEnforceCache(t, e, "alice", "data1", "read", false)
testEnforceCache(t, e, "alice", "data1", "write", false)
testEnforceCache(t, e, "alice", "data2", "read", false)
testEnforceCache(t, e, "alice", "data2", "write", false)
e, _ = NewCachedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
testEnforceCache(t, e, "alice", "data1", "read", true)
testEnforceCache(t, e, "bob", "data2", "write", true)
testEnforceCache(t, e, "alice", "data2", "read", true)
testEnforceCache(t, e, "alice", "data2", "write", true)
_, _ = e.RemovePolicies([][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
})
testEnforceCache(t, e, "alice", "data1", "read", false)
testEnforceCache(t, e, "bob", "data2", "write", false)
testEnforceCache(t, e, "alice", "data2", "read", true)
testEnforceCache(t, e, "alice", "data2", "write", true)
e, _ = NewCachedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
testEnforceCache(t, e, "alice", "data1", "read", true)
testEnforceCache(t, e, "bob", "data2", "write", true)
testEnforceCache(t, e, "alice", "data2", "read", true)
testEnforceCache(t, e, "alice", "data2", "write", true)
e.ClearPolicy()
testEnforceCache(t, e, "alice", "data1", "read", false)
testEnforceCache(t, e, "bob", "data2", "write", false)
testEnforceCache(t, e, "alice", "data2", "read", false)
testEnforceCache(t, e, "alice", "data2", "write", false)
}

861
enforcer_context.go Normal file
View File

@@ -0,0 +1,861 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"context"
"errors"
"fmt"
Err "github.com/casbin/casbin/v3/errors"
"github.com/casbin/casbin/v3/model"
"github.com/casbin/casbin/v3/persist"
)
// ContextEnforcer wraps Enforcer and provides context-aware operations.
type ContextEnforcer struct {
*Enforcer
adapterCtx persist.ContextAdapter
}
// NewContextEnforcer creates a context-aware enforcer via file or DB.
func NewContextEnforcer(params ...interface{}) (IEnforcerContext, error) {
e := &ContextEnforcer{}
var err error
e.Enforcer, err = NewEnforcer(params...)
if err != nil {
return nil, err
}
if e.adapter != nil {
if contextAdapter, ok := e.adapter.(persist.ContextAdapter); ok {
e.adapterCtx = contextAdapter
} else {
return nil, errors.New("adapter does not support context operations, ContextAdapter interface not implemented")
}
} else {
return nil, errors.New("no adapter provided, ContextEnforcer requires a ContextAdapter")
}
return e, nil
}
// LoadPolicyCtx loads all policy rules from the storage with context.
func (e *ContextEnforcer) LoadPolicyCtx(ctx context.Context) error {
newModel, err := e.loadPolicyFromAdapterCtx(ctx, e.model)
if err != nil {
return err
}
err = e.applyModifiedModel(newModel)
if err != nil {
return err
}
return nil
}
func (e *ContextEnforcer) loadPolicyFromAdapterCtx(ctx context.Context, baseModel model.Model) (model.Model, error) {
newModel := baseModel.Copy()
newModel.ClearPolicy()
if err := e.adapterCtx.LoadPolicyCtx(ctx, newModel); err != nil && err.Error() != "invalid file path, file path cannot be empty" {
return nil, err
}
if err := newModel.SortPoliciesBySubjectHierarchy(); err != nil {
return nil, err
}
if err := newModel.SortPoliciesByPriority(); err != nil {
return nil, err
}
return newModel, nil
}
// LoadFilteredPolicyCtx loads all policy rules from the storage with context and filter.
func (e *Enforcer) LoadFilteredPolicyCtx(ctx context.Context, filter interface{}) error {
e.model.ClearPolicy()
return e.loadFilteredPolicyCtx(ctx, filter)
}
// LoadIncrementalFilteredPolicyCtx append a filtered policy from file/database with context.
func (e *Enforcer) LoadIncrementalFilteredPolicyCtx(ctx context.Context, filter interface{}) error {
return e.loadFilteredPolicyCtx(ctx, filter)
}
func (e *Enforcer) loadFilteredPolicyCtx(ctx context.Context, filter interface{}) error {
e.invalidateMatcherMap()
var filteredAdapter persist.ContextFilteredAdapter
// Attempt to cast the Adapter as a FilteredAdapter
switch adapter := e.adapter.(type) {
case persist.ContextFilteredAdapter:
filteredAdapter = adapter
default:
return errors.New("filtered policies are not supported by this adapter")
}
if err := filteredAdapter.LoadFilteredPolicyCtx(ctx, e.model, filter); err != nil && err.Error() != "invalid file path, file path cannot be empty" {
return err
}
if err := e.model.SortPoliciesBySubjectHierarchy(); err != nil {
return err
}
if err := e.model.SortPoliciesByPriority(); err != nil {
return err
}
e.initRmMap()
e.model.PrintPolicy()
if e.autoBuildRoleLinks {
err := e.BuildRoleLinks()
if err != nil {
return err
}
}
return nil
}
// IsFilteredCtx returns true if the loaded policy has been filtered with context.
func (e *ContextEnforcer) IsFilteredCtx(ctx context.Context) bool {
if adapter, ok := e.adapter.(persist.ContextFilteredAdapter); ok {
return adapter.IsFilteredCtx(ctx)
} else {
return false
}
}
func (e *ContextEnforcer) SavePolicyCtx(ctx context.Context) error {
if e.IsFiltered() {
return errors.New("cannot save a filtered policy")
}
if err := e.adapterCtx.SavePolicyCtx(ctx, e.model); err != nil {
return err
}
if e.watcher != nil {
var err error
if watcher, ok := e.watcher.(persist.WatcherEx); ok {
err = watcher.UpdateForSavePolicy(e.model)
} else {
err = e.watcher.Update()
}
return err
}
return nil
}
// AddPolicyCtx adds a policy rule to the storage with context.
func (e *ContextEnforcer) AddPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) {
return e.AddNamedPolicyCtx(ctx, "p", params...)
}
// AddPoliciesCtx adds policy rules to the storage with context.
func (e *ContextEnforcer) AddPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) {
return e.AddNamedPoliciesCtx(ctx, "p", rules)
}
// AddNamedPolicyCtx adds a named policy rule to the storage with context.
func (e *ContextEnforcer) AddNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) {
if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
strSlice = append(make([]string, 0, len(strSlice)), strSlice...)
return e.addPolicyCtx(ctx, "p", ptype, strSlice)
}
policy := make([]string, 0)
for _, param := range params {
policy = append(policy, param.(string))
}
return e.addPolicyCtx(ctx, "p", ptype, policy)
}
// AddNamedPoliciesCtx adds named policy rules to the storage with context.
func (e *ContextEnforcer) AddNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
return e.addPoliciesCtx(ctx, "p", ptype, rules, false)
}
func (e *ContextEnforcer) AddPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error) {
return e.AddNamedPoliciesExCtx(ctx, "p", rules)
}
func (e *ContextEnforcer) AddNamedPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
return e.addPoliciesCtx(ctx, "p", ptype, rules, true)
}
// RemovePolicyCtx removes a policy rule from the storage with context.
func (e *ContextEnforcer) RemovePolicyCtx(ctx context.Context, params ...interface{}) (bool, error) {
return e.RemoveNamedPolicyCtx(ctx, "p", params...)
}
// RemoveNamedPolicyCtx removes a named policy rule from the storage with context.
func (e *ContextEnforcer) RemoveNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) {
if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
return e.removePolicyCtx(ctx, "p", ptype, strSlice)
}
policy := make([]string, 0)
for _, param := range params {
policy = append(policy, param.(string))
}
return e.removePolicyCtx(ctx, "p", ptype, policy)
}
// RemovePoliciesCtx removes policy rules from the storage with context.
func (e *ContextEnforcer) RemovePoliciesCtx(ctx context.Context, rules [][]string) (bool, error) {
return e.RemoveNamedPoliciesCtx(ctx, "p", rules)
}
// RemoveNamedPoliciesCtx removes named policy rules from the storage with context.
func (e *ContextEnforcer) RemoveNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
return e.removePoliciesCtx(ctx, "p", ptype, rules)
}
// RemoveFilteredPolicyCtx removes policy rules that match the filter from the storage with context.
func (e *ContextEnforcer) RemoveFilteredPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error) {
return e.RemoveFilteredNamedPolicyCtx(ctx, "p", fieldIndex, fieldValues...)
}
// RemoveFilteredNamedPolicyCtx removes named policy rules that match the filter from the storage with context.
func (e *ContextEnforcer) RemoveFilteredNamedPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error) {
return e.removeFilteredPolicyCtx(ctx, "p", ptype, fieldIndex, fieldValues)
}
// UpdatePolicyCtx updates a policy rule in the storage with context.
func (e *ContextEnforcer) UpdatePolicyCtx(ctx context.Context, oldPolicy []string, newPolicy []string) (bool, error) {
return e.UpdateNamedPolicyCtx(ctx, "p", oldPolicy, newPolicy)
}
// UpdateNamedPolicyCtx updates a named policy rule in the storage with context.
func (e *ContextEnforcer) UpdateNamedPolicyCtx(ctx context.Context, ptype string, p1 []string, p2 []string) (bool, error) {
return e.updatePolicyCtx(ctx, "p", ptype, p1, p2)
}
// UpdatePoliciesCtx updates policy rules in the storage with context.
func (e *ContextEnforcer) UpdatePoliciesCtx(ctx context.Context, oldPolicies [][]string, newPolicies [][]string) (bool, error) {
return e.UpdateNamedPoliciesCtx(ctx, "p", oldPolicies, newPolicies)
}
// UpdateNamedPoliciesCtx updates named policy rules in the storage with context.
func (e *ContextEnforcer) UpdateNamedPoliciesCtx(ctx context.Context, ptype string, p1 [][]string, p2 [][]string) (bool, error) {
return e.updatePoliciesCtx(ctx, "p", ptype, p1, p2)
}
// UpdateFilteredPoliciesCtx updates policy rules that match the filter in the storage with context.
func (e *ContextEnforcer) UpdateFilteredPoliciesCtx(ctx context.Context, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) {
return e.UpdateFilteredNamedPoliciesCtx(ctx, "p", newPolicies, fieldIndex, fieldValues...)
}
// UpdateFilteredNamedPoliciesCtx updates named policy rules that match the filter in the storage with context.
func (e *ContextEnforcer) UpdateFilteredNamedPoliciesCtx(ctx context.Context, ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) {
return e.updateFilteredPoliciesCtx(ctx, "p", ptype, newPolicies, fieldIndex, fieldValues...)
}
// Grouping Policy Context Methods
// AddGroupingPolicyCtx adds a grouping policy rule to the storage with context.
func (e *ContextEnforcer) AddGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) {
return e.AddNamedGroupingPolicyCtx(ctx, "g", params...)
}
// AddGroupingPoliciesCtx adds grouping policy rules to the storage with context.
func (e *ContextEnforcer) AddGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) {
return e.AddNamedGroupingPoliciesCtx(ctx, "g", rules)
}
func (e *ContextEnforcer) AddGroupingPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error) {
return e.AddNamedGroupingPoliciesExCtx(ctx, "g", rules)
}
// AddNamedGroupingPolicyCtx adds a named grouping policy rule to the storage with context.
func (e *ContextEnforcer) AddNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) {
var ruleAdded bool
var err error
if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
ruleAdded, err = e.addPolicyCtx(ctx, "g", ptype, strSlice)
} else {
policy := make([]string, 0)
for _, param := range params {
policy = append(policy, param.(string))
}
ruleAdded, err = e.addPolicyCtx(ctx, "g", ptype, policy)
}
return ruleAdded, err
}
// AddNamedGroupingPoliciesCtx adds named grouping policy rules to the storage with context.
func (e *ContextEnforcer) AddNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
return e.addPoliciesCtx(ctx, "g", ptype, rules, false)
}
func (e *ContextEnforcer) AddNamedGroupingPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
return e.addPoliciesCtx(ctx, "g", ptype, rules, true)
}
// RemoveGroupingPolicyCtx removes a grouping policy rule from the storage with context.
func (e *ContextEnforcer) RemoveGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) {
return e.RemoveNamedGroupingPolicyCtx(ctx, "g", params...)
}
// RemoveNamedGroupingPolicyCtx removes a named grouping policy rule from the storage with context.
func (e *ContextEnforcer) RemoveNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) {
var ruleRemoved bool
var err error
if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
ruleRemoved, err = e.removePolicyCtx(ctx, "g", ptype, strSlice)
} else {
policy := make([]string, 0)
for _, param := range params {
policy = append(policy, param.(string))
}
ruleRemoved, err = e.removePolicyCtx(ctx, "g", ptype, policy)
}
return ruleRemoved, err
}
// RemoveGroupingPoliciesCtx removes grouping policy rules from the storage with context.
func (e *ContextEnforcer) RemoveGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) {
return e.RemoveNamedGroupingPoliciesCtx(ctx, "g", rules)
}
// RemoveNamedGroupingPoliciesCtx removes named grouping policy rules from the storage with context.
func (e *ContextEnforcer) RemoveNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
return e.removePoliciesCtx(ctx, "g", ptype, rules)
}
// RemoveFilteredGroupingPolicyCtx removes grouping policy rules that match the filter from the storage with context.
func (e *ContextEnforcer) RemoveFilteredGroupingPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error) {
return e.RemoveFilteredNamedGroupingPolicyCtx(ctx, "g", fieldIndex, fieldValues...)
}
// RemoveFilteredNamedGroupingPolicyCtx removes named grouping policy rules that match the filter from the storage with context.
func (e *ContextEnforcer) RemoveFilteredNamedGroupingPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error) {
return e.removeFilteredPolicyCtx(ctx, "g", ptype, fieldIndex, fieldValues)
}
// UpdateGroupingPolicyCtx updates a grouping policy rule in the storage with context.
func (e *ContextEnforcer) UpdateGroupingPolicyCtx(ctx context.Context, oldRule []string, newRule []string) (bool, error) {
return e.UpdateNamedGroupingPolicyCtx(ctx, "g", oldRule, newRule)
}
// UpdateNamedGroupingPolicyCtx updates a named grouping policy rule in the storage with context.
func (e *ContextEnforcer) UpdateNamedGroupingPolicyCtx(ctx context.Context, ptype string, oldRule []string, newRule []string) (bool, error) {
return e.updatePolicyCtx(ctx, "g", ptype, oldRule, newRule)
}
// UpdateGroupingPoliciesCtx updates grouping policy rules in the storage with context.
func (e *ContextEnforcer) UpdateGroupingPoliciesCtx(ctx context.Context, oldRules [][]string, newRules [][]string) (bool, error) {
return e.UpdateNamedGroupingPoliciesCtx(ctx, "g", oldRules, newRules)
}
// UpdateNamedGroupingPoliciesCtx updates named grouping policy rules in the storage with context.
func (e *ContextEnforcer) UpdateNamedGroupingPoliciesCtx(ctx context.Context, ptype string, oldRules [][]string, newRules [][]string) (bool, error) {
return e.updatePoliciesCtx(ctx, "g", ptype, oldRules, newRules)
}
// Self Context Methods (bypass watcher notifications)
// SelfAddPolicyCtx adds a policy rule to the current policy with context.
func (e *ContextEnforcer) SelfAddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) {
return e.addPolicyWithoutNotifyCtx(ctx, sec, ptype, rule)
}
// SelfAddPoliciesCtx adds policy rules to the current policy with context.
func (e *ContextEnforcer) SelfAddPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) {
return e.addPoliciesWithoutNotifyCtx(ctx, sec, ptype, rules, false)
}
func (e *ContextEnforcer) SelfAddPoliciesExCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) {
return e.addPoliciesWithoutNotifyCtx(ctx, sec, ptype, rules, true)
}
// SelfRemovePolicyCtx removes a policy rule from the current policy with context.
func (e *ContextEnforcer) SelfRemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) {
return e.removePolicyWithoutNotifyCtx(ctx, sec, ptype, rule)
}
// SelfRemovePoliciesCtx removes policy rules from the current policy with context.
func (e *ContextEnforcer) SelfRemovePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) {
return e.removePoliciesWithoutNotifyCtx(ctx, sec, ptype, rules)
}
// SelfRemoveFilteredPolicyCtx removes policy rules that match the filter from the current policy with context.
func (e *ContextEnforcer) SelfRemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error) {
return e.removeFilteredPolicyWithoutNotifyCtx(ctx, sec, ptype, fieldIndex, fieldValues)
}
// SelfUpdatePolicyCtx updates a policy rule in the current policy with context.
func (e *ContextEnforcer) SelfUpdatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) (bool, error) {
return e.updatePolicyWithoutNotifyCtx(ctx, sec, ptype, oldRule, newRule)
}
// SelfUpdatePoliciesCtx updates policy rules in the current policy with context.
func (e *ContextEnforcer) SelfUpdatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules, newRules [][]string) (bool, error) {
return e.updatePoliciesWithoutNotifyCtx(ctx, sec, ptype, oldRules, newRules)
}
// Internal API methods with context support
// addPolicyWithoutNotifyCtx adds a rule to the current policy with context.
func (e *ContextEnforcer) addPolicyWithoutNotifyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) {
if e.dispatcher != nil && e.autoNotifyDispatcher {
return true, e.dispatcher.AddPolicies(sec, ptype, [][]string{rule})
}
hasPolicy, err := e.model.HasPolicy(sec, ptype, rule)
if hasPolicy || err != nil {
return false, err
}
if e.shouldPersist() {
if err = e.adapterCtx.AddPolicyCtx(ctx, sec, ptype, rule); err != nil {
if err.Error() != notImplemented {
return false, err
}
}
}
err = e.model.AddPolicy(sec, ptype, rule)
if err != nil {
return false, err
}
if sec == "g" {
err := e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{rule})
if err != nil {
return true, err
}
}
return true, nil
}
// addPoliciesWithoutNotifyCtx adds rules to the current policy with context.
func (e *ContextEnforcer) addPoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, rules [][]string, autoRemoveRepeat bool) (bool, error) {
if e.dispatcher != nil && e.autoNotifyDispatcher {
return true, e.dispatcher.AddPolicies(sec, ptype, rules)
}
if !autoRemoveRepeat {
hasPolicies, err := e.model.HasPolicies(sec, ptype, rules)
if hasPolicies || err != nil {
return false, err
}
}
if e.shouldPersist() {
if err := e.adapterCtx.(persist.ContextBatchAdapter).AddPoliciesCtx(ctx, sec, ptype, rules); err != nil {
if err.Error() != notImplemented {
return false, err
}
}
}
err := e.model.AddPolicies(sec, ptype, rules)
if err != nil {
return false, err
}
if sec == "g" {
err := e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, rules)
if err != nil {
return true, err
}
err = e.BuildIncrementalConditionalRoleLinks(model.PolicyAdd, ptype, rules)
if err != nil {
return true, err
}
}
return true, nil
}
// removePolicyWithoutNotifyCtx removes a rule from the current policy with context.
func (e *ContextEnforcer) removePolicyWithoutNotifyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) {
if e.dispatcher != nil && e.autoNotifyDispatcher {
return true, e.dispatcher.RemovePolicies(sec, ptype, [][]string{rule})
}
if e.shouldPersist() {
if err := e.adapterCtx.RemovePolicyCtx(ctx, sec, ptype, rule); err != nil {
if err.Error() != notImplemented {
return false, err
}
}
}
ruleRemoved, err := e.model.RemovePolicy(sec, ptype, rule)
if !ruleRemoved || err != nil {
return ruleRemoved, err
}
if sec == "g" {
err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{rule})
if err != nil {
return ruleRemoved, err
}
}
return ruleRemoved, nil
}
// removePoliciesWithoutNotifyCtx removes rules from the current policy with context.
func (e *ContextEnforcer) removePoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) {
if hasPolicies, err := e.model.HasPolicies(sec, ptype, rules); !hasPolicies || err != nil {
return hasPolicies, err
}
if e.dispatcher != nil && e.autoNotifyDispatcher {
return true, e.dispatcher.RemovePolicies(sec, ptype, rules)
}
if e.shouldPersist() {
if err := e.adapterCtx.(persist.ContextBatchAdapter).RemovePoliciesCtx(ctx, sec, ptype, rules); err != nil {
if err.Error() != notImplemented {
return false, err
}
}
}
rulesRemoved, err := e.model.RemovePolicies(sec, ptype, rules)
if !rulesRemoved || err != nil {
return rulesRemoved, err
}
if sec == "g" {
err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, rules)
if err != nil {
return rulesRemoved, err
}
}
return rulesRemoved, nil
}
// removeFilteredPolicyWithoutNotifyCtx removes policy rules that match the filter from the current policy with context.
func (e *ContextEnforcer) removeFilteredPolicyWithoutNotifyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues []string) (bool, error) {
if len(fieldValues) == 0 {
return false, Err.ErrInvalidFieldValuesParameter
}
if e.dispatcher != nil && e.autoNotifyDispatcher {
return true, e.dispatcher.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
}
if e.shouldPersist() {
if err := e.adapterCtx.RemoveFilteredPolicyCtx(ctx, sec, ptype, fieldIndex, fieldValues...); err != nil {
if err.Error() != notImplemented {
return false, err
}
}
}
ruleRemoved, effects, err := e.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
if !ruleRemoved || err != nil {
return ruleRemoved, err
}
if sec == "g" {
err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, effects)
if err != nil {
return ruleRemoved, err
}
}
return ruleRemoved, nil
}
// updatePolicyWithoutNotifyCtx updates a policy rule in the current policy with context.
func (e *ContextEnforcer) updatePolicyWithoutNotifyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) (bool, error) {
if e.dispatcher != nil && e.autoNotifyDispatcher {
return true, e.dispatcher.UpdatePolicy(sec, ptype, oldRule, newRule)
}
if e.shouldPersist() {
if err := e.adapterCtx.(persist.ContextUpdatableAdapter).UpdatePolicyCtx(ctx, sec, ptype, oldRule, newRule); err != nil {
if err.Error() != notImplemented {
return false, err
}
}
}
ruleUpdated, err := e.model.UpdatePolicy(sec, ptype, oldRule, newRule)
if !ruleUpdated || err != nil {
return ruleUpdated, err
}
if sec == "g" {
err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{oldRule}) // remove the old rule
if err != nil {
return ruleUpdated, err
}
err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{newRule}) // add the new rule
if err != nil {
return ruleUpdated, err
}
}
return ruleUpdated, nil
}
func (e *ContextEnforcer) updatePoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) {
if len(newRules) != len(oldRules) {
return false, fmt.Errorf("the length of oldRules should be equal to the length of newRules, but got the length of oldRules is %d, the length of newRules is %d", len(oldRules), len(newRules))
}
if e.dispatcher != nil && e.autoNotifyDispatcher {
return true, e.dispatcher.UpdatePolicies(sec, ptype, oldRules, newRules)
}
if e.shouldPersist() {
if err := e.adapterCtx.(persist.ContextUpdatableAdapter).UpdatePoliciesCtx(ctx, sec, ptype, oldRules, newRules); err != nil {
if err.Error() != notImplemented {
return false, err
}
}
}
ruleUpdated, err := e.model.UpdatePolicies(sec, ptype, oldRules, newRules)
if !ruleUpdated || err != nil {
return ruleUpdated, err
}
if sec == "g" {
err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rules
if err != nil {
return ruleUpdated, err
}
err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rules
if err != nil {
return ruleUpdated, err
}
}
return ruleUpdated, nil
}
func (e *ContextEnforcer) addPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) {
ok, err := e.addPolicyWithoutNotifyCtx(ctx, sec, ptype, rule)
if !ok || err != nil {
return ok, err
}
if e.shouldNotify() {
var err error
if watcher, ok := e.watcher.(persist.WatcherEx); ok {
err = watcher.UpdateForAddPolicy(sec, ptype, rule...)
} else {
err = e.watcher.Update()
}
return true, err
}
return true, nil
}
func (e *ContextEnforcer) addPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string, autoRemoveRepeat bool) (bool, error) {
ok, err := e.addPoliciesWithoutNotifyCtx(ctx, sec, ptype, rules, autoRemoveRepeat)
if !ok || err != nil {
return ok, err
}
if e.shouldNotify() {
var err error
if watcher, ok := e.watcher.(persist.WatcherEx); ok {
err = watcher.UpdateForAddPolicies(sec, ptype, rules...)
} else {
err = e.watcher.Update()
}
return true, err
}
return true, nil
}
func (e *ContextEnforcer) updatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule []string, newRule []string) (bool, error) {
ok, err := e.updatePolicyWithoutNotifyCtx(ctx, sec, ptype, oldRule, newRule)
if !ok || err != nil {
return ok, err
}
if e.shouldNotify() {
var err error
if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok {
err = watcher.UpdateForUpdatePolicy(sec, ptype, oldRule, newRule)
} else {
err = e.watcher.Update()
}
return true, err
}
return true, nil
}
func (e *ContextEnforcer) updatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) {
ok, err := e.updatePoliciesWithoutNotifyCtx(ctx, sec, ptype, oldRules, newRules)
if !ok || err != nil {
return ok, err
}
if e.shouldNotify() {
var err error
if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok {
err = watcher.UpdateForUpdatePolicies(sec, ptype, oldRules, newRules)
} else {
err = e.watcher.Update()
}
return true, err
}
return true, nil
}
func (e *ContextEnforcer) removePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) {
ok, err := e.removePolicyWithoutNotifyCtx(ctx, sec, ptype, rule)
if !ok || err != nil {
return ok, err
}
if e.shouldNotify() {
var err error
if watcher, ok := e.watcher.(persist.WatcherEx); ok {
err = watcher.UpdateForRemovePolicy(sec, ptype, rule...)
} else {
err = e.watcher.Update()
}
return true, err
}
return true, nil
}
func (e *ContextEnforcer) removePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) {
ok, err := e.removePoliciesWithoutNotifyCtx(ctx, sec, ptype, rules)
if !ok || err != nil {
return ok, err
}
if e.shouldNotify() {
var err error
if watcher, ok := e.watcher.(persist.WatcherEx); ok {
err = watcher.UpdateForRemovePolicies(sec, ptype, rules...)
} else {
err = e.watcher.Update()
}
return true, err
}
return true, nil
}
// removeFilteredPolicy removes rules based on field filters from the current policy.
func (e *ContextEnforcer) removeFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues []string) (bool, error) {
ok, err := e.removeFilteredPolicyWithoutNotifyCtx(ctx, sec, ptype, fieldIndex, fieldValues)
if !ok || err != nil {
return ok, err
}
if e.shouldNotify() {
var err error
if watcher, ok := e.watcher.(persist.WatcherEx); ok {
err = watcher.UpdateForRemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
} else {
err = e.watcher.Update()
}
return true, err
}
return true, nil
}
func (e *ContextEnforcer) updateFilteredPoliciesCtx(ctx context.Context, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) (bool, error) {
oldRules, err := e.updateFilteredPoliciesWithoutNotifyCtx(ctx, sec, ptype, newRules, fieldIndex, fieldValues...)
ok := len(oldRules) != 0
if !ok || err != nil {
return ok, err
}
if e.shouldNotify() {
var err error
if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok {
err = watcher.UpdateForUpdatePolicies(sec, ptype, oldRules, newRules)
} else {
err = e.watcher.Update()
}
return true, err
}
return true, nil
}
func (e *ContextEnforcer) updateFilteredPoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) {
var (
oldRules [][]string
err error
)
if _, err = e.model.GetAssertion(sec, ptype); err != nil {
return oldRules, err
}
if e.shouldPersist() {
if oldRules, err = e.adapter.(persist.ContextUpdatableAdapter).UpdateFilteredPoliciesCtx(ctx, sec, ptype, newRules, fieldIndex, fieldValues...); err != nil {
if err.Error() != notImplemented {
return nil, err
}
}
// For compatibility, because some adapters return oldRules containing ptype, see https://github.com/casbin/xorm-adapter/issues/49
for i, oldRule := range oldRules {
if len(oldRules[i]) == len(e.model[sec][ptype].Tokens)+1 {
oldRules[i] = oldRule[1:]
}
}
}
if e.dispatcher != nil && e.autoNotifyDispatcher {
return oldRules, e.dispatcher.UpdateFilteredPolicies(sec, ptype, oldRules, newRules)
}
ruleChanged, err := e.model.RemovePolicies(sec, ptype, oldRules)
if err != nil {
return oldRules, err
}
err = e.model.AddPolicies(sec, ptype, newRules)
if err != nil {
return oldRules, err
}
ruleChanged = ruleChanged && len(newRules) != 0
if !ruleChanged {
return make([][]string, 0), nil
}
if sec == "g" {
err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rules
if err != nil {
return oldRules, err
}
err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rules
if err != nil {
return oldRules, err
}
}
return oldRules, nil
}

View File

@@ -0,0 +1,99 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import "context"
type IEnforcerContext interface {
IEnforcer
/* Enforcer API */
LoadPolicyCtx(ctx context.Context) error
LoadFilteredPolicyCtx(ctx context.Context, filter interface{}) error
LoadIncrementalFilteredPolicyCtx(ctx context.Context, filter interface{}) error
IsFilteredCtx(ctx context.Context) bool
SavePolicyCtx(ctx context.Context) error
/* RBAC API */
AddRoleForUserCtx(ctx context.Context, user string, role string, domain ...string) (bool, error)
AddPermissionForUserCtx(ctx context.Context, user string, permission ...string) (bool, error)
AddPermissionsForUserCtx(ctx context.Context, user string, permissions ...[]string) (bool, error)
DeletePermissionForUserCtx(ctx context.Context, user string, permission ...string) (bool, error)
DeletePermissionsForUserCtx(ctx context.Context, user string) (bool, error)
DeleteRoleForUserCtx(ctx context.Context, user string, role string, domain ...string) (bool, error)
DeleteRolesForUserCtx(ctx context.Context, user string, domain ...string) (bool, error)
DeleteUserCtx(ctx context.Context, user string) (bool, error)
DeleteRoleCtx(ctx context.Context, role string) (bool, error)
DeletePermissionCtx(ctx context.Context, permission ...string) (bool, error)
/* RBAC API with domains*/
AddRoleForUserInDomainCtx(ctx context.Context, user string, role string, domain string) (bool, error)
DeleteRoleForUserInDomainCtx(ctx context.Context, user string, role string, domain string) (bool, error)
DeleteRolesForUserInDomainCtx(ctx context.Context, user string, domain string) (bool, error)
DeleteAllUsersByDomainCtx(ctx context.Context, domain string) (bool, error)
DeleteDomainsCtx(ctx context.Context, domains ...string) (bool, error)
/* Management API */
AddPolicyCtx(ctx context.Context, params ...interface{}) (bool, error)
AddPoliciesCtx(ctx context.Context, rules [][]string) (bool, error)
AddNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error)
AddNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
AddPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error)
AddNamedPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
RemovePolicyCtx(ctx context.Context, params ...interface{}) (bool, error)
RemovePoliciesCtx(ctx context.Context, rules [][]string) (bool, error)
RemoveFilteredPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error)
RemoveNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error)
RemoveNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
RemoveFilteredNamedPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error)
AddGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error)
AddGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error)
AddGroupingPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error)
AddNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error)
AddNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
AddNamedGroupingPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
RemoveGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error)
RemoveGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error)
RemoveFilteredGroupingPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error)
RemoveNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error)
RemoveNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
RemoveFilteredNamedGroupingPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error)
UpdatePolicyCtx(ctx context.Context, oldPolicy []string, newPolicy []string) (bool, error)
UpdatePoliciesCtx(ctx context.Context, oldPolicies [][]string, newPolicies [][]string) (bool, error)
UpdateFilteredPoliciesCtx(ctx context.Context, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error)
UpdateGroupingPolicyCtx(ctx context.Context, oldRule []string, newRule []string) (bool, error)
UpdateGroupingPoliciesCtx(ctx context.Context, oldRules [][]string, newRules [][]string) (bool, error)
UpdateNamedGroupingPolicyCtx(ctx context.Context, ptype string, oldRule []string, newRule []string) (bool, error)
UpdateNamedGroupingPoliciesCtx(ctx context.Context, ptype string, oldRules [][]string, newRules [][]string) (bool, error)
/* Management API with autoNotifyWatcher disabled */
SelfAddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error)
SelfAddPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error)
SelfAddPoliciesExCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error)
SelfRemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error)
SelfRemovePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error)
SelfRemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error)
SelfUpdatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) (bool, error)
SelfUpdatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules, newRules [][]string) (bool, error)
}

235
enforcer_context_test.go Normal file
View File

@@ -0,0 +1,235 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"context"
"testing"
"time"
)
func TestIEnforcerContext_BasicOperations(t *testing.T) {
e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("NewContextEnforcer failed: %v", err)
}
ctx := context.Background()
err = e.LoadPolicyCtx(ctx)
if err != nil {
t.Fatalf("LoadPolicyCtx failed: %v", err)
}
added, err := e.AddPolicyCtx(ctx, "eve", "data3", "read")
if err != nil {
t.Fatalf("AddPolicyCtx failed: %v", err)
}
if !added {
t.Error("AddPolicyCtx should return true for new policy")
}
removed, err := e.RemovePolicyCtx(ctx, "eve", "data3", "read")
if err != nil {
t.Fatalf("RemovePolicyCtx failed: %v", err)
}
if !removed {
t.Error("RemovePolicyCtx should return true for existing policy")
}
err = e.SavePolicyCtx(ctx)
if err != nil {
t.Fatalf("SavePolicyCtx failed: %v", err)
}
}
func TestIEnforcerContext_RBACOperations(t *testing.T) {
e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("NewContextEnforcer failed: %v", err)
}
ctx := context.Background()
added, err := e.AddRoleForUserCtx(ctx, "eve", "data1_admin")
if err != nil {
t.Fatalf("AddRoleForUserCtx failed: %v", err)
}
if !added {
t.Error("AddRoleForUserCtx should return true for new role")
}
added, err = e.AddPermissionForUserCtx(ctx, "eve", "data3", "write")
if err != nil {
t.Fatalf("AddPermissionForUserCtx failed: %v", err)
}
if !added {
t.Error("AddPermissionForUserCtx should return true for new permission")
}
deleted, err := e.DeleteRoleForUserCtx(ctx, "eve", "data1_admin")
if err != nil {
t.Fatalf("DeleteRoleForUserCtx failed: %v", err)
}
if !deleted {
t.Error("DeleteRoleForUserCtx should return true for existing role")
}
deleted, err = e.DeletePermissionForUserCtx(ctx, "eve", "data3", "write")
if err != nil {
t.Fatalf("DeletePermissionForUserCtx failed: %v", err)
}
if !deleted {
t.Error("DeletePermissionForUserCtx should return true for existing permission")
}
}
func TestIEnforcerContext_BatchOperations(t *testing.T) {
e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("NewContextEnforcer failed: %v", err)
}
ctx := context.Background()
rules := [][]string{
{"eve", "data3", "read"},
{"eve", "data3", "write"},
}
added, err := e.AddPoliciesCtx(ctx, rules)
if err != nil {
t.Fatalf("AddPoliciesCtx failed: %v", err)
}
if !added {
t.Error("AddPoliciesCtx should return true for new policies")
}
removed, err := e.RemovePoliciesCtx(ctx, rules)
if err != nil {
t.Fatalf("RemovePoliciesCtx failed: %v", err)
}
if !removed {
t.Error("RemovePoliciesCtx should return true for existing policies")
}
}
func TestIEnforcerContext_ContextCancellation(t *testing.T) {
e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("NewContextEnforcer failed: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
err = e.LoadPolicyCtx(ctx)
if err != nil {
t.Logf("LoadPolicyCtx with cancelled context returned error: %v", err)
}
}
func TestIEnforcerContext_ContextTimeout(t *testing.T) {
e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("NewContextEnforcer failed: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()
time.Sleep(2 * time.Millisecond)
_, err = e.AddPolicyCtx(ctx, "test", "data", "read")
if err != nil {
t.Logf("AddPolicyCtx with timeout context returned error: %v", err)
}
}
func TestIEnforcerContext_GroupingPolicyOperations(t *testing.T) {
e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("NewContextEnforcer failed: %v", err)
}
ctx := context.Background()
added, err := e.AddGroupingPolicyCtx(ctx, "eve", "data3_admin")
if err != nil {
t.Fatalf("AddGroupingPolicyCtx failed: %v", err)
}
if !added {
t.Error("AddGroupingPolicyCtx should return true for new grouping policy")
}
removed, err := e.RemoveGroupingPolicyCtx(ctx, "eve", "data3_admin")
if err != nil {
t.Fatalf("RemoveGroupingPolicyCtx failed: %v", err)
}
if !removed {
t.Error("RemoveGroupingPolicyCtx should return true for existing grouping policy")
}
}
func TestIEnforcerContext_UpdateOperations(t *testing.T) {
e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("NewContextEnforcer failed: %v", err)
}
ctx := context.Background()
_, err = e.AddPolicyCtx(ctx, "eve", "data3", "read")
if err != nil {
t.Fatalf("AddPolicyCtx failed: %v", err)
}
updated, err := e.UpdatePolicyCtx(ctx, []string{"eve", "data3", "read"}, []string{"eve", "data3", "write"})
if err != nil {
t.Fatalf("UpdatePolicyCtx failed: %v", err)
}
if !updated {
t.Error("UpdatePolicyCtx should return true for successful update")
}
_, _ = e.RemovePolicyCtx(ctx, "eve", "data3", "write")
}
func TestIEnforcerContext_SelfMethods(t *testing.T) {
e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
if err != nil {
t.Fatalf("NewContextEnforcer failed: %v", err)
}
ctx := context.Background()
added, err := e.SelfAddPolicyCtx(ctx, "p", "p", []string{"eve", "data3", "read"})
if err != nil {
t.Fatalf("SelfAddPolicyCtx failed: %v", err)
}
if !added {
t.Error("SelfAddPolicyCtx should return true for new policy")
}
removed, err := e.SelfRemovePolicyCtx(ctx, "p", "p", []string{"eve", "data3", "read"})
if err != nil {
t.Fatalf("SelfRemovePolicyCtx failed: %v", err)
}
if !removed {
t.Error("SelfRemovePolicyCtx should return true for existing policy")
}
}

256
enforcer_distributed.go Normal file
View File

@@ -0,0 +1,256 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"github.com/casbin/casbin/v3/model"
"github.com/casbin/casbin/v3/persist"
)
// DistributedEnforcer wraps SyncedEnforcer for dispatcher.
type DistributedEnforcer struct {
*SyncedEnforcer
}
func NewDistributedEnforcer(params ...interface{}) (*DistributedEnforcer, error) {
e := &DistributedEnforcer{}
var err error
e.SyncedEnforcer, err = NewSyncedEnforcer(params...)
if err != nil {
return nil, err
}
return e, nil
}
// SetDispatcher sets the current dispatcher.
func (d *DistributedEnforcer) SetDispatcher(dispatcher persist.Dispatcher) {
d.dispatcher = dispatcher
}
// AddPoliciesSelf provides a method for dispatcher to add authorization rules to the current policy.
// The function returns the rules affected and error.
func (d *DistributedEnforcer) AddPoliciesSelf(shouldPersist func() bool, sec string, ptype string, rules [][]string) (affected [][]string, err error) {
d.m.Lock()
defer d.m.Unlock()
if shouldPersist != nil && shouldPersist() {
var noExistsPolicy [][]string
for _, rule := range rules {
var hasPolicy bool
hasPolicy, err = d.model.HasPolicy(sec, ptype, rule)
if err != nil {
return nil, err
}
if !hasPolicy {
noExistsPolicy = append(noExistsPolicy, rule)
}
}
if err = d.adapter.(persist.BatchAdapter).AddPolicies(sec, ptype, noExistsPolicy); err != nil && err.Error() != notImplemented {
return nil, err
}
}
affected, err = d.model.AddPoliciesWithAffected(sec, ptype, rules)
if err != nil {
return affected, err
}
if sec == "g" {
err := d.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, affected)
if err != nil {
return affected, err
}
}
return affected, nil
}
// RemovePoliciesSelf provides a method for dispatcher to remove a set of rules from current policy.
// The function returns the rules affected and error.
func (d *DistributedEnforcer) RemovePoliciesSelf(shouldPersist func() bool, sec string, ptype string, rules [][]string) (affected [][]string, err error) {
d.m.Lock()
defer d.m.Unlock()
if shouldPersist != nil && shouldPersist() {
if err = d.adapter.(persist.BatchAdapter).RemovePolicies(sec, ptype, rules); err != nil {
if err.Error() != notImplemented {
return nil, err
}
}
}
affected, err = d.model.RemovePoliciesWithAffected(sec, ptype, rules)
if err != nil {
return affected, err
}
if sec == "g" {
err = d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, affected)
if err != nil {
return affected, err
}
}
return affected, err
}
// RemoveFilteredPolicySelf provides a method for dispatcher to remove an authorization rule from the current policy, field filters can be specified.
// The function returns the rules affected and error.
func (d *DistributedEnforcer) RemoveFilteredPolicySelf(shouldPersist func() bool, sec string, ptype string, fieldIndex int, fieldValues ...string) (affected [][]string, err error) {
d.m.Lock()
defer d.m.Unlock()
if shouldPersist != nil && shouldPersist() {
if err = d.adapter.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...); err != nil {
if err.Error() != notImplemented {
return nil, err
}
}
}
_, affected, err = d.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
if err != nil {
return affected, err
}
if sec == "g" {
err := d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, affected)
if err != nil {
return affected, err
}
}
return affected, nil
}
// ClearPolicySelf provides a method for dispatcher to clear all rules from the current policy.
func (d *DistributedEnforcer) ClearPolicySelf(shouldPersist func() bool) error {
d.m.Lock()
defer d.m.Unlock()
if shouldPersist != nil && shouldPersist() {
err := d.adapter.SavePolicy(nil)
if err != nil {
return err
}
}
d.model.ClearPolicy()
return nil
}
// UpdatePolicySelf provides a method for dispatcher to update an authorization rule from the current policy.
func (d *DistributedEnforcer) UpdatePolicySelf(shouldPersist func() bool, sec string, ptype string, oldRule, newRule []string) (affected bool, err error) {
d.m.Lock()
defer d.m.Unlock()
if shouldPersist != nil && shouldPersist() {
err = d.adapter.(persist.UpdatableAdapter).UpdatePolicy(sec, ptype, oldRule, newRule)
if err != nil {
return false, err
}
}
ruleUpdated, err := d.model.UpdatePolicy(sec, ptype, oldRule, newRule)
if !ruleUpdated || err != nil {
return ruleUpdated, err
}
if sec == "g" {
err := d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{oldRule}) // remove the old rule
if err != nil {
return ruleUpdated, err
}
err = d.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{newRule}) // add the new rule
if err != nil {
return ruleUpdated, err
}
}
return ruleUpdated, nil
}
// UpdatePoliciesSelf provides a method for dispatcher to update a set of authorization rules from the current policy.
func (d *DistributedEnforcer) UpdatePoliciesSelf(shouldPersist func() bool, sec string, ptype string, oldRules, newRules [][]string) (affected bool, err error) {
d.m.Lock()
defer d.m.Unlock()
if shouldPersist != nil && shouldPersist() {
err = d.adapter.(persist.UpdatableAdapter).UpdatePolicies(sec, ptype, oldRules, newRules)
if err != nil {
return false, err
}
}
ruleUpdated, err := d.model.UpdatePolicies(sec, ptype, oldRules, newRules)
if !ruleUpdated || err != nil {
return ruleUpdated, err
}
if sec == "g" {
err := d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rule
if err != nil {
return ruleUpdated, err
}
err = d.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rule
if err != nil {
return ruleUpdated, err
}
}
return ruleUpdated, nil
}
// UpdateFilteredPoliciesSelf provides a method for dispatcher to update a set of authorization rules from the current policy.
func (d *DistributedEnforcer) UpdateFilteredPoliciesSelf(shouldPersist func() bool, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) (bool, error) {
d.m.Lock()
defer d.m.Unlock()
var (
oldRules [][]string
err error
)
if shouldPersist != nil && shouldPersist() {
oldRules, err = d.adapter.(persist.UpdatableAdapter).UpdateFilteredPolicies(sec, ptype, newRules, fieldIndex, fieldValues...)
if err != nil {
return false, err
}
}
ruleChanged, err := d.model.RemovePolicies(sec, ptype, oldRules)
if err != nil {
return ruleChanged, err
}
err = d.model.AddPolicies(sec, ptype, newRules)
if err != nil {
return ruleChanged, err
}
ruleChanged = ruleChanged && len(newRules) != 0
if !ruleChanged {
return ruleChanged, nil
}
if sec == "g" {
err := d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rule
if err != nil {
return ruleChanged, err
}
err = d.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rule
if err != nil {
return ruleChanged, err
}
}
return true, nil
}

184
enforcer_interface.go Normal file
View File

@@ -0,0 +1,184 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"github.com/casbin/casbin/v3/effector"
"github.com/casbin/casbin/v3/model"
"github.com/casbin/casbin/v3/persist"
"github.com/casbin/casbin/v3/rbac"
"github.com/casbin/govaluate"
)
var (
_ IEnforcer = &Enforcer{}
_ IEnforcer = &SyncedEnforcer{}
_ IEnforcer = &CachedEnforcer{}
)
// IEnforcer is the API interface of Enforcer.
type IEnforcer interface {
/* Enforcer API */
InitWithFile(modelPath string, policyPath string) error
InitWithAdapter(modelPath string, adapter persist.Adapter) error
InitWithModelAndAdapter(m model.Model, adapter persist.Adapter) error
LoadModel() error
GetModel() model.Model
SetModel(m model.Model)
GetAdapter() persist.Adapter
SetAdapter(adapter persist.Adapter)
SetWatcher(watcher persist.Watcher) error
GetRoleManager() rbac.RoleManager
SetRoleManager(rm rbac.RoleManager)
SetEffector(eft effector.Effector)
SetAIConfig(config AIConfig)
ClearPolicy()
LoadPolicy() error
LoadFilteredPolicy(filter interface{}) error
LoadIncrementalFilteredPolicy(filter interface{}) error
IsFiltered() bool
SavePolicy() error
EnableEnforce(enable bool)
EnableAutoNotifyWatcher(enable bool)
EnableAutoSave(autoSave bool)
EnableAutoBuildRoleLinks(autoBuildRoleLinks bool)
BuildRoleLinks() error
Enforce(rvals ...interface{}) (bool, error)
EnforceWithMatcher(matcher string, rvals ...interface{}) (bool, error)
EnforceEx(rvals ...interface{}) (bool, []string, error)
EnforceExWithMatcher(matcher string, rvals ...interface{}) (bool, []string, error)
BatchEnforce(requests [][]interface{}) ([]bool, error)
BatchEnforceWithMatcher(matcher string, requests [][]interface{}) ([]bool, error)
Explain(rvals ...interface{}) (string, error)
/* RBAC API */
GetRolesForUser(name string, domain ...string) ([]string, error)
GetUsersForRole(name string, domain ...string) ([]string, error)
HasRoleForUser(name string, role string, domain ...string) (bool, error)
AddRoleForUser(user string, role string, domain ...string) (bool, error)
AddPermissionForUser(user string, permission ...string) (bool, error)
AddPermissionsForUser(user string, permissions ...[]string) (bool, error)
DeletePermissionForUser(user string, permission ...string) (bool, error)
DeletePermissionsForUser(user string) (bool, error)
GetPermissionsForUser(user string, domain ...string) ([][]string, error)
HasPermissionForUser(user string, permission ...string) (bool, error)
GetImplicitRolesForUser(name string, domain ...string) ([]string, error)
GetImplicitPermissionsForUser(user string, domain ...string) ([][]string, error)
GetImplicitUsersForPermission(permission ...string) ([]string, error)
DeleteRoleForUser(user string, role string, domain ...string) (bool, error)
DeleteRolesForUser(user string, domain ...string) (bool, error)
DeleteUser(user string) (bool, error)
DeleteRole(role string) (bool, error)
DeletePermission(permission ...string) (bool, error)
/* RBAC API with domains*/
GetUsersForRoleInDomain(name string, domain string) []string
GetRolesForUserInDomain(name string, domain string) []string
GetPermissionsForUserInDomain(user string, domain string) [][]string
AddRoleForUserInDomain(user string, role string, domain string) (bool, error)
DeleteRoleForUserInDomain(user string, role string, domain string) (bool, error)
GetAllUsersByDomain(domain string) ([]string, error)
DeleteRolesForUserInDomain(user string, domain string) (bool, error)
DeleteAllUsersByDomain(domain string) (bool, error)
DeleteDomains(domains ...string) (bool, error)
GetAllDomains() ([]string, error)
GetAllRolesByDomain(domain string) ([]string, error)
/* Management API */
GetAllSubjects() ([]string, error)
GetAllNamedSubjects(ptype string) ([]string, error)
GetAllObjects() ([]string, error)
GetAllNamedObjects(ptype string) ([]string, error)
GetAllActions() ([]string, error)
GetAllNamedActions(ptype string) ([]string, error)
GetAllRoles() ([]string, error)
GetAllNamedRoles(ptype string) ([]string, error)
GetAllUsers() ([]string, error)
GetPolicy() ([][]string, error)
GetFilteredPolicy(fieldIndex int, fieldValues ...string) ([][]string, error)
GetNamedPolicy(ptype string) ([][]string, error)
GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error)
GetGroupingPolicy() ([][]string, error)
GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) ([][]string, error)
GetNamedGroupingPolicy(ptype string) ([][]string, error)
GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error)
HasPolicy(params ...interface{}) (bool, error)
HasNamedPolicy(ptype string, params ...interface{}) (bool, error)
AddPolicy(params ...interface{}) (bool, error)
AddPolicies(rules [][]string) (bool, error)
AddNamedPolicy(ptype string, params ...interface{}) (bool, error)
AddNamedPolicies(ptype string, rules [][]string) (bool, error)
AddPoliciesEx(rules [][]string) (bool, error)
AddNamedPoliciesEx(ptype string, rules [][]string) (bool, error)
RemovePolicy(params ...interface{}) (bool, error)
RemovePolicies(rules [][]string) (bool, error)
RemoveFilteredPolicy(fieldIndex int, fieldValues ...string) (bool, error)
RemoveNamedPolicy(ptype string, params ...interface{}) (bool, error)
RemoveNamedPolicies(ptype string, rules [][]string) (bool, error)
RemoveFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error)
HasGroupingPolicy(params ...interface{}) (bool, error)
HasNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error)
AddGroupingPolicy(params ...interface{}) (bool, error)
AddGroupingPolicies(rules [][]string) (bool, error)
AddGroupingPoliciesEx(rules [][]string) (bool, error)
AddNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error)
AddNamedGroupingPolicies(ptype string, rules [][]string) (bool, error)
AddNamedGroupingPoliciesEx(ptype string, rules [][]string) (bool, error)
RemoveGroupingPolicy(params ...interface{}) (bool, error)
RemoveGroupingPolicies(rules [][]string) (bool, error)
RemoveFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) (bool, error)
RemoveNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error)
RemoveNamedGroupingPolicies(ptype string, rules [][]string) (bool, error)
RemoveFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error)
AddFunction(name string, function govaluate.ExpressionFunction)
UpdatePolicy(oldPolicy []string, newPolicy []string) (bool, error)
UpdatePolicies(oldPolicies [][]string, newPolicies [][]string) (bool, error)
UpdateFilteredPolicies(newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error)
UpdateGroupingPolicy(oldRule []string, newRule []string) (bool, error)
UpdateGroupingPolicies(oldRules [][]string, newRules [][]string) (bool, error)
UpdateNamedGroupingPolicy(ptype string, oldRule []string, newRule []string) (bool, error)
UpdateNamedGroupingPolicies(ptype string, oldRules [][]string, newRules [][]string) (bool, error)
/* Management API with autoNotifyWatcher disabled */
SelfAddPolicy(sec string, ptype string, rule []string) (bool, error)
SelfAddPolicies(sec string, ptype string, rules [][]string) (bool, error)
SelfAddPoliciesEx(sec string, ptype string, rules [][]string) (bool, error)
SelfRemovePolicy(sec string, ptype string, rule []string) (bool, error)
SelfRemovePolicies(sec string, ptype string, rules [][]string) (bool, error)
SelfRemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error)
SelfUpdatePolicy(sec string, ptype string, oldRule, newRule []string) (bool, error)
SelfUpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) (bool, error)
}
var _ IDistributedEnforcer = &DistributedEnforcer{}
// IDistributedEnforcer defines dispatcher enforcer.
type IDistributedEnforcer interface {
IEnforcer
SetDispatcher(dispatcher persist.Dispatcher)
/* Management API for DistributedEnforcer*/
AddPoliciesSelf(shouldPersist func() bool, sec string, ptype string, rules [][]string) (affected [][]string, err error)
RemovePoliciesSelf(shouldPersist func() bool, sec string, ptype string, rules [][]string) (affected [][]string, err error)
RemoveFilteredPolicySelf(shouldPersist func() bool, sec string, ptype string, fieldIndex int, fieldValues ...string) (affected [][]string, err error)
ClearPolicySelf(shouldPersist func() bool) error
UpdatePolicySelf(shouldPersist func() bool, sec string, ptype string, oldRule, newRule []string) (affected bool, err error)
UpdatePoliciesSelf(shouldPersist func() bool, sec string, ptype string, oldRules, newRules [][]string) (affected bool, err error)
UpdateFilteredPoliciesSelf(shouldPersist func() bool, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) (bool, error)
}

83
enforcer_json_test.go Normal file
View File

@@ -0,0 +1,83 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"strings"
"testing"
"github.com/casbin/casbin/v3/model"
)
func TestInvalidJsonRequest(t *testing.T) {
modelText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub.Name == " "
`
m, err := model.NewModelFromString(modelText)
if err != nil {
t.Fatalf("Failed to create model: %v", err)
}
e, err := NewEnforcer(m)
if err != nil {
t.Fatalf("Failed to create enforcer: %v", err)
}
e.EnableAcceptJsonRequest(true)
// Test with invalid JSON (contains \x escape sequence which is not valid in JSON)
invalidJSON := `{"Name": "\x20"}`
_, err = e.Enforce(invalidJSON, "obj", "read")
if err == nil {
t.Fatalf("Expected error for invalid JSON, got nil")
}
if !strings.Contains(err.Error(), "failed to parse JSON parameter") {
t.Fatalf("Expected error message to contain 'failed to parse JSON parameter', got: %v", err)
}
// Test with valid JSON - should work
validJSON := `{"Name": " "}`
res, err := e.Enforce(validJSON, "obj", "read")
if err != nil {
t.Fatalf("Valid JSON should not return error: %v", err)
}
if !res {
t.Fatalf("Expected true for valid JSON with matching Name")
}
// Test with plain string (doesn't start with { or [) - should not try to parse as JSON
plainString := "alice"
_, err = e.Enforce(plainString, "obj", "read")
// This will fail because plainString is not a struct with Name field,
// but it shouldn't fail with JSON parsing error
if err != nil && strings.Contains(err.Error(), "failed to parse JSON parameter") {
t.Fatalf("Plain string should not trigger JSON parsing error: %v", err)
}
}

689
enforcer_synced.go Normal file
View File

@@ -0,0 +1,689 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"sync"
"sync/atomic"
"time"
"github.com/casbin/govaluate"
"github.com/casbin/casbin/v3/persist"
"github.com/casbin/casbin/v3/rbac"
)
// SyncedEnforcer wraps Enforcer and provides synchronized access.
type SyncedEnforcer struct {
*Enforcer
m sync.RWMutex
stopAutoLoad chan struct{}
autoLoadRunning int32
}
// NewSyncedEnforcer creates a synchronized enforcer via file or DB.
func NewSyncedEnforcer(params ...interface{}) (*SyncedEnforcer, error) {
e := &SyncedEnforcer{}
var err error
e.Enforcer, err = NewEnforcer(params...)
if err != nil {
return nil, err
}
e.stopAutoLoad = make(chan struct{}, 1)
e.autoLoadRunning = 0
return e, nil
}
// GetLock return the private RWMutex lock.
func (e *SyncedEnforcer) GetLock() *sync.RWMutex {
return &e.m
}
// GetRoleManager gets the current role manager with synchronization.
func (e *SyncedEnforcer) GetRoleManager() rbac.RoleManager {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetRoleManager()
}
// GetNamedRoleManager gets the role manager for the named policy with synchronization.
func (e *SyncedEnforcer) GetNamedRoleManager(ptype string) rbac.RoleManager {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetNamedRoleManager(ptype)
}
// SetRoleManager sets the current role manager with synchronization.
func (e *SyncedEnforcer) SetRoleManager(rm rbac.RoleManager) {
e.m.Lock()
defer e.m.Unlock()
e.Enforcer.SetRoleManager(rm)
}
// SetNamedRoleManager sets the role manager for the named policy with synchronization.
func (e *SyncedEnforcer) SetNamedRoleManager(ptype string, rm rbac.RoleManager) {
e.m.Lock()
defer e.m.Unlock()
e.Enforcer.SetNamedRoleManager(ptype, rm)
}
// IsAutoLoadingRunning check if SyncedEnforcer is auto loading policies.
func (e *SyncedEnforcer) IsAutoLoadingRunning() bool {
return atomic.LoadInt32(&(e.autoLoadRunning)) != 0
}
// StartAutoLoadPolicy starts a go routine that will every specified duration call LoadPolicy.
func (e *SyncedEnforcer) StartAutoLoadPolicy(d time.Duration) {
// Don't start another goroutine if there is already one running
if !atomic.CompareAndSwapInt32(&e.autoLoadRunning, 0, 1) {
return
}
ticker := time.NewTicker(d)
go func() {
defer func() {
ticker.Stop()
atomic.StoreInt32(&(e.autoLoadRunning), int32(0))
}()
n := 1
for {
select {
case <-ticker.C:
// error intentionally ignored
_ = e.LoadPolicy()
// Uncomment this line to see when the policy is loaded.
// log.Print("Load policy for time: ", n)
n++
case <-e.stopAutoLoad:
return
}
}
}()
}
// StopAutoLoadPolicy causes the go routine to exit.
func (e *SyncedEnforcer) StopAutoLoadPolicy() {
if e.IsAutoLoadingRunning() {
e.stopAutoLoad <- struct{}{}
}
}
// SetWatcher sets the current watcher.
func (e *SyncedEnforcer) SetWatcher(watcher persist.Watcher) error {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.SetWatcher(watcher)
}
// LoadModel reloads the model from the model CONF file.
func (e *SyncedEnforcer) LoadModel() error {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.LoadModel()
}
// ClearPolicy clears all policy.
func (e *SyncedEnforcer) ClearPolicy() {
e.m.Lock()
defer e.m.Unlock()
e.Enforcer.ClearPolicy()
}
// LoadPolicy reloads the policy from file/database.
func (e *SyncedEnforcer) LoadPolicy() error {
e.m.RLock()
newModel, err := e.loadPolicyFromAdapter(e.model)
e.m.RUnlock()
if err != nil {
return err
}
e.m.Lock()
err = e.applyModifiedModel(newModel)
e.m.Unlock()
if err != nil {
return err
}
return nil
}
// LoadFilteredPolicy reloads a filtered policy from file/database.
func (e *SyncedEnforcer) LoadFilteredPolicy(filter interface{}) error {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.LoadFilteredPolicy(filter)
}
// LoadIncrementalFilteredPolicy reloads a filtered policy from file/database.
func (e *SyncedEnforcer) LoadIncrementalFilteredPolicy(filter interface{}) error {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.LoadIncrementalFilteredPolicy(filter)
}
// SavePolicy saves the current policy (usually after changed with Casbin API) back to file/database.
func (e *SyncedEnforcer) SavePolicy() error {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.SavePolicy()
}
// BuildRoleLinks manually rebuild the role inheritance relations.
func (e *SyncedEnforcer) BuildRoleLinks() error {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.BuildRoleLinks()
}
// Enforce decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (sub, obj, act).
func (e *SyncedEnforcer) Enforce(rvals ...interface{}) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.Enforce(rvals...)
}
// EnforceWithMatcher use a custom matcher to decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (matcher, sub, obj, act), use model matcher by default when matcher is "".
func (e *SyncedEnforcer) EnforceWithMatcher(matcher string, rvals ...interface{}) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.EnforceWithMatcher(matcher, rvals...)
}
// EnforceEx explain enforcement by informing matched rules.
func (e *SyncedEnforcer) EnforceEx(rvals ...interface{}) (bool, []string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.EnforceEx(rvals...)
}
// EnforceExWithMatcher use a custom matcher and explain enforcement by informing matched rules.
func (e *SyncedEnforcer) EnforceExWithMatcher(matcher string, rvals ...interface{}) (bool, []string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.EnforceExWithMatcher(matcher, rvals...)
}
// BatchEnforce enforce in batches.
func (e *SyncedEnforcer) BatchEnforce(requests [][]interface{}) ([]bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.BatchEnforce(requests)
}
// BatchEnforceWithMatcher enforce with matcher in batches.
func (e *SyncedEnforcer) BatchEnforceWithMatcher(matcher string, requests [][]interface{}) ([]bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.BatchEnforceWithMatcher(matcher, requests)
}
// GetAllSubjects gets the list of subjects that show up in the current policy.
func (e *SyncedEnforcer) GetAllSubjects() ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllSubjects()
}
// GetAllNamedSubjects gets the list of subjects that show up in the current named policy.
func (e *SyncedEnforcer) GetAllNamedSubjects(ptype string) ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllNamedSubjects(ptype)
}
// GetAllObjects gets the list of objects that show up in the current policy.
func (e *SyncedEnforcer) GetAllObjects() ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllObjects()
}
// GetAllNamedObjects gets the list of objects that show up in the current named policy.
func (e *SyncedEnforcer) GetAllNamedObjects(ptype string) ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllNamedObjects(ptype)
}
// GetAllActions gets the list of actions that show up in the current policy.
func (e *SyncedEnforcer) GetAllActions() ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllActions()
}
// GetAllNamedActions gets the list of actions that show up in the current named policy.
func (e *SyncedEnforcer) GetAllNamedActions(ptype string) ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllNamedActions(ptype)
}
// GetAllRoles gets the list of roles that show up in the current policy.
func (e *SyncedEnforcer) GetAllRoles() ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllRoles()
}
// GetAllNamedRoles gets the list of roles that show up in the current named policy.
func (e *SyncedEnforcer) GetAllNamedRoles(ptype string) ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllNamedRoles(ptype)
}
// GetAllUsers gets the list of users that show up in the current policy.
func (e *SyncedEnforcer) GetAllUsers() ([]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetAllUsers()
}
// GetPolicy gets all the authorization rules in the policy.
func (e *SyncedEnforcer) GetPolicy() ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetPolicy()
}
// GetFilteredPolicy gets all the authorization rules in the policy, field filters can be specified.
func (e *SyncedEnforcer) GetFilteredPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetFilteredPolicy(fieldIndex, fieldValues...)
}
// GetNamedPolicy gets all the authorization rules in the named policy.
func (e *SyncedEnforcer) GetNamedPolicy(ptype string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetNamedPolicy(ptype)
}
// GetFilteredNamedPolicy gets all the authorization rules in the named policy, field filters can be specified.
func (e *SyncedEnforcer) GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetFilteredNamedPolicy(ptype, fieldIndex, fieldValues...)
}
// GetGroupingPolicy gets all the role inheritance rules in the policy.
func (e *SyncedEnforcer) GetGroupingPolicy() ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetGroupingPolicy()
}
// GetFilteredGroupingPolicy gets all the role inheritance rules in the policy, field filters can be specified.
func (e *SyncedEnforcer) GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetFilteredGroupingPolicy(fieldIndex, fieldValues...)
}
// GetNamedGroupingPolicy gets all the role inheritance rules in the policy.
func (e *SyncedEnforcer) GetNamedGroupingPolicy(ptype string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetNamedGroupingPolicy(ptype)
}
// GetFilteredNamedGroupingPolicy gets all the role inheritance rules in the policy, field filters can be specified.
func (e *SyncedEnforcer) GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.GetFilteredNamedGroupingPolicy(ptype, fieldIndex, fieldValues...)
}
// HasPolicy determines whether an authorization rule exists.
func (e *SyncedEnforcer) HasPolicy(params ...interface{}) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.HasPolicy(params...)
}
// HasNamedPolicy determines whether a named authorization rule exists.
func (e *SyncedEnforcer) HasNamedPolicy(ptype string, params ...interface{}) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.HasNamedPolicy(ptype, params...)
}
// AddPolicy adds an authorization rule to the current policy.
// If the rule already exists, the function returns false and the rule will not be added.
// Otherwise the function returns true by adding the new rule.
func (e *SyncedEnforcer) AddPolicy(params ...interface{}) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddPolicy(params...)
}
// AddPolicies adds authorization rules to the current policy.
// If the rule already exists, the function returns false for the corresponding rule and the rule will not be added.
// Otherwise the function returns true for the corresponding rule by adding the new rule.
func (e *SyncedEnforcer) AddPolicies(rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddPolicies(rules)
}
// AddPoliciesEx adds authorization rules to the current policy.
// If the rule already exists, the rule will not be added.
// But unlike AddPolicies, other non-existent rules are added instead of returning false directly.
func (e *SyncedEnforcer) AddPoliciesEx(rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddPoliciesEx(rules)
}
// AddNamedPolicy adds an authorization rule to the current named policy.
// If the rule already exists, the function returns false and the rule will not be added.
// Otherwise the function returns true by adding the new rule.
func (e *SyncedEnforcer) AddNamedPolicy(ptype string, params ...interface{}) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddNamedPolicy(ptype, params...)
}
// AddNamedPolicies adds authorization rules to the current named policy.
// If the rule already exists, the function returns false for the corresponding rule and the rule will not be added.
// Otherwise the function returns true for the corresponding by adding the new rule.
func (e *SyncedEnforcer) AddNamedPolicies(ptype string, rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddNamedPolicies(ptype, rules)
}
// AddNamedPoliciesEx adds authorization rules to the current named policy.
// If the rule already exists, the rule will not be added.
// But unlike AddNamedPolicies, other non-existent rules are added instead of returning false directly.
func (e *SyncedEnforcer) AddNamedPoliciesEx(ptype string, rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddNamedPoliciesEx(ptype, rules)
}
// RemovePolicy removes an authorization rule from the current policy.
func (e *SyncedEnforcer) RemovePolicy(params ...interface{}) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemovePolicy(params...)
}
// UpdatePolicy updates an authorization rule from the current policy.
func (e *SyncedEnforcer) UpdatePolicy(oldPolicy []string, newPolicy []string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.UpdatePolicy(oldPolicy, newPolicy)
}
func (e *SyncedEnforcer) UpdateNamedPolicy(ptype string, p1 []string, p2 []string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.UpdateNamedPolicy(ptype, p1, p2)
}
// UpdatePolicies updates authorization rules from the current policies.
func (e *SyncedEnforcer) UpdatePolicies(oldPolices [][]string, newPolicies [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.UpdatePolicies(oldPolices, newPolicies)
}
func (e *SyncedEnforcer) UpdateNamedPolicies(ptype string, p1 [][]string, p2 [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.UpdateNamedPolicies(ptype, p1, p2)
}
func (e *SyncedEnforcer) UpdateFilteredPolicies(newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.UpdateFilteredPolicies(newPolicies, fieldIndex, fieldValues...)
}
func (e *SyncedEnforcer) UpdateFilteredNamedPolicies(ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.UpdateFilteredNamedPolicies(ptype, newPolicies, fieldIndex, fieldValues...)
}
// RemovePolicies removes authorization rules from the current policy.
func (e *SyncedEnforcer) RemovePolicies(rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemovePolicies(rules)
}
// RemoveFilteredPolicy removes an authorization rule from the current policy, field filters can be specified.
func (e *SyncedEnforcer) RemoveFilteredPolicy(fieldIndex int, fieldValues ...string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemoveFilteredPolicy(fieldIndex, fieldValues...)
}
// RemoveNamedPolicy removes an authorization rule from the current named policy.
func (e *SyncedEnforcer) RemoveNamedPolicy(ptype string, params ...interface{}) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemoveNamedPolicy(ptype, params...)
}
// RemoveNamedPolicies removes authorization rules from the current named policy.
func (e *SyncedEnforcer) RemoveNamedPolicies(ptype string, rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemoveNamedPolicies(ptype, rules)
}
// RemoveFilteredNamedPolicy removes an authorization rule from the current named policy, field filters can be specified.
func (e *SyncedEnforcer) RemoveFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemoveFilteredNamedPolicy(ptype, fieldIndex, fieldValues...)
}
// HasGroupingPolicy determines whether a role inheritance rule exists.
func (e *SyncedEnforcer) HasGroupingPolicy(params ...interface{}) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.HasGroupingPolicy(params...)
}
// HasNamedGroupingPolicy determines whether a named role inheritance rule exists.
func (e *SyncedEnforcer) HasNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) {
e.m.RLock()
defer e.m.RUnlock()
return e.Enforcer.HasNamedGroupingPolicy(ptype, params...)
}
// AddGroupingPolicy adds a role inheritance rule to the current policy.
// If the rule already exists, the function returns false and the rule will not be added.
// Otherwise the function returns true by adding the new rule.
func (e *SyncedEnforcer) AddGroupingPolicy(params ...interface{}) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddGroupingPolicy(params...)
}
// AddGroupingPolicies adds role inheritance rulea to the current policy.
// If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added.
// Otherwise the function returns true for the corresponding policy rule by adding the new rule.
func (e *SyncedEnforcer) AddGroupingPolicies(rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddGroupingPolicies(rules)
}
// AddGroupingPoliciesEx adds role inheritance rules to the current policy.
// If the rule already exists, the rule will not be added.
// But unlike AddGroupingPolicies, other non-existent rules are added instead of returning false directly.
func (e *SyncedEnforcer) AddGroupingPoliciesEx(rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddGroupingPoliciesEx(rules)
}
// AddNamedGroupingPolicy adds a named role inheritance rule to the current policy.
// If the rule already exists, the function returns false and the rule will not be added.
// Otherwise the function returns true by adding the new rule.
func (e *SyncedEnforcer) AddNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddNamedGroupingPolicy(ptype, params...)
}
// AddNamedGroupingPolicies adds named role inheritance rules to the current policy.
// If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added.
// Otherwise the function returns true for the corresponding policy rule by adding the new rule.
func (e *SyncedEnforcer) AddNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddNamedGroupingPolicies(ptype, rules)
}
// AddNamedGroupingPoliciesEx adds named role inheritance rules to the current policy.
// If the rule already exists, the rule will not be added.
// But unlike AddNamedGroupingPolicies, other non-existent rules are added instead of returning false directly.
func (e *SyncedEnforcer) AddNamedGroupingPoliciesEx(ptype string, rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.AddNamedGroupingPoliciesEx(ptype, rules)
}
// RemoveGroupingPolicy removes a role inheritance rule from the current policy.
func (e *SyncedEnforcer) RemoveGroupingPolicy(params ...interface{}) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemoveGroupingPolicy(params...)
}
// RemoveGroupingPolicies removes role inheritance rules from the current policy.
func (e *SyncedEnforcer) RemoveGroupingPolicies(rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemoveGroupingPolicies(rules)
}
// RemoveFilteredGroupingPolicy removes a role inheritance rule from the current policy, field filters can be specified.
func (e *SyncedEnforcer) RemoveFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemoveFilteredGroupingPolicy(fieldIndex, fieldValues...)
}
// RemoveNamedGroupingPolicy removes a role inheritance rule from the current named policy.
func (e *SyncedEnforcer) RemoveNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemoveNamedGroupingPolicy(ptype, params...)
}
// RemoveNamedGroupingPolicies removes role inheritance rules from the current named policy.
func (e *SyncedEnforcer) RemoveNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemoveNamedGroupingPolicies(ptype, rules)
}
func (e *SyncedEnforcer) UpdateGroupingPolicy(oldRule []string, newRule []string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.UpdateGroupingPolicy(oldRule, newRule)
}
func (e *SyncedEnforcer) UpdateGroupingPolicies(oldRules [][]string, newRules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.UpdateGroupingPolicies(oldRules, newRules)
}
func (e *SyncedEnforcer) UpdateNamedGroupingPolicy(ptype string, oldRule []string, newRule []string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.UpdateNamedGroupingPolicy(ptype, oldRule, newRule)
}
func (e *SyncedEnforcer) UpdateNamedGroupingPolicies(ptype string, oldRules [][]string, newRules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.UpdateNamedGroupingPolicies(ptype, oldRules, newRules)
}
// RemoveFilteredNamedGroupingPolicy removes a role inheritance rule from the current named policy, field filters can be specified.
func (e *SyncedEnforcer) RemoveFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.RemoveFilteredNamedGroupingPolicy(ptype, fieldIndex, fieldValues...)
}
// AddFunction adds a customized function.
func (e *SyncedEnforcer) AddFunction(name string, function govaluate.ExpressionFunction) {
e.m.Lock()
defer e.m.Unlock()
e.Enforcer.AddFunction(name, function)
}
func (e *SyncedEnforcer) SelfAddPolicy(sec string, ptype string, rule []string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.SelfAddPolicy(sec, ptype, rule)
}
func (e *SyncedEnforcer) SelfAddPolicies(sec string, ptype string, rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.SelfAddPolicies(sec, ptype, rules)
}
func (e *SyncedEnforcer) SelfAddPoliciesEx(sec string, ptype string, rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.SelfAddPoliciesEx(sec, ptype, rules)
}
func (e *SyncedEnforcer) SelfRemovePolicy(sec string, ptype string, rule []string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.SelfRemovePolicy(sec, ptype, rule)
}
func (e *SyncedEnforcer) SelfRemovePolicies(sec string, ptype string, rules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.SelfRemovePolicies(sec, ptype, rules)
}
func (e *SyncedEnforcer) SelfRemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.SelfRemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
}
func (e *SyncedEnforcer) SelfUpdatePolicy(sec string, ptype string, oldRule, newRule []string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.SelfUpdatePolicy(sec, ptype, oldRule, newRule)
}
func (e *SyncedEnforcer) SelfUpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.SelfUpdatePolicies(sec, ptype, oldRules, newRules)
}

537
enforcer_synced_test.go Normal file
View File

@@ -0,0 +1,537 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"sort"
"testing"
"time"
"github.com/casbin/casbin/v3/errors"
"github.com/casbin/casbin/v3/util"
)
func testEnforceSync(t *testing.T, e *SyncedEnforcer, sub string, obj interface{}, act string, res bool) {
t.Helper()
if myRes, _ := e.Enforce(sub, obj, act); myRes != res {
t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, myRes, res)
}
}
func TestSync(t *testing.T) {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
// Start reloading the policy every 200 ms.
e.StartAutoLoadPolicy(time.Millisecond * 200)
testEnforceSync(t, e, "alice", "data1", "read", true)
testEnforceSync(t, e, "alice", "data1", "write", false)
testEnforceSync(t, e, "alice", "data2", "read", false)
testEnforceSync(t, e, "alice", "data2", "write", false)
testEnforceSync(t, e, "bob", "data1", "read", false)
testEnforceSync(t, e, "bob", "data1", "write", false)
testEnforceSync(t, e, "bob", "data2", "read", false)
testEnforceSync(t, e, "bob", "data2", "write", true)
// Simulate a policy change
e.ClearPolicy()
testEnforceSync(t, e, "bob", "data2", "write", false)
// Wait for at least one sync
time.Sleep(time.Millisecond * 300)
testEnforceSync(t, e, "bob", "data2", "write", true)
// Stop the reloading policy periodically.
e.StopAutoLoadPolicy()
}
func TestStopAutoLoadPolicy(t *testing.T) {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
e.StartAutoLoadPolicy(5 * time.Millisecond)
if !e.IsAutoLoadingRunning() {
t.Error("auto load is not running")
}
e.StopAutoLoadPolicy()
// Need a moment, to exit goroutine
time.Sleep(10 * time.Millisecond)
if e.IsAutoLoadingRunning() {
t.Error("auto load is still running")
}
}
func testSyncedEnforcerGetPolicy(t *testing.T, e *SyncedEnforcer, res [][]string) {
t.Helper()
myRes, err := e.GetPolicy()
if err != nil {
t.Error(err)
}
if !util.SortedArray2DEquals(res, myRes) {
t.Error("Policy: ", myRes, ", supposed to be ", res)
} else {
t.Log("Policy: ", myRes)
}
}
func TestSyncedEnforcerSelfAddPolicy(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "read"},
{"user2", "data2", "read"},
{"user3", "data3", "read"},
{"user4", "data4", "read"},
{"user5", "data5", "read"},
{"user6", "data6", "read"},
})
}
}
func TestSyncedEnforcerSelfAddPolicies(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
go func() {
_, _ = e.SelfAddPolicies("p", "p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}})
}()
go func() {
_, _ = e.SelfAddPolicies("p", "p", [][]string{{"user3", "data3", "read"}, {"user4", "data4", "read"}})
}()
go func() {
_, _ = e.SelfAddPolicies("p", "p", [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}})
}()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "read"},
{"user2", "data2", "read"},
{"user3", "data3", "read"},
{"user4", "data4", "read"},
{"user5", "data5", "read"},
{"user6", "data6", "read"},
})
}
}
func TestSyncedEnforcerSelfAddPoliciesEx(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
go func() {
_, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}})
}()
go func() {
_, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user2", "data2", "read"}, {"user3", "data3", "read"}})
}()
go func() {
_, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user3", "data3", "read"}, {"user4", "data4", "read"}})
}()
go func() {
_, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user4", "data4", "read"}, {"user5", "data5", "read"}})
}()
go func() {
_, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}})
}()
go func() {
_, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user6", "data6", "read"}, {"user1", "data1", "read"}})
}()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "read"},
{"user2", "data2", "read"},
{"user3", "data3", "read"},
{"user4", "data4", "read"},
{"user5", "data5", "read"},
{"user6", "data6", "read"},
})
}
}
func TestSyncedEnforcerSelfRemovePolicy(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "read"},
{"user2", "data2", "read"},
{"user3", "data3", "read"},
{"user4", "data4", "read"},
{"user5", "data5", "read"},
{"user6", "data6", "read"},
})
go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user1", "data1", "read"}) }()
go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user2", "data2", "read"}) }()
go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user3", "data3", "read"}) }()
go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user4", "data4", "read"}) }()
go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user5", "data5", "read"}) }()
go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user6", "data6", "read"}) }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
})
}
}
func TestSyncedEnforcerSelfRemovePolicies(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "read"},
{"user2", "data2", "read"},
{"user3", "data3", "read"},
{"user4", "data4", "read"},
{"user5", "data5", "read"},
{"user6", "data6", "read"},
})
go func() {
_, _ = e.SelfRemovePolicies("p", "p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}})
}()
go func() {
_, _ = e.SelfRemovePolicies("p", "p", [][]string{{"user3", "data3", "read"}, {"user4", "data4", "read"}})
}()
go func() {
_, _ = e.SelfRemovePolicies("p", "p", [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}})
}()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
})
}
}
func TestSyncedEnforcerSelfRemoveFilteredPolicy(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user7", "data7", "write"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user8", "data8", "write"}) }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "read"},
{"user2", "data2", "read"},
{"user3", "data3", "read"},
{"user4", "data4", "read"},
{"user5", "data5", "read"},
{"user6", "data6", "read"},
{"user7", "data7", "write"},
{"user8", "data8", "write"},
})
go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 0, "user1") }()
go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 0, "user2") }()
go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 1, "data3") }()
go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 1, "data4") }()
go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 0, "user5") }()
go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 0, "user6") }()
go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 2, "write") }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
})
}
}
func TestSyncedEnforcerSelfUpdatePolicy(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "read"},
{"user2", "data2", "read"},
{"user3", "data3", "read"},
{"user4", "data4", "read"},
{"user5", "data5", "read"},
{"user6", "data6", "read"},
})
go func() {
_, _ = e.SelfUpdatePolicy("p", "p", []string{"user1", "data1", "read"}, []string{"user1", "data1", "write"})
}()
go func() {
_, _ = e.SelfUpdatePolicy("p", "p", []string{"user2", "data2", "read"}, []string{"user2", "data2", "write"})
}()
go func() {
_, _ = e.SelfUpdatePolicy("p", "p", []string{"user3", "data3", "read"}, []string{"user3", "data3", "write"})
}()
go func() {
_, _ = e.SelfUpdatePolicy("p", "p", []string{"user4", "data4", "read"}, []string{"user4", "data4", "write"})
}()
go func() {
_, _ = e.SelfUpdatePolicy("p", "p", []string{"user5", "data5", "read"}, []string{"user5", "data5", "write"})
}()
go func() {
_, _ = e.SelfUpdatePolicy("p", "p", []string{"user6", "data6", "read"}, []string{"user6", "data6", "write"})
}()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "write"},
{"user2", "data2", "write"},
{"user3", "data3", "write"},
{"user4", "data4", "write"},
{"user5", "data5", "write"},
{"user6", "data6", "write"},
})
}
}
func TestSyncedEnforcerSelfUpdatePolicies(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }()
go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "read"},
{"user2", "data2", "read"},
{"user3", "data3", "read"},
{"user4", "data4", "read"},
{"user5", "data5", "read"},
{"user6", "data6", "read"},
})
go func() {
_, _ = e.SelfUpdatePolicies("p", "p",
[][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}},
[][]string{{"user1", "data1", "write"}, {"user2", "data2", "write"}})
}()
go func() {
_, _ = e.SelfUpdatePolicies("p", "p",
[][]string{{"user3", "data3", "read"}, {"user4", "data4", "read"}},
[][]string{{"user3", "data3", "write"}, {"user4", "data4", "write"}})
}()
go func() {
_, _ = e.SelfUpdatePolicies("p", "p",
[][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}},
[][]string{{"user5", "data5", "write"}, {"user6", "data6", "write"}})
}()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "write"},
{"user2", "data2", "write"},
{"user3", "data3", "write"},
{"user4", "data4", "write"},
{"user5", "data5", "write"},
{"user6", "data6", "write"},
})
}
}
func TestSyncedEnforcerAddPoliciesEx(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
go func() { _, _ = e.AddPoliciesEx([][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) }()
go func() { _, _ = e.AddPoliciesEx([][]string{{"user2", "data2", "read"}, {"user3", "data3", "read"}}) }()
go func() { _, _ = e.AddPoliciesEx([][]string{{"user4", "data4", "read"}, {"user5", "data5", "read"}}) }()
go func() { _, _ = e.AddPoliciesEx([][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}}) }()
go func() { _, _ = e.AddPoliciesEx([][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) }()
go func() { _, _ = e.AddPoliciesEx([][]string{{"user2", "data2", "read"}, {"user3", "data3", "read"}}) }()
go func() { _, _ = e.AddPoliciesEx([][]string{{"user4", "data4", "read"}, {"user5", "data5", "read"}}) }()
go func() { _, _ = e.AddPoliciesEx([][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}}) }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "read"},
{"user2", "data2", "read"},
{"user3", "data3", "read"},
{"user4", "data4", "read"},
{"user5", "data5", "read"},
{"user6", "data6", "read"},
})
}
}
func TestSyncedEnforcerAddNamedPoliciesEx(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
go func() {
_, _ = e.AddNamedPoliciesEx("p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}})
}()
go func() {
_, _ = e.AddNamedPoliciesEx("p", [][]string{{"user2", "data2", "read"}, {"user3", "data3", "read"}})
}()
go func() {
_, _ = e.AddNamedPoliciesEx("p", [][]string{{"user4", "data4", "read"}, {"user5", "data5", "read"}})
}()
go func() {
_, _ = e.AddNamedPoliciesEx("p", [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}})
}()
go func() {
_, _ = e.AddNamedPoliciesEx("p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}})
}()
go func() {
_, _ = e.AddNamedPoliciesEx("p", [][]string{{"user2", "data2", "read"}, {"user3", "data3", "read"}})
}()
go func() {
_, _ = e.AddNamedPoliciesEx("p", [][]string{{"user4", "data4", "read"}, {"user5", "data5", "read"}})
}()
go func() {
_, _ = e.AddNamedPoliciesEx("p", [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}})
}()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"user1", "data1", "read"},
{"user2", "data2", "read"},
{"user3", "data3", "read"},
{"user4", "data4", "read"},
{"user5", "data5", "read"},
{"user6", "data6", "read"},
})
}
}
func testSyncedEnforcerGetUsers(t *testing.T, e *SyncedEnforcer, res []string, name string, domain ...string) {
t.Helper()
myRes, err := e.GetUsersForRole(name, domain...)
myResCopy := make([]string, len(myRes))
copy(myResCopy, myRes)
sort.Strings(myRes)
sort.Strings(res)
switch err {
case nil:
break
case errors.ErrNameNotFound:
t.Log("No name found")
default:
t.Error("Users for ", name, " could not be fetched: ", err.Error())
}
t.Log("Users for ", name, ": ", myRes)
if !util.SetEquals(res, myRes) {
t.Error("Users for ", name, ": ", myRes, ", supposed to be ", res)
}
}
func TestSyncedEnforcerAddGroupingPoliciesEx(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
e.ClearPolicy()
go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user1", "member"}, {"user2", "member"}}) }()
go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user2", "member"}, {"user3", "member"}}) }()
go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user4", "member"}, {"user5", "member"}}) }()
go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user5", "member"}, {"user6", "member"}}) }()
go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user1", "member"}, {"user2", "member"}}) }()
go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user2", "member"}, {"user3", "member"}}) }()
go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user4", "member"}, {"user5", "member"}}) }()
go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user5", "member"}, {"user6", "member"}}) }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetUsers(t, e, []string{"user1", "user2", "user3", "user4", "user5", "user6"}, "member")
}
}
func TestSyncedEnforcerAddNamedGroupingPoliciesEx(t *testing.T) {
for i := 0; i < 10; i++ {
e, _ := NewSyncedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
e.ClearPolicy()
go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user1", "member"}, {"user2", "member"}}) }()
go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user2", "member"}, {"user3", "member"}}) }()
go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user4", "member"}, {"user5", "member"}}) }()
go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user5", "member"}, {"user6", "member"}}) }()
go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user1", "member"}, {"user2", "member"}}) }()
go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user2", "member"}, {"user3", "member"}}) }()
go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user4", "member"}, {"user5", "member"}}) }()
go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user5", "member"}, {"user6", "member"}}) }()
time.Sleep(100 * time.Millisecond)
testSyncedEnforcerGetUsers(t, e, []string{"user1", "user2", "user3", "user4", "user5", "user6"}, "member")
}
}

806
enforcer_test.go Normal file
View File

@@ -0,0 +1,806 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"strings"
"sync"
"testing"
"github.com/casbin/casbin/v3/detector"
"github.com/casbin/casbin/v3/model"
fileadapter "github.com/casbin/casbin/v3/persist/file-adapter"
"github.com/casbin/casbin/v3/util"
)
func TestKeyMatchModelInMemory(t *testing.T) {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)")
a := fileadapter.NewAdapter("examples/keymatch_policy.csv")
e, _ := NewEnforcer(m, a)
testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true)
testEnforce(t, e, "alice", "/alice_data/resource1", "POST", true)
testEnforce(t, e, "alice", "/alice_data/resource2", "GET", true)
testEnforce(t, e, "alice", "/alice_data/resource2", "POST", false)
testEnforce(t, e, "alice", "/bob_data/resource1", "GET", false)
testEnforce(t, e, "alice", "/bob_data/resource1", "POST", false)
testEnforce(t, e, "alice", "/bob_data/resource2", "GET", false)
testEnforce(t, e, "alice", "/bob_data/resource2", "POST", false)
testEnforce(t, e, "bob", "/alice_data/resource1", "GET", false)
testEnforce(t, e, "bob", "/alice_data/resource1", "POST", false)
testEnforce(t, e, "bob", "/alice_data/resource2", "GET", true)
testEnforce(t, e, "bob", "/alice_data/resource2", "POST", false)
testEnforce(t, e, "bob", "/bob_data/resource1", "GET", false)
testEnforce(t, e, "bob", "/bob_data/resource1", "POST", true)
testEnforce(t, e, "bob", "/bob_data/resource2", "GET", false)
testEnforce(t, e, "bob", "/bob_data/resource2", "POST", true)
testEnforce(t, e, "cathy", "/cathy_data", "GET", true)
testEnforce(t, e, "cathy", "/cathy_data", "POST", true)
testEnforce(t, e, "cathy", "/cathy_data", "DELETE", false)
e, _ = NewEnforcer(m)
_ = a.LoadPolicy(e.GetModel())
testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true)
testEnforce(t, e, "alice", "/alice_data/resource1", "POST", true)
testEnforce(t, e, "alice", "/alice_data/resource2", "GET", true)
testEnforce(t, e, "alice", "/alice_data/resource2", "POST", false)
testEnforce(t, e, "alice", "/bob_data/resource1", "GET", false)
testEnforce(t, e, "alice", "/bob_data/resource1", "POST", false)
testEnforce(t, e, "alice", "/bob_data/resource2", "GET", false)
testEnforce(t, e, "alice", "/bob_data/resource2", "POST", false)
testEnforce(t, e, "bob", "/alice_data/resource1", "GET", false)
testEnforce(t, e, "bob", "/alice_data/resource1", "POST", false)
testEnforce(t, e, "bob", "/alice_data/resource2", "GET", true)
testEnforce(t, e, "bob", "/alice_data/resource2", "POST", false)
testEnforce(t, e, "bob", "/bob_data/resource1", "GET", false)
testEnforce(t, e, "bob", "/bob_data/resource1", "POST", true)
testEnforce(t, e, "bob", "/bob_data/resource2", "GET", false)
testEnforce(t, e, "bob", "/bob_data/resource2", "POST", true)
testEnforce(t, e, "cathy", "/cathy_data", "GET", true)
testEnforce(t, e, "cathy", "/cathy_data", "POST", true)
testEnforce(t, e, "cathy", "/cathy_data", "DELETE", false)
}
func TestKeyMatchWithRBACInDomain(t *testing.T) {
e, _ := NewEnforcer("examples/keymatch_with_rbac_in_domain.conf", "examples/keymatch_with_rbac_in_domain.csv")
testDomainEnforce(t, e, "Username==test2", "engines/engine1", "*", "attach", true)
}
func TestKeyMatchModelInMemoryDeny(t *testing.T) {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("e", "e", "!some(where (p.eft == deny))")
m.AddDef("m", "m", "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)")
a := fileadapter.NewAdapter("examples/keymatch_policy.csv")
e, _ := NewEnforcer(m, a)
testEnforce(t, e, "alice", "/alice_data/resource2", "POST", true)
}
func TestRBACModelInMemoryIndeterminate(t *testing.T) {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("g", "g", "_, _")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act")
e, _ := NewEnforcer(m)
_, _ = e.AddPermissionForUser("alice", "data1", "invalid")
testEnforce(t, e, "alice", "data1", "read", false)
}
func TestRBACModelInMemory(t *testing.T) {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("g", "g", "_, _")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act")
e, _ := NewEnforcer(m)
_, _ = e.AddPermissionForUser("alice", "data1", "read")
_, _ = e.AddPermissionForUser("bob", "data2", "write")
_, _ = e.AddPermissionForUser("data2_admin", "data2", "read")
_, _ = e.AddPermissionForUser("data2_admin", "data2", "write")
_, _ = e.AddRoleForUser("alice", "data2_admin")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "alice", "data2", "read", true)
testEnforce(t, e, "alice", "data2", "write", true)
testEnforce(t, e, "bob", "data1", "read", false)
testEnforce(t, e, "bob", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "read", false)
testEnforce(t, e, "bob", "data2", "write", true)
}
func TestRBACModelInMemory2(t *testing.T) {
text := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`
m, _ := model.NewModelFromString(text)
// The above is the same as:
// m := NewModel()
// m.LoadModelFromText(text)
e, _ := NewEnforcer(m)
_, _ = e.AddPermissionForUser("alice", "data1", "read")
_, _ = e.AddPermissionForUser("bob", "data2", "write")
_, _ = e.AddPermissionForUser("data2_admin", "data2", "read")
_, _ = e.AddPermissionForUser("data2_admin", "data2", "write")
_, _ = e.AddRoleForUser("alice", "data2_admin")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "alice", "data2", "read", true)
testEnforce(t, e, "alice", "data2", "write", true)
testEnforce(t, e, "bob", "data1", "read", false)
testEnforce(t, e, "bob", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "read", false)
testEnforce(t, e, "bob", "data2", "write", true)
}
func TestNotUsedRBACModelInMemory(t *testing.T) {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("g", "g", "_, _")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act")
e, _ := NewEnforcer(m)
_, _ = e.AddPermissionForUser("alice", "data1", "read")
_, _ = e.AddPermissionForUser("bob", "data2", "write")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "alice", "data2", "read", false)
testEnforce(t, e, "alice", "data2", "write", false)
testEnforce(t, e, "bob", "data1", "read", false)
testEnforce(t, e, "bob", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "read", false)
testEnforce(t, e, "bob", "data2", "write", true)
}
func TestMatcherUsingInOperator(t *testing.T) {
// From file config
e, _ := NewEnforcer("examples/rbac_model_matcher_using_in_op.conf")
_, _ = e.AddPermissionForUser("alice", "data1", "read")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data2", "read", true)
testEnforce(t, e, "alice", "data3", "read", true)
testEnforce(t, e, "anyone", "data1", "read", false)
testEnforce(t, e, "anyone", "data2", "read", true)
testEnforce(t, e, "anyone", "data3", "read", true)
}
func TestMatcherUsingInOperatorBracket(t *testing.T) {
e, _ := NewEnforcer("examples/rbac_model_matcher_using_in_op_bracket.conf")
_, _ = e.AddPermissionForUser("alice", "data1", "read")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data2", "read", true)
testEnforce(t, e, "alice", "data3", "read", true)
testEnforce(t, e, "anyone", "data1", "read", false)
testEnforce(t, e, "anyone", "data2", "read", true)
testEnforce(t, e, "anyone", "data3", "read", true)
}
func TestReloadPolicy(t *testing.T) {
e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
_ = e.LoadPolicy()
testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}})
}
func TestSavePolicy(t *testing.T) {
e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
_ = e.SavePolicy()
}
func TestClearPolicy(t *testing.T) {
e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
e.ClearPolicy()
}
func TestEnableEnforce(t *testing.T) {
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
e.EnableEnforce(false)
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", true)
testEnforce(t, e, "alice", "data2", "read", true)
testEnforce(t, e, "alice", "data2", "write", true)
testEnforce(t, e, "bob", "data1", "read", true)
testEnforce(t, e, "bob", "data1", "write", true)
testEnforce(t, e, "bob", "data2", "read", true)
testEnforce(t, e, "bob", "data2", "write", true)
e.EnableEnforce(true)
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "alice", "data2", "read", false)
testEnforce(t, e, "alice", "data2", "write", false)
testEnforce(t, e, "bob", "data1", "read", false)
testEnforce(t, e, "bob", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "read", false)
testEnforce(t, e, "bob", "data2", "write", true)
}
func TestEnableLog(t *testing.T) {
// This test is now a no-op since the logger has been removed
// Keeping it for backward compatibility, but it just tests enforcement
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "alice", "data2", "read", false)
testEnforce(t, e, "alice", "data2", "write", false)
testEnforce(t, e, "bob", "data1", "read", false)
testEnforce(t, e, "bob", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "read", false)
testEnforce(t, e, "bob", "data2", "write", true)
}
func TestEnableAutoSave(t *testing.T) {
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
e.EnableAutoSave(false)
// Because AutoSave is disabled, the policy change only affects the policy in Casbin enforcer,
// it doesn't affect the policy in the storage.
_, _ = e.RemovePolicy("alice", "data1", "read")
// Reload the policy from the storage to see the effect.
_ = e.LoadPolicy()
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "alice", "data2", "read", false)
testEnforce(t, e, "alice", "data2", "write", false)
testEnforce(t, e, "bob", "data1", "read", false)
testEnforce(t, e, "bob", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "read", false)
testEnforce(t, e, "bob", "data2", "write", true)
e.EnableAutoSave(true)
// Because AutoSave is enabled, the policy change not only affects the policy in Casbin enforcer,
// but also affects the policy in the storage.
_, _ = e.RemovePolicy("alice", "data1", "read")
// However, the file adapter doesn't implement the AutoSave feature, so enabling it has no effect at all here.
// Reload the policy from the storage to see the effect.
_ = e.LoadPolicy()
testEnforce(t, e, "alice", "data1", "read", true) // Will not be false here.
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "alice", "data2", "read", false)
testEnforce(t, e, "alice", "data2", "write", false)
testEnforce(t, e, "bob", "data1", "read", false)
testEnforce(t, e, "bob", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "read", false)
testEnforce(t, e, "bob", "data2", "write", true)
}
func TestInitWithAdapter(t *testing.T) {
adapter := fileadapter.NewAdapter("examples/basic_policy.csv")
e, _ := NewEnforcer("examples/basic_model.conf", adapter)
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "alice", "data2", "read", false)
testEnforce(t, e, "alice", "data2", "write", false)
testEnforce(t, e, "bob", "data1", "read", false)
testEnforce(t, e, "bob", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "read", false)
testEnforce(t, e, "bob", "data2", "write", true)
}
func TestRoleLinks(t *testing.T) {
e, _ := NewEnforcer("examples/rbac_model.conf")
e.EnableAutoBuildRoleLinks(false)
_ = e.BuildRoleLinks()
_, _ = e.Enforce("user501", "data9", "read")
}
func TestEnforceConcurrency(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Enforce is not concurrent")
}
}()
e, _ := NewEnforcer("examples/rbac_model.conf")
_ = e.LoadModel()
var wg sync.WaitGroup
// Simulate concurrency (maybe use a timer?)
for i := 1; i <= 10000; i++ {
wg.Add(1)
go func() {
_, _ = e.Enforce("user501", "data9", "read")
wg.Done()
}()
}
wg.Wait()
}
func TestGetAndSetModel(t *testing.T) {
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
e2, _ := NewEnforcer("examples/basic_with_root_model.conf", "examples/basic_policy.csv")
testEnforce(t, e, "root", "data1", "read", false)
e.SetModel(e2.GetModel())
testEnforce(t, e, "root", "data1", "read", true)
}
func TestGetAndSetAdapterInMem(t *testing.T) {
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
e2, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_inverse_policy.csv")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
a2 := e2.GetAdapter()
e.SetAdapter(a2)
_ = e.LoadPolicy()
testEnforce(t, e, "alice", "data1", "read", false)
testEnforce(t, e, "alice", "data1", "write", true)
}
func TestSetAdapterFromFile(t *testing.T) {
e, _ := NewEnforcer("examples/basic_model.conf")
testEnforce(t, e, "alice", "data1", "read", false)
a := fileadapter.NewAdapter("examples/basic_policy.csv")
e.SetAdapter(a)
_ = e.LoadPolicy()
testEnforce(t, e, "alice", "data1", "read", true)
}
func TestInitEmpty(t *testing.T) {
e, _ := NewEnforcer()
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)")
a := fileadapter.NewAdapter("examples/keymatch_policy.csv")
e.SetModel(m)
e.SetAdapter(a)
_ = e.LoadPolicy()
testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true)
}
func testEnforceEx(t *testing.T, e *Enforcer, sub, obj, act interface{}, res []string) {
t.Helper()
_, myRes, _ := e.EnforceEx(sub, obj, act)
if ok := util.ArrayEquals(res, myRes); !ok {
t.Error("Key: ", myRes, ", supposed to be ", res)
}
}
func TestEnforceEx(t *testing.T) {
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
testEnforceEx(t, e, "alice", "data1", "read", []string{"alice", "data1", "read"})
testEnforceEx(t, e, "alice", "data1", "write", []string{})
testEnforceEx(t, e, "alice", "data2", "read", []string{})
testEnforceEx(t, e, "alice", "data2", "write", []string{})
testEnforceEx(t, e, "bob", "data1", "read", []string{})
testEnforceEx(t, e, "bob", "data1", "write", []string{})
testEnforceEx(t, e, "bob", "data2", "read", []string{})
testEnforceEx(t, e, "bob", "data2", "write", []string{"bob", "data2", "write"})
e, _ = NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
testEnforceEx(t, e, "alice", "data1", "read", []string{"alice", "data1", "read"})
testEnforceEx(t, e, "alice", "data1", "write", []string{})
testEnforceEx(t, e, "alice", "data2", "read", []string{"data2_admin", "data2", "read"})
testEnforceEx(t, e, "alice", "data2", "write", []string{"data2_admin", "data2", "write"})
testEnforceEx(t, e, "bob", "data1", "read", []string{})
testEnforceEx(t, e, "bob", "data1", "write", []string{})
testEnforceEx(t, e, "bob", "data2", "read", []string{})
testEnforceEx(t, e, "bob", "data2", "write", []string{"bob", "data2", "write"})
e, _ = NewEnforcer("examples/priority_model.conf", "examples/priority_policy.csv")
testEnforceEx(t, e, "alice", "data1", "read", []string{"alice", "data1", "read", "allow"})
testEnforceEx(t, e, "alice", "data1", "write", []string{"data1_deny_group", "data1", "write", "deny"})
testEnforceEx(t, e, "alice", "data2", "read", []string{})
testEnforceEx(t, e, "alice", "data2", "write", []string{})
testEnforceEx(t, e, "bob", "data1", "write", []string{})
testEnforceEx(t, e, "bob", "data2", "read", []string{"data2_allow_group", "data2", "read", "allow"})
testEnforceEx(t, e, "bob", "data2", "write", []string{"bob", "data2", "write", "deny"})
e, _ = NewEnforcer("examples/abac_model.conf")
obj := struct{ Owner string }{Owner: "alice"}
testEnforceEx(t, e, "alice", obj, "write", []string{})
}
func TestEnforceExLog(t *testing.T) {
// This test was previously named for logging, but actually tests EnforceEx explain functionality
// Logger parameter has been removed, but the test still validates explain behavior
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
testEnforceEx(t, e, "alice", "data1", "read", []string{"alice", "data1", "read"})
testEnforceEx(t, e, "alice", "data1", "write", []string{})
testEnforceEx(t, e, "alice", "data2", "read", []string{})
testEnforceEx(t, e, "alice", "data2", "write", []string{})
testEnforceEx(t, e, "bob", "data1", "read", []string{})
testEnforceEx(t, e, "bob", "data1", "write", []string{})
testEnforceEx(t, e, "bob", "data2", "read", []string{})
testEnforceEx(t, e, "bob", "data2", "write", []string{"bob", "data2", "write"})
}
func testBatchEnforce(t *testing.T, e *Enforcer, requests [][]interface{}, results []bool) {
t.Helper()
myRes, _ := e.BatchEnforce(requests)
if len(myRes) != len(results) {
t.Errorf("%v supposed to be %v", myRes, results)
}
for i, v := range myRes {
if v != results[i] {
t.Errorf("%v supposed to be %v", myRes, results)
}
}
}
func TestBatchEnforce(t *testing.T) {
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
results := []bool{true, true, false}
testBatchEnforce(t, e, [][]interface{}{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"jack", "data3", "read"}}, results)
}
func TestSubjectPriority(t *testing.T) {
e, _ := NewEnforcer("examples/subject_priority_model.conf", "examples/subject_priority_policy.csv")
testBatchEnforce(t, e, [][]interface{}{
{"jane", "data1", "read"},
{"alice", "data1", "read"},
}, []bool{
true, true,
})
}
func TestSubjectPriorityWithDomain(t *testing.T) {
e, _ := NewEnforcer("examples/subject_priority_model_with_domain.conf", "examples/subject_priority_policy_with_domain.csv")
testBatchEnforce(t, e, [][]interface{}{
{"alice", "data1", "domain1", "write"},
{"bob", "data2", "domain2", "write"},
}, []bool{
true, true,
})
}
func TestSubjectPriorityInFilter(t *testing.T) {
e, _ := NewEnforcer()
adapter := fileadapter.NewFilteredAdapter("examples/subject_priority_policy_with_domain.csv")
_ = e.InitWithAdapter("examples/subject_priority_model_with_domain.conf", adapter)
if err := e.loadFilteredPolicy(&fileadapter.Filter{
P: []string{"", "", "domain1"},
}); err != nil {
t.Errorf("unexpected error in LoadFilteredPolicy: %v", err)
}
testBatchEnforce(t, e, [][]interface{}{
{"alice", "data1", "domain1", "write"},
{"admin", "data1", "domain1", "write"},
}, []bool{
true, false,
})
}
func TestMultiplePolicyDefinitions(t *testing.T) {
e, _ := NewEnforcer("examples/multiple_policy_definitions_model.conf", "examples/multiple_policy_definitions_policy.csv")
enforceContext := NewEnforceContext("2")
enforceContext.EType = "e"
testBatchEnforce(t, e, [][]interface{}{
{"alice", "data2", "read"},
{enforceContext, struct{ Age int }{Age: 70}, "/data1", "read"},
{enforceContext, struct{ Age int }{Age: 30}, "/data1", "read"},
}, []bool{
true, false, true,
})
}
func TestPriorityExplicit(t *testing.T) {
e, _ := NewEnforcer("examples/priority_model_explicit.conf", "examples/priority_policy_explicit.csv")
testBatchEnforce(t, e, [][]interface{}{
{"alice", "data1", "write"},
{"alice", "data1", "read"},
{"bob", "data2", "read"},
{"bob", "data2", "write"},
{"data1_deny_group", "data1", "read"},
{"data1_deny_group", "data1", "write"},
{"data2_allow_group", "data2", "read"},
{"data2_allow_group", "data2", "write"},
}, []bool{
true, true, false, true, false, false, true, true,
})
_, err := e.AddPolicy("1", "bob", "data2", "write", "deny")
if err != nil {
t.Fatalf("Add Policy: %v", err)
}
testBatchEnforce(t, e, [][]interface{}{
{"alice", "data1", "write"},
{"alice", "data1", "read"},
{"bob", "data2", "read"},
{"bob", "data2", "write"},
{"data1_deny_group", "data1", "read"},
{"data1_deny_group", "data1", "write"},
{"data2_allow_group", "data2", "read"},
{"data2_allow_group", "data2", "write"},
}, []bool{
true, true, false, false, false, false, true, true,
})
}
func TestFailedToLoadPolicy(t *testing.T) {
e, _ := NewEnforcer("examples/rbac_with_pattern_model.conf", "examples/rbac_with_pattern_policy.csv")
e.AddNamedMatchingFunc("g2", "matchingFunc", util.KeyMatch2)
testEnforce(t, e, "alice", "/book/1", "GET", true)
testEnforce(t, e, "bob", "/pen/3", "GET", true)
e.SetAdapter(fileadapter.NewAdapter("not found"))
_ = e.LoadPolicy()
testEnforce(t, e, "alice", "/book/1", "GET", true)
testEnforce(t, e, "bob", "/pen/3", "GET", true)
}
func TestReloadPolicyWithFunc(t *testing.T) {
e, _ := NewEnforcer("examples/rbac_with_pattern_model.conf", "examples/rbac_with_pattern_policy.csv")
e.AddNamedMatchingFunc("g2", "matchingFunc", util.KeyMatch2)
testEnforce(t, e, "alice", "/book/1", "GET", true)
testEnforce(t, e, "bob", "/pen/3", "GET", true)
_ = e.LoadPolicy()
testEnforce(t, e, "alice", "/book/1", "GET", true)
testEnforce(t, e, "bob", "/pen/3", "GET", true)
}
func TestEvalPriority(t *testing.T) {
e, _ := NewEnforcer("examples/eval_operator_model.conf", "examples/eval_operator_policy.csv")
testEnforce(t, e, "admin", "users", "write", true)
testEnforce(t, e, "admin", "none", "write", false)
testEnforce(t, e, "user", "users", "write", false)
}
func TestLinkConditionFunc(t *testing.T) {
TrueFunc := func(args ...string) (bool, error) {
if len(args) != 0 {
return args[0] == "_" || args[0] == "true", nil
}
return false, nil
}
FalseFunc := func(args ...string) (bool, error) {
if len(args) != 0 {
return args[0] == "_" || args[0] == "false", nil
}
return false, nil
}
m, _ := model.NewModelFromFile("examples/rbac_with_temporal_roles_model.conf")
e, _ := NewEnforcer(m)
_, _ = e.AddPolicies([][]string{
{"alice", "data1", "read"},
{"alice", "data1", "write"},
{"data2_admin", "data2", "read"},
{"data2_admin", "data2", "write"},
{"data3_admin", "data3", "read"},
{"data3_admin", "data3", "write"},
{"data4_admin", "data4", "read"},
{"data4_admin", "data4", "write"},
{"data5_admin", "data5", "read"},
{"data5_admin", "data5", "write"},
})
_, _ = e.AddGroupingPolicies([][]string{
{"alice", "data2_admin", "_", "_"},
{"alice", "data3_admin", "_", "_"},
{"alice", "data4_admin", "_", "_"},
{"alice", "data5_admin", "_", "_"},
})
e.AddNamedLinkConditionFunc("g", "alice", "data2_admin", TrueFunc)
e.AddNamedLinkConditionFunc("g", "alice", "data3_admin", TrueFunc)
e.AddNamedLinkConditionFunc("g", "alice", "data4_admin", FalseFunc)
e.AddNamedLinkConditionFunc("g", "alice", "data5_admin", FalseFunc)
e.SetNamedLinkConditionFuncParams("g", "alice", "data2_admin", "true")
e.SetNamedLinkConditionFuncParams("g", "alice", "data3_admin", "not true")
e.SetNamedLinkConditionFuncParams("g", "alice", "data4_admin", "false")
e.SetNamedLinkConditionFuncParams("g", "alice", "data5_admin", "not false")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", true)
testEnforce(t, e, "alice", "data2", "read", true)
testEnforce(t, e, "alice", "data2", "write", true)
testEnforce(t, e, "alice", "data3", "read", false)
testEnforce(t, e, "alice", "data3", "write", false)
testEnforce(t, e, "alice", "data4", "read", true)
testEnforce(t, e, "alice", "data4", "write", true)
testEnforce(t, e, "alice", "data5", "read", false)
testEnforce(t, e, "alice", "data5", "write", false)
m, _ = model.NewModelFromFile("examples/rbac_with_domain_temporal_roles_model.conf")
e, _ = NewEnforcer(m)
_, _ = e.AddPolicies([][]string{
{"alice", "domain1", "data1", "read"},
{"alice", "domain1", "data1", "write"},
{"data2_admin", "domain2", "data2", "read"},
{"data2_admin", "domain2", "data2", "write"},
{"data3_admin", "domain3", "data3", "read"},
{"data3_admin", "domain3", "data3", "write"},
{"data4_admin", "domain4", "data4", "read"},
{"data4_admin", "domain4", "data4", "write"},
{"data5_admin", "domain5", "data5", "read"},
{"data5_admin", "domain5", "data5", "write"},
})
_, _ = e.AddGroupingPolicies([][]string{
{"alice", "data2_admin", "domain2", "_", "_"},
{"alice", "data3_admin", "domain3", "_", "_"},
{"alice", "data4_admin", "domain4", "_", "_"},
{"alice", "data5_admin", "domain5", "_", "_"},
})
e.AddNamedDomainLinkConditionFunc("g", "alice", "data2_admin", "domain2", TrueFunc)
e.AddNamedDomainLinkConditionFunc("g", "alice", "data3_admin", "domain3", TrueFunc)
e.AddNamedDomainLinkConditionFunc("g", "alice", "data4_admin", "domain4", FalseFunc)
e.AddNamedDomainLinkConditionFunc("g", "alice", "data5_admin", "domain5", FalseFunc)
e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data2_admin", "domain2", "true")
e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data3_admin", "domain3", "not true")
e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data4_admin", "domain4", "false")
e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data5_admin", "domain5", "not false")
testDomainEnforce(t, e, "alice", "domain1", "data1", "read", true)
testDomainEnforce(t, e, "alice", "domain1", "data1", "write", true)
testDomainEnforce(t, e, "alice", "domain2", "data2", "read", true)
testDomainEnforce(t, e, "alice", "domain2", "data2", "write", true)
testDomainEnforce(t, e, "alice", "domain3", "data3", "read", false)
testDomainEnforce(t, e, "alice", "domain3", "data3", "write", false)
testDomainEnforce(t, e, "alice", "domain4", "data4", "read", true)
testDomainEnforce(t, e, "alice", "domain4", "data4", "write", true)
testDomainEnforce(t, e, "alice", "domain5", "data5", "read", false)
testDomainEnforce(t, e, "alice", "domain5", "data5", "write", false)
}
func TestEnforcerWithDefaultDetector(t *testing.T) {
// Test that default detector is enabled and detects cycles
_, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_with_cycle_policy.csv")
// Expect an error because the policy contains a cycle
if err == nil {
t.Error("Expected cycle detection error when loading policy with cycle, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "cycle detected") {
t.Errorf("Expected error message to contain 'cycle detected', got: %s", errMsg)
}
}
}
func TestEnforcerRunDetections(t *testing.T) {
// Test explicit RunDetections() call
e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
// Should not error on valid policy
err := e.RunDetections()
if err != nil {
t.Errorf("Expected no error when running detections on valid policy, but got: %v", err)
}
// Now add a cycle manually
_, _ = e.AddGroupingPolicy("alice", "data2_admin")
_, _ = e.AddGroupingPolicy("data2_admin", "super_admin")
_, _ = e.AddGroupingPolicy("super_admin", "alice")
// Should detect the cycle
err = e.RunDetections()
if err == nil {
t.Error("Expected cycle detection error, but got nil")
} else {
errMsg := err.Error()
if !strings.Contains(errMsg, "cycle detected") {
t.Errorf("Expected error message to contain 'cycle detected', got: %s", errMsg)
}
}
}
func TestEnforcerSetDetector(t *testing.T) {
// Test SetDetector() method
e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
// Create a custom detector
customDetector := detector.NewDefaultDetector()
e.SetDetector(customDetector)
// Should still work with custom detector
err := e.RunDetections()
if err != nil {
t.Errorf("Expected no error with custom detector, but got: %v", err)
}
}
func TestEnforcerSetDetectors(t *testing.T) {
// Test SetDetectors() method
e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
// Create multiple detectors
detectors := []detector.Detector{
detector.NewDefaultDetector(),
detector.NewDefaultDetector(),
}
e.SetDetectors(detectors)
// Should work with multiple detectors
err := e.RunDetections()
if err != nil {
t.Errorf("Expected no error with multiple detectors, but got: %v", err)
}
}

124
enforcer_transactional.go Normal file
View File

@@ -0,0 +1,124 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"context"
"errors"
"sync"
"sync/atomic"
"time"
"github.com/casbin/casbin/v3/persist"
"github.com/google/uuid"
)
// TransactionalEnforcer extends Enforcer with transaction support.
// It provides atomic policy operations through transactions.
type TransactionalEnforcer struct {
*Enforcer // Embedded enforcer for all standard functionality
activeTransactions sync.Map // Stores active transactions.
modelVersion int64 // Model version number for optimistic locking.
commitLock sync.Mutex // Protects commit and rollback operations.
}
// NewTransactionalEnforcer creates a new TransactionalEnforcer.
// It accepts the same parameters as NewEnforcer.
func NewTransactionalEnforcer(params ...interface{}) (*TransactionalEnforcer, error) {
enforcer, err := NewEnforcer(params...)
if err != nil {
return nil, err
}
return &TransactionalEnforcer{
Enforcer: enforcer,
}, nil
}
// BeginTransaction starts a new transaction.
// Returns an error if a transaction is already in progress or if the adapter doesn't support transactions.
func (te *TransactionalEnforcer) BeginTransaction(ctx context.Context) (*Transaction, error) {
// Check if adapter supports transactions.
txAdapter, ok := te.adapter.(persist.TransactionalAdapter)
if !ok {
return nil, errors.New("adapter does not support transactions")
}
// Start database transaction.
txContext, err := txAdapter.BeginTransaction(ctx)
if err != nil {
return nil, err
}
// Create transaction buffer with current model snapshot.
buffer := NewTransactionBuffer(te.model)
tx := &Transaction{
id: uuid.New().String(),
enforcer: te,
buffer: buffer,
txContext: txContext,
ctx: ctx,
baseVersion: atomic.LoadInt64(&te.modelVersion),
startTime: time.Now(),
}
te.activeTransactions.Store(tx.id, tx)
return tx, nil
}
// GetTransaction returns a transaction by its ID, or nil if not found.
func (te *TransactionalEnforcer) GetTransaction(id string) *Transaction {
if tx, ok := te.activeTransactions.Load(id); ok {
return tx.(*Transaction)
}
return nil
}
// IsTransactionActive returns true if the transaction with the given ID is active.
func (te *TransactionalEnforcer) IsTransactionActive(id string) bool {
if tx := te.GetTransaction(id); tx != nil {
return tx.IsActive()
}
return false
}
// WithTransaction executes a function within a transaction.
// If the function returns an error, the transaction is rolled back.
// Otherwise, it's committed automatically.
func (te *TransactionalEnforcer) WithTransaction(ctx context.Context, fn func(*Transaction) error) error {
tx, err := te.BeginTransaction(ctx)
if err != nil {
return err
}
defer func() {
if r := recover(); r != nil {
_ = tx.Rollback()
panic(r)
}
}()
err = fn(tx)
if err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
}

288
error_test.go Normal file
View File

@@ -0,0 +1,288 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"testing"
fileadapter "github.com/casbin/casbin/v3/persist/file-adapter"
)
func TestPathError(t *testing.T) {
_, err := NewEnforcer("hope_this_path_wont_exist", "")
if err == nil {
t.Errorf("Should be error here.")
} else {
t.Log("Test on error: ")
t.Log(err.Error())
}
}
func TestEnforcerParamError(t *testing.T) {
_, err := NewEnforcer(1, 2, 3)
if err == nil {
t.Errorf("Should not be error here.")
} else {
t.Log("Test on error: ")
t.Log(err.Error())
}
_, err2 := NewEnforcer(1, "2")
if err2 == nil {
t.Errorf("Should not be error here.")
} else {
t.Log("Test on error: ")
t.Log(err2.Error())
}
}
func TestModelError(t *testing.T) {
_, err := NewEnforcer("examples/error/error_model.conf", "examples/error/error_policy.csv")
if err == nil {
t.Errorf("Should be error here.")
} else {
t.Log("Test on error: ")
t.Log(err.Error())
}
}
// func TestPolicyError(t *testing.T) {
// _, err := NewEnforcer("examples/basic_model.conf", "examples/error/error_policy.csv")
// if err == nil {
// t.Errorf("Should be error here.")
// } else {
// t.Log("Test on error: ")
// t.Log(err.Error())
// }
//}
func TestEnforceError(t *testing.T) {
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
_, err := e.Enforce("wrong", "wrong")
if err == nil {
t.Errorf("Should be error here.")
} else {
t.Log("Test on error: ")
t.Log(err.Error())
}
e, _ = NewEnforcer("examples/abac_rule_model.conf")
_, err = e.Enforce("wang", "wang", "wang")
if err == nil {
t.Errorf("Should be error here.")
} else {
t.Log("Test on error: ")
t.Log(err.Error())
}
}
func TestNoError(t *testing.T) {
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
err := e.LoadModel()
if err != nil {
t.Errorf("Should be no error here.")
t.Log("Unexpected error: ")
t.Log(err.Error())
}
err = e.LoadPolicy()
if err != nil {
t.Errorf("Should be no error here.")
t.Log("Unexpected error: ")
t.Log(err.Error())
}
err = e.SavePolicy()
if err != nil {
t.Errorf("Should be no error here.")
t.Log("Unexpected error: ")
t.Log(err.Error())
}
}
func TestModelNoError(t *testing.T) {
e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
e.modelPath = "hope_this_path_wont_exist"
err := e.LoadModel()
if err == nil {
t.Errorf("Should be error here.")
} else {
t.Log("Test on error: ")
t.Log(err.Error())
}
}
func TestMockAdapterErrors(t *testing.T) {
adapter := fileadapter.NewAdapterMock("examples/rbac_with_domains_policy.csv")
adapter.SetMockErr("mock error")
e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", adapter)
added, err := e.AddPolicy("admin", "domain3", "data1", "read")
if added {
t.Errorf("added should be false")
}
if err == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err.Error())
}
rules := [][]string{
{"admin", "domain4", "data1", "read"},
}
added, err = e.AddPolicies(rules)
if added {
t.Errorf("added should be false")
}
if err == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err.Error())
}
removed, err2 := e.RemoveFilteredPolicy(1, "domain1", "data1")
if removed {
t.Errorf("removed should be false")
}
if err2 == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err2.Error())
}
removed, err3 := e.RemovePolicy("admin", "domain2", "data2", "read")
if removed {
t.Errorf("removed should be false")
}
if err3 == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err3.Error())
}
rules = [][]string{
{"admin", "domain1", "data1", "read"},
}
removed, err = e.RemovePolicies(rules)
if removed {
t.Errorf("removed should be false")
}
if err == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err.Error())
}
added, err4 := e.AddGroupingPolicy("bob", "admin2")
if added {
t.Errorf("added should be false")
}
if err4 == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err4.Error())
}
added, err5 := e.AddNamedGroupingPolicy("g", []string{"eve", "admin2", "domain1"})
if added {
t.Errorf("added should be false")
}
if err5 == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err5.Error())
}
added, err6 := e.AddNamedPolicy("p", []string{"admin2", "domain2", "data2", "write"})
if added {
t.Errorf("added should be false")
}
if err6 == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err6.Error())
}
removed, err7 := e.RemoveGroupingPolicy("bob", "admin2")
if removed {
t.Errorf("removed should be false")
}
if err7 == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err7.Error())
}
removed, err8 := e.RemoveFilteredGroupingPolicy(0, "bob")
if removed {
t.Errorf("removed should be false")
}
if err8 == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err8.Error())
}
removed, err9 := e.RemoveNamedGroupingPolicy("g", []string{"alice", "admin", "domain1"})
if removed {
t.Errorf("removed should be false")
}
if err9 == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err9.Error())
}
removed, err10 := e.RemoveFilteredNamedGroupingPolicy("g", 0, "eve")
if removed {
t.Errorf("removed should be false")
}
if err10 == nil {
t.Errorf("Should be an error here.")
} else {
t.Log("Test on error: ")
t.Log(err10.Error())
}
}

View File

@@ -0,0 +1,49 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package errors
import (
"errors"
"fmt"
)
// Global errors for constraints defined here.
var (
ErrConstraintViolation = errors.New("constraint violation")
ErrConstraintParsingError = errors.New("constraint parsing error")
ErrConstraintRequiresRBAC = errors.New("constraints require RBAC to be enabled (role_definition section must exist)")
ErrInvalidConstraintDefinition = errors.New("invalid constraint definition")
)
// ConstraintViolationError represents a specific constraint violation.
type ConstraintViolationError struct {
ConstraintName string
Message string
}
func (e *ConstraintViolationError) Error() string {
return fmt.Sprintf("constraint violation [%s]: %s", e.ConstraintName, e.Message)
}
// NewConstraintViolationError creates a new constraint violation error.
func NewConstraintViolationError(constraintName, message string) error {
return &ConstraintViolationError{
ConstraintName: constraintName,
Message: message,
}
}

33
errors/rbac_errors.go Normal file
View File

@@ -0,0 +1,33 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package errors
import "errors"
// Global errors for rbac defined here.
var (
ErrNameNotFound = errors.New("error: name does not exist")
ErrDomainParameter = errors.New("error: domain should be 1 parameter")
ErrLinkNotFound = errors.New("error: link between name1 and name2 does not exist")
ErrUseDomainParameter = errors.New("error: useDomain should be 1 parameter")
ErrInvalidFieldValuesParameter = errors.New("fieldValues requires at least one parameter")
// GetAllowedObjectConditions errors.
ErrObjCondition = errors.New("need to meet the prefix required by the object condition")
ErrEmptyCondition = errors.New("GetAllowedObjectConditions have an empty condition")
)

11
examples/abac_model.conf Normal file
View File

@@ -0,0 +1,11 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == r.obj.Owner

View File

@@ -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

View File

@@ -0,0 +1,4 @@
p, alice, /data1, read, deny
p, alice, /data1, write, allow
p, bob, /data2, write, deny
p, bob, /data2, read, allow
1 p alice /data1 read deny
2 p alice /data1 write allow
3 p bob /data2 write deny
4 p bob /data2 read allow

View File

@@ -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

View File

@@ -0,0 +1,2 @@
p, r.sub.Age > 18, /data1, read
p, r.sub.Age < 60, /data2, write
1 p r.sub.Age > 18 /data1 read
2 p r.sub.Age < 60 /data2 write

View File

@@ -0,0 +1,2 @@
p, alice, data1, write
p, bob, data2, read
1 p alice data1 write
2 p bob data2 read

11
examples/basic_model.conf Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
p, alice, data1, read
p, bob, data2, write
1 p alice data1 read
2 p bob data2 write

View File

@@ -0,0 +1,11 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"

View File

@@ -0,0 +1,11 @@
[request_definition]
r = sub, act
[policy_definition]
p = sub, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.act == p.act

View File

@@ -0,0 +1,2 @@
p, alice, read
p, bob, write
1 p alice read
2 p bob write

View File

@@ -0,0 +1,11 @@
[request_definition]
r = obj, act
[policy_definition]
p = obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.obj == p.obj && r.act == p.act

View File

@@ -0,0 +1,2 @@
p, data1, read
p, data2, write
1 p data1 read
2 p data2 write

14
examples/biba_model.conf Normal file
View File

@@ -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)

14
examples/blp_model.conf Normal file
View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
p, alice, data1, read
bob, data2, write
1 p, alice, data1, read
2 bob, data2, write

View File

@@ -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)

View File

@@ -0,0 +1 @@
p, r.sub == 'admin' || false, r.obj == 'users', write
1 p r.sub == 'admin' || false r.obj == 'users' write

11
examples/glob_model.conf Normal file
View File

@@ -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

4
examples/glob_policy.csv Normal file
View File

@@ -0,0 +1,4 @@
p, u1, /foo/*, read
p, u2, /foo*, read
p, u3, /*/foo/*, read
p, u4, *, read
1 p u1 /foo/* read
2 p u2 /foo* read
3 p u3 /*/foo/* read
4 p u4 * read

View File

@@ -0,0 +1,11 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = ipMatch(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

View File

@@ -0,0 +1,2 @@
p, 192.168.2.0/24, data1, read
p, 10.0.0.0/16, data2, write
1 p 192.168.2.0/24 data1 read
2 p 10.0.0.0/16 data2 write

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,11 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)

View File

@@ -0,0 +1,2 @@
p, alice, /alice_data/:resource, GET
p, alice, /alice_data2/:id/using/:resId, GET
1 p alice /alice_data/:resource GET
2 p alice /alice_data2/:id/using/:resId GET

View File

@@ -0,0 +1,11 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatchCustom(r.obj, p.obj) && regexMatch(r.act, p.act)

View File

@@ -0,0 +1,11 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)

View File

@@ -0,0 +1,7 @@
p, alice, /alice_data/*, GET
p, alice, /alice_data/resource1, POST
p, bob, /alice_data/resource2, GET
p, bob, /bob_data/*, POST
p, cathy, /cathy_data, (GET)|(POST)
1 p alice /alice_data/* GET
2 p alice /alice_data/resource1 POST
3 p bob /alice_data/resource2 GET
4 p bob /bob_data/* POST
5 p cathy /cathy_data (GET)|(POST)

View File

@@ -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)

View File

@@ -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
1 g, can_manage, can_use, *
2 p, can_manage, engines/*, *, (pause)|(resume)
3 p, can_use, engines/*, *, (attach)|(detach)
4 g, Username==test2, can_manage, engines/engine1

14
examples/lbac_model.conf Normal file
View File

@@ -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)

View File

@@ -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

View File

@@ -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
1 p, data2_admin, data2, read
2 p2, r2.sub.Age > 18 && r2.sub.Age < 60, /data1, read, allow
3 p2, r2.sub.Age > 60 && r2.sub.Age < 100, /data1, read, deny
4 g, alice, data2_admin

View File

@@ -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

View File

@@ -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
1 p, alice, r.obj.price < 25, read
2 p, admin, r.obj.category_id = 2, read
3 p, bob, r.obj.author = bob, write
4 g, alice, admin

16
examples/orbac_model.conf Normal file
View File

@@ -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

25
examples/orbac_policy.csv Normal file
View File

@@ -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
1 # Permission rules: role, activity, view, organization
2 p, manager, modify, document, org1
3 p, manager, consult, document, org1
4 p, employee, consult, document, org1
5 p, manager, modify, report, org2
6 p, manager, consult, report, org2
7 p, employee, consult, report, org2
8 # Empower: subject, role, organization
9 g, alice, manager, org1
10 g, bob, employee, org1
11 g, charlie, manager, org2
12 g, david, employee, org2
13 # Use: action, activity, organization
14 g2, write, modify, org1
15 g2, read, consult, org1
16 g2, write, modify, org2
17 g2, read, consult, org2
18 # Consider: object, view, organization
19 g3, data1, document, org1
20 g3, data2, document, org1
21 g3, report1, report, org2
22 g3, report2, report, org2

11
examples/pbac_model.conf Normal file
View File

@@ -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

2
examples/pbac_policy.csv Normal file
View File

@@ -0,0 +1,2 @@
p, r.sub.Role == 'admin', r.obj.Type == 'doc', read
p, r.sub.Age >= 18, r.obj.Type == 'video', play
1 p r.sub.Role == 'admin' r.obj.Type == 'doc' read
2 p r.sub.Age >= 18 r.obj.Type == 'video' play

Some files were not shown because too many files have changed in this diff Show More