Skip to content

Commit

Permalink
feat: add interface stubs for async adapters (#335)
Browse files Browse the repository at this point in the history
* refactor!: change reorganize imports for sane asyncio ext

* feat: async adapter interface stubs

* chore: reflect pr comments

* chore: revert black bump pending decision

* refactor: move update adapter interface to persist module

* ci: bump tooling and linting versions

* ci: fix linting action
  • Loading branch information
thearchitector authored Dec 28, 2023
1 parent 47e5ef5 commit d557189
Show file tree
Hide file tree
Showing 23 changed files with 360 additions and 238 deletions.
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

0 comments on commit d557189

Please sign in to comment.