Git Product home page Git Product logo

fastapi-authz's Introduction

fastapi-authz

Build Status Coverage Status Version PyPI - Wheel Pyversions Download Discord

fastapi-authz is an authorization middleware for FastAPI, it's based on PyCasbin.

Installation

Install from pip

pip install fastapi-authz

Clone this repo

git clone https://github.com/pycasbin/fastapi-authz.git
python setup.py install

Quickstart

This middleware is designed to work with another middleware which implement AuthenticationMiddleware interface.

import base64
import binascii

import casbin

from fastapi import FastAPI
from starlette.authentication import AuthenticationBackend, AuthenticationError, SimpleUser, AuthCredentials
from starlette.middleware.authentication import AuthenticationMiddleware

from fastapi_authz import CasbinMiddleware

app = FastAPI()


class BasicAuth(AuthenticationBackend):
    async def authenticate(self, request):
        if "Authorization" not in request.headers:
            return None

        auth = request.headers["Authorization"]
        try:
            scheme, credentials = auth.split()
            decoded = base64.b64decode(credentials).decode("ascii")
        except (ValueError, UnicodeDecodeError, binascii.Error):
            raise AuthenticationError("Invalid basic auth credentials")

        username, _, password = decoded.partition(":")
        return AuthCredentials(["authenticated"]), SimpleUser(username)


enforcer = casbin.Enforcer('../examples/rbac_model.conf', '../examples/rbac_policy.csv')

app.add_middleware(CasbinMiddleware, enforcer=enforcer)
app.add_middleware(AuthenticationMiddleware, backend=BasicAuth())


@app.get('/')
async def index():
    return "If you see this, you have been authenticated."


@app.get('/dataset1/protected')
async def auth_test():
    return "You must be alice to see this."
  • anonymous request
curl -i http://127.0.0.1:8000/dataset1/protected
HTTP/1.1 403 Forbidden
date: Mon, 01 Mar 2021 09:00:08 GMT
server: uvicorn
content-length: 11
content-type: application/json

"Forbidden"
  • authenticated request
curl -i -u alice:password http://127.0.0.1:8000/dataset1/protected
HTTP/1.1 200 OK
date: Mon, 01 Mar 2021 09:04:54 GMT
server: uvicorn
content-length: 32
content-type: application/json

"You must be alice to see this."

It used the casbin config from examples folder, and you can find this demo in demo folder.

You can also view the unit tests to understand this middleware.

Besides, there is another example for CasbinMiddleware which is designed to work with JWT authentication. You can find it in demo/jwt_test.py.

Development

Run unit tests

  1. Fork/Clone repository
  2. Install fastapi-authz dependencies, and run pytest
pip install -r dev_requirements.txt
pip install -r requirements.txt
pytest

Update requirements with pip-tools

# update requirements.txt
pip-compile --no-annotate --no-header --rebuild requirements.in
# sync venv
pip-sync

Manually Bump Version

bumpversion major  # major release
or
bumpversion minor  # minor release
or
bumpversion patch  # hotfix release

Documentation

The authorization determines a request based on {subject, object, action}, which means what subject can perform what action on what object. In this plugin, the meanings are:

  1. subject: the logged-in user name
  2. object: the URL path for the web resource like dataset1/item1
  3. action: HTTP method like GET, POST, PUT, DELETE, or the high-level actions you defined like "read-file", " write-blog" (currently no official support in this middleware)

For how to write authorization policy and other details, please refer to the Casbin's documentation.

Getting Help

License

This project is under Apache 2.0 License. See the LICENSE file for the full license text.

fastapi-authz's People

Contributors

hsluoyz avatar nekotoxin avatar selflocking avatar semantic-release-bot avatar zxilly avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

fastapi-authz's Issues

Encounterd with preflight request failure when using fastapi-authz with axios

Code

backend

fastapi 0.68
main.py

