Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add interface stubs for async adapters #335

Merged
merged 7 commits into from
Dec 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 43 additions & 45 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,55 +11,53 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.9', '3.10', '3.11']
python-version: ["3.9", "3.10", "3.11"]
os: [ubuntu-latest, macOS-latest, windows-latest]

steps:
- name: Checkout
uses: actions/checkout@v2
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements_dev.txt
pip install coveralls
pip install pytest
pip install pytest-benchmark
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements_dev.txt
pip install coveralls

- name: Run tests
run: coverage run -m unittest discover -s tests -t tests
- name: Run tests
run: coverage run -m unittest discover -s tests -t tests

- name: Run benchmark
run: python3 -m pytest
--benchmark-verbose
--benchmark-columns=mean,stddev,iqr,ops,rounds
tests/benchmarks/benchmark_model.py
tests/benchmarks/benchmark_management_api.py
tests/benchmarks/benchmark_role_manager.py
- name: Run benchmark
run: python3 -m pytest
--benchmark-verbose
--benchmark-columns=mean,stddev,iqr,ops,rounds
tests/benchmarks/benchmark_model.py
tests/benchmarks/benchmark_management_api.py
tests/benchmarks/benchmark_role_manager.py

- name: Upload coverage data to coveralls.io
run: coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: ${{ matrix.os }} - ${{ matrix.python-version }}
COVERALLS_PARALLEL: true
- name: Upload coverage data to coveralls.io
run: coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: ${{ matrix.os }} - ${{ matrix.python-version }}
COVERALLS_PARALLEL: true

lint:
name: Run Linters
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Super-Linter
uses: github/super-linter@v4.9.2
uses: super-linter/super-linter@v5.7.2
env:
VALIDATE_ALL_CODEBASE: false
VALIDATE_PYTHON_BLACK: true
Expand All @@ -74,36 +72,36 @@ jobs:
runs-on: ubuntu-latest
container: python:3-slim
steps:
- name: Finished
run: |
pip3 install --upgrade coveralls
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Finished
run: |
pip3 install --upgrade coveralls
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

release:
name: Release
runs-on: ubuntu-latest
needs: [ test, coveralls ]
needs: [test, coveralls]
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '18'
node-version: "18"

- name: Setup
run: npm install

- name: Set up python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: 3.11

- name: Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ https://casbin.org/docs/role-managers

If your code use `async` / `await` and is heavily dependent on I/O operations, you can adopt Async Enforcer!

1. Create an async engine and new a Casbin AsyncEnforcer with a model file and an async Pycasbin adapter:
1. Create an async engine and new a Casbin AsyncEnforcer with a model file and an async Pycasbin adapter (AsyncAdapter subclass):

```python
import asyncio
Expand Down Expand Up @@ -266,6 +266,8 @@ async def get_enforcer():

Note: you can see all supported adapters in [Adapters | Casbin](https://casbin.org/docs/adapters).

Built-in async adapters are available in `casbin.persist.adapters.asyncio`.

2. Add an enforcement hook into your code right before the access happens:

```python
Expand Down
5 changes: 2 additions & 3 deletions casbin/async_internal_enforcer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@

from casbin.core_enforcer import CoreEnforcer
from casbin.model import Model, FunctionMap
from casbin.persist import Adapter
from casbin.persist.adapters.async_file_adapter import AsyncFileAdapter
from casbin.persist.adapters.asyncio import AsyncFileAdapter, AsyncAdapter


class AsyncInternalEnforcer(CoreEnforcer):
Expand All @@ -32,7 +31,7 @@ def init_with_file(self, model_path, policy_path):
def init_with_model_and_adapter(self, m, adapter=None):
"""initializes an enforcer with a model and a database adapter."""

if not isinstance(m, Model) or adapter is not None and not isinstance(adapter, Adapter):
if not isinstance(m, Model) or adapter is not None and not isinstance(adapter, AsyncAdapter):
raise RuntimeError("Invalid parameters for enforcer.")

self.adapter = adapter
Expand Down
1 change: 0 additions & 1 deletion casbin/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ def _parse_buffer(self, f):
buf.append(p)

def _write(self, section, line_num, b):

buf = "".join(b)
if len(buf) <= 0:
return
Expand Down
3 changes: 1 addition & 2 deletions casbin/distributed_enforcer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
# limitations under the License.

from casbin.model.policy_op import PolicyOp
from casbin.persist import batch_adapter
from casbin.persist.adapters import update_adapter
from casbin.persist import batch_adapter, update_adapter
from casbin.synced_enforcer import SyncedEnforcer


Expand Down
2 changes: 1 addition & 1 deletion casbin/persist/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@

from .adapter import *
from .adapter_filtered import *
from .batch_adapter import *
from .adapters import *
from .batch_adapter import *
9 changes: 7 additions & 2 deletions casbin/persist/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@
# limitations under the License.

from .file_adapter import FileAdapter
from .adapter_filtered import FilteredAdapter
from .update_adapter import UpdateAdapter
from .filtered_file_adapter import FilteredFileAdapter
from ..update_adapter import UpdateAdapter