app.add_middleware(CORSMiddleware,
        allow_origins=["http://localhost:8080"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
app.add_middleware(CasbinMiddleware, enforcer=enforcer)
app.add_middleware(AuthenticationMiddleware, backend=JWTAuth())

app.include_router(api_router, prefix=settings.API_V1_STR)

rbac.py for AuthenticationMiddleware backend, and Casbin enforcer

class JWTAuth(AuthenticationBackend):
    async def authenticate(self, request):
        if "Authorization" not in request.headers:
            return None

        if "Authorization" in request.headers and not request.headers["Authorization"].startswith("Bearer"):
            return None

        if request.headers["Authorization"].startswith("Bearer"):
            auth = request.headers["Authorization"]

        try:
            scheme, token = auth.split()
            payload = jwt.decode(
                token, settings.SECRET_KEY, algorithms=["HS256"])

            email = payload["email"]
        except (jwt.JWTError, ValidationError):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Cannot validate credential",
            )

        return AuthCredentials(["authenticated"]), SimpleUser(email)

enforcer = casbin.Enforcer(
    '../app/core/rbac_rules/rbac_model.conf',
    '../app/core/rbac_rules/rbac_policy.csv'
)

endpoints/users.py - read_user_me

@router.get("/me", response_model=schemas.User)
def read_user_me(
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    return current_user

rbac_policy.csv

p, *, /api/v1/openapi.json, GET
p, *, /docs, *
p, *, /api/v1/login/access-token, *
p, admin, *, *

g, [email protected], admin,

frontend

vue.sj 3 Compositional API + Quasar 2 + axios 0.21.1 + vuex 4.0.1
store/auth/actions.js

export const doLogin = async ({ commit, dispatch }, formData) => {
  await api.post("/login/access-token", formData).then((response) => {
    const token = response.data;
    commit("setToken", token);
    api.defaults.headers.common["Authorization"] =
      "Bearer " + token.access_token;
    dispatch("getMe", token);
  });
};

export const getMe = async ({ commit }, token) => {
  await api.get("/users/me", token).then((response) => {
    commit("setMe", response.data);
  });
};

Description

The problem is Axios (api) sends Authorization info with the request header, then the browser needs to use a preflight request to check that request. But the preflight check failed, and the request headers passed to the backend would be missing the Authorization (Bearer + Token), which is causing the Authentication error.

And then, with no Authentication, there would be no user displayname, and the is_authenticated would be false. Without the username, Casbin won't let anonymous users use OPTIONS on /api/v1/users/me.

image

The browser shows the error message as below. But I guess is not the CORS policy that blocked the access, it is actually Casbin who blocked the access.

image

My Solution

I tried to add permission for anonymous users to use OPTIONS on /api/v1/users/me, and that worked. Finally, Axios can get user info from the backend.

rbac_policy.csv

p, *, /api/v1/openapi.json, GET
p, *, /docs, *
p, *, /api/v1/login/access-token, *
p, anonymous, /api/v1/users/me, OPTIONS  # see this line
p, admin, *, *

g, [email protected], admin,

But I'm not sure it's the right way to do so, because after logging in, I still had to use the anonymous user to get permission, it feels weird and insecure.

I'm wondering is there any better way to fix it, if anyone can help me to improve, thanks.

The automated release is failing 🚨

🚨 The automated release from the master branch failed. 🚨

I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.

You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this πŸ’ͺ.

Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.

Once all the errors are resolved, semantic-release will release your package the next time you push a commit to the master branch. You can also manually restart the failed CI job that runs semantic-release.

If you are not sure how to resolve this, here is some links that can help you:

If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.


Missing package.json file.

A package.json file at the root of your project is required to release on npm.

Please follow the npm guideline to create a valid package.json file.


Good luck with your project ✨

Your semantic-release bot πŸ“¦πŸš€

broken package

Hi all,
I'm trying to install fastapi-authz via pip, but seems there is an error with the dist package :

pip install fastapi-authz
Collecting fastapi-authz
  Using cached fastapi-authz-0.0.2.tar.gz (4.9 kB)
    ERROR: Command errored out with exit status 1:
     command: /usr/local/bin/python -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/tmp/pip-install-pu2tern5/fastapi-authz_d8d17d7906794d5fa50c26850a619893/setup.py'"'"'; __file__='"'"'/tmp/pip-install-pu2tern5/fastapi-authz_d8d17d7906794d5fa50c26850a619893/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' egg_info --egg-base /tmp/pip-pip-egg-info-58f2tko6
         cwd: /tmp/pip-install-pu2tern5/fastapi-authz_d8d17d7906794d5fa50c26850a619893/
    Complete output (159 lines):
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-install-pu2tern5/fastapi-authz_d8d17d7906794d5fa50c26850a619893/setup.py", line 16, in <module>
        with open(path.join(here, "requirements.txt"), encoding="utf-8") as f:
    FileNotFoundError: [Errno 2] No such file or directory: '/tmp/pip-install-pu2tern5/fastapi-authz_d8d17d7906794d5fa50c26850a619893/requirements.txt'
    # fastapi-authz
    
    fastapi-authz is an authorization middleware for [FastAPI](https://fastapi.tiangolo.com/), it's based
    on [PyCasbin](https://github.com/casbin/pycasbin).

How do I casbin_redis_watcher with a device?

I throw an error when I use the following code

def add_authentication_middleware(app: FastAPI):
    class BasicAuth(AuthenticationBackend):
        async def authenticate(self, request):
            if "Authorization" not in request.headers:
                return None

            auth = request.headers["Authorization"]
            try:
                scheme, credentials = auth.split()
                decoded = base64.b64decode(credentials).decode("ascii")
            except (ValueError, UnicodeDecodeError, binascii.Error):
                raise AuthenticationError("Invalid basic auth credentials")

            username, _, password = decoded.partition(":")
            return AuthCredentials(["authenticated"]), SimpleUser(username)

    model_file_path = Path(__file__).parent / "core/configs/casbin_model.conf"
    model_file_path = str(model_file_path.absolute())

    adapter = casbin_sqlalchemy_adapter.Adapter(settings.SQLALCHEMY_DATABASE_URI)

    enforcer = casbin.Enforcer(model_file_path, adapter, settings.CASBIN_LOG)

    watcher = RedisWatcher(
        settings.CASBIN_REDIS_WATCHER_HOST, settings.CASBIN_REDIS_WATCHER_PORT
    )
    watcher.set_update_callback(enforcer.load_policy)
    enforcer.set_watcher(watcher)

    app.add_middleware(CasbinMiddleware, enforcer=enforcer)
    app.add_middleware(AuthenticationMiddleware, backend=BasicAuth())
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/spawn.py", line 105, in spawn_main
    exitcode = _main(fd)
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/spawn.py", line 114, in _main
    prepare(preparation_data)
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/spawn.py", line 225, in prepare
    _fixup_main_from_path(data['init_main_from_path'])
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/spawn.py", line 277, in _fixup_main_from_path
    run_name="__mp_main__")
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/runpy.py", line 263, in run_path
    pkg_name=pkg_name, script_name=fname)
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/runpy.py", line 96, in _run_module_code
    mod_name, mod_spec, pkg_name, script_name)
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/Users/luyifei/PycharmProjects/pinstreet_open_service_backend/main.py", line 4, in <module>
    app = create_app()
  File "/Users/luyifei/PycharmProjects/pinstreet_open_service_backend/app/__init__.py", line 75, in create_app
    add_authentication_middleware(app)
  File "/Users/luyifei/PycharmProjects/pinstreet_open_service_backend/app/__init__.py", line 107, in add_authentication_middleware
    settings.CASBIN_REDIS_WATCHER_HOST, settings.CASBIN_REDIS_WATCHER_PORT
  File "/Users/luyifei/PycharmProjects/pinstreet_open_service_backend/.venv/lib/python3.7/site-packages/casbin_redis_watcher/watcher.py", line 44, in __init__
    self.create_subscriber_process(start_process)
  File "/Users/luyifei/PycharmProjects/pinstreet_open_service_backend/.venv/lib/python3.7/site-packages/casbin_redis_watcher/watcher.py", line 52, in create_subscriber_process
    p.start()
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/process.py", line 112, in start
    self._popen = self._Popen(self)
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/context.py", line 223, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/context.py", line 284, in _Popen
    return Popen(process_obj)
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/popen_spawn_posix.py", line 32, in __init__
    super().__init__(process_obj)
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/popen_fork.py", line 20, in __init__
    self._launch(process_obj)
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/popen_spawn_posix.py", line 42, in _launch
    prep_data = spawn.get_preparation_data(process_obj._name)
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/spawn.py", line 143, in get_preparation_data
    _check_not_importing_main()
  File "/Users/luyifei/.pyenv/versions/3.7.9/lib/python3.7/multiprocessing/spawn.py", line 136, in _check_not_importing_main
    is not going to be frozen to produce an executable.''')
RuntimeError: 
        An attempt has been made to start a new process before the
        current process has finished its bootstrapping phase.

        This probably means that you are not using fork to start your
        child processes and you have forgotten to use the proper idiom
        in the main module:

            if __name__ == '__main__':
                freeze_support()
                ...

        The "freeze_support()" line can be omitted if the program
        is not going to be frozen to produce an executable.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.