# alias import for backwards compatibility
FilteredAdapter = FilteredFileAdapter

__all__ = ["FileAdapter", "FilteredFileAdapter", "FilteredAdapter", "UpdateAdapter"]
110 changes: 5 additions & 105 deletions casbin/persist/adapters/adapter_filtered.py
Original file line number Diff line number Diff line change
@@ -1,107 +1,7 @@
# Copyright 2021 The casbin Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# NOTE: this file exists as a backwards compatible alias. please directly
# use FilteredFileAdapter from `casbin.persist.adapters.filtered_file_adapter` instead.

from casbin import persist
from .file_adapter import FileAdapter
import os
from .filtered_file_adapter import Filter
from .filtered_file_adapter import FilteredFileAdapter as FilteredAdapter


class Filter:
# P,G are string []
P = []
G = []


class FilteredAdapter(FileAdapter, persist.FilteredAdapter):
filtered = False
_file_path = ""
filter = Filter()
# new_filtered_adapte is the constructor for FilteredAdapter.
def __init__(self, file_path):
self.filtered = True
self._file_path = file_path

def load_policy(self, model):
if not os.path.isfile(self._file_path):
raise RuntimeError("invalid file path, file path cannot be empty")
self.filtered = False
self._load_policy_file(model)

# load_filtered_policy loads only policy rules that match the filter.
def load_filtered_policy(self, model, filter):
if filter == None:
return self.load_policy(model)

if not os.path.isfile(self._file_path):
raise RuntimeError("invalid file path, file path cannot be empty")

try:
filter_value = [filter.__dict__["P"]] + [filter.__dict__["G"]]
except:
raise RuntimeError("invalid filter type")

self.load_filtered_policy_file(model, filter_value, persist.load_policy_line)
self.filtered = True

def load_filtered_policy_file(self, model, filter, hanlder):
with open(self._file_path, "rb") as file:
while True:
line = file.readline()
line = line.decode().strip()
if line == "\n":
continue
if not line:
break
if filter_line(line, filter):
continue

hanlder(line, model)

# is_filtered returns true if the loaded policy has been filtered.
def is_filtered(self):
return self.filtered

def save_policy(self, model):
if self.filtered:
raise RuntimeError("cannot save a filtered policy")

self._save_policy_file(model)


def filter_line(line, filter):
if filter == None:
return False

p = line.split(",")
if len(p) == 0:
return True
filter_slice = []

if p[0].strip() == "p":
filter_slice = filter[0]
elif p[0].strip() == "g":
filter_slice = filter[1]
return filter_words(p, filter_slice)


def filter_words(line, filter):
if len(line) < len(filter) + 1:
return True
skip_line = False
for i, v in enumerate(filter):
if len(v) > 0 and (v.strip() != line[i + 1].strip()):
skip_line = True
break

return skip_line
__all__ = ["Filter", "FilteredAdapter"]
13 changes: 13 additions & 0 deletions casbin/persist/adapters/asyncio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .adapter import AsyncAdapter
from .adapter_filtered import AsyncFilteredAdapter
from .batch_adapter import AsyncBatchAdapter
from .file_adapter import AsyncFileAdapter
from .update_adapter import AsyncUpdateAdapter

__all__ = [
"AsyncAdapter",
"AsyncFilteredAdapter",
"AsyncBatchAdapter",
"AsyncFileAdapter",
"AsyncUpdateAdapter",
]
32 changes: 32 additions & 0 deletions casbin/persist/adapters/asyncio/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from abc import ABCMeta, abstractmethod


class AsyncAdapter(metaclass=ABCMeta):
"""The interface for async Casbin adapters."""

@abstractmethod
async def load_policy(self, model):
"""loads all policy rules from the storage."""
pass

@abstractmethod
async def save_policy(self, model):
"""saves all policy rules to the storage."""
pass

@abstractmethod
async def add_policy(self, sec, ptype, rule):
"""adds a policy rule to the storage."""
pass

@abstractmethod
async def remove_policy(self, sec, ptype, rule):
"""removes a policy rule from the storage."""
pass

@abstractmethod
async def remove_filtered_policy(self, sec, ptype, field_index, *field_values):
"""removes policy rules that match the filter from the storage.
This is part of the Auto-Save feature.
"""
pass
17 changes: 17 additions & 0 deletions casbin/persist/adapters/asyncio/adapter_filtered.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from abc import ABCMeta, abstractmethod


class AsyncFilteredAdapter(metaclass=ABCMeta):
"""AsyncFilteredAdapter is the interface for async Casbin adapters supporting filtered policies."""

@abstractmethod
async def is_filtered(self):
"""IsFiltered returns true if the loaded policy has been filtered
Marks if the loaded policy is filtered or not
"""
pass

@abstractmethod
async def load_filtered_policy(self, model, filter):
"""Loads policy rules that match the filter from the storage."""
pass
Loading