I would like to use the flask-htpasswd but I need to authenticate against custom htpasswd file. My use case is that for specific endpoint, for instance http://localhost:5000/api/tenant it will look up the tenant's directory and authenticates using his htpasswd if there is one.
I am wondering how to perform that. I am not that deep into Python, tough currently digging into it.
My solution was to take what exists and adapt it to my specific use case (inherit in Python):
from flask_htpasswd import HtPasswdAuth
from passlib.apache import HtpasswdFile
from crypt import crypt
import logging
import os
import messages as msg
import settings
from functools import wraps
log = logging.getLogger(__name__) # pylint: disable=invalid-name
class serviceHtPasswdAuth(HtPasswdAuth):
def __init__(self, app):
super().__init__(app)
self.users = None
def required(self, func):
"""
Decorator function with basic and token authentication handler
"""
#wraps(func)
def decorated(*args, **kwargs):
"""
Actual wrapper to run the auth checks.
"""
# here an htpasswd file for that specific tenant id is read
try:
self.users = HtpasswdFile(path=os.path.join(settings.TENANTS_DIRECTORY, kwargs['tenant_id'], 'htpasswd'))
except(FileNotFoundError) as error:
log.error(msg.MISSING_TENANT_HTPASSWD_FILE)
return {"error": msg.MISSING_TENANT_HTPASSWD_FILE}, 404
is_valid, user = self.authenticate()
if not is_valid:
return self.auth_failed()
kwargs['user'] = user
return func(*args, **kwargs)
return decorated
Related
While developing an API using python based azure function. I have come across a situation where I have to implement custom JWT authentication and authorization middleware.
I have tried to use python worker extension pre_invocation_app_level method. However, I am not able to pass the verification result to the endpoint or return from the middleware in case of invalid user.
Sample source code:
from azure.functions import AppExtensionBase, Context
import typing
from logging import Logger
import azure.functions as func
class AuthMiddleware(AppExtensionBase):
#classmethod
def pre_invocation_app_level(
cls,
logger: Logger,
context: Context,
func_args: typing.Dict[str, object] = ...,
*args,
**kwargs,
) -> None:
req = func_args.get("req")
auth_header = req.headers.get("Authorization")
if not auth_header:
return func.HttpResponse(
"Unauthorized", status_code=401, headers={"WWW-Authenticate": "Custom"}
)
I have a decorator which adds a user onto the flask global context g:
class User:
def __init__(self, user_data) -> None:
self.username: str = user_data["username"]
self.email: str = user_data["email"]
def login_required(f):
#wraps(f)
def wrap(*args, **kwargs):
user_data = get_user_data()
user = User(user_data)
g.user = User(user_data)
return f(*args, **kwargs)
return wrap
I want the type (User) of g.user to be known when I access g.user in the controllers. How can I achieve this? (I am using pyright)
I had a similar issue described in Typechecking dynamically added attributes. One solution is to add the custom type hints using typing.TYPE_CHECKING:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from flask.ctx import _AppCtxGlobals
class MyGlobals(_AppCtxGlobals):
user: 'User'
g = MyGlobals()
else:
from flask import g
Now e.g.
reveal_type(g.user)
will emit
note: Revealed type is 'myapp.User'
If the custom types should be reused in multiple modules, you can introduce a partial stub for flask. The location of the stubs is dependent on the type checker, e.g. mypy reads custom stubs from the MYPY_PATH environment variable, pyright looks for a typings directory in the project root dir etc. Example of a partial stub:
# _typeshed/flask/__init__.pyi
from typing import Any
from flask.ctx import _AppCtxGlobals
from models import User
def __getattr__(name: str) -> Any: ... # incomplete
class MyGlobals(_AppCtxGlobals):
user: User
def __getattr__(self, name: str) -> Any: ... # incomplete
g: MyGlobals
This is a solution with an opinion:
flask.g is magic and is tied really hard to the server implementation. IMO, its usage should be kept contained and minimal.
I have created a file to manage g, which allowed me to type it
# request_context.py
from flask import g
from somewhere import User
def set_user(user: User) -> None:
g.user = user
def get_user() -> User:
# you could validate here that the user exists
return g.user
and then in your code:
# yourcode.py
import request_context
class User:
...
def login_required(f):
#wraps(f)
def wrap(*args, **kwargs):
user_data = get_user_data()
user = User(user_data)
request_context.set_user(User(user_data))
return f(*args, **kwargs)
return wrap
You could proxy the g object. Consider the following implementation:
import flask
class User:
...
class _g:
user: User
# Add type hints for other attributes
# ...
def __getattr__(self, key):
return getattr(flask.g, key)
g = _g()
You can annotate an attribute on a class, even if that class isn't yours, simply with a colon after it. For example:
g.user: User
That's it. Since it's presumably valid everywhere, I would put it at the top of your code:
from functools import wraps
from flask import Flask, g
app = Flask(__name__)
class User:
def __init__(self, user_data) -> None:
self.username: str = user_data["username"]
self.email: str = user_data["email"]
# Annotate the g.user attribute
g.user: User
def login_required(f):
#wraps(f)
def wrap(*args, **kwargs):
g.user = User({'username': 'wile-e-coyote',
'email': 'coyote#localhost'})
return f(*args, **kwargs)
return wrap
#app.route('/')
#login_required
def hello_world():
return f'Hello, {g.user.email}'
if __name__ == '__main__':
app.run()
That's it.
Problem:
So my problem is I have a Flask microservice want to implement the unit tests to it so when I start writing my test cases I found that I need to authenticate the unit test client because of some endpoints need authorization and here comes the problem the whole authentication system in another service this service all can do about the authentication is to validate the JWT token and get user ID from it so here is one of the views.py
from flask_restful import Resource
from common.decorators import authorize
class PointsView(Resource):
decorators = [authorize]
def get(self, user):
result = {"points": user.active_points}
return result
and authorize decorator from decorators.py
import flask
import jwt
from jwt.exceptions import DecodeError, InvalidSignatureError
from functools import wraps
from flask import request
from flask import current_app as app
from app import db
from common.models import User
from common.utils import generate_error_response
def authorize(f):
"""This decorator for validate the logged in user """
#wraps(f)
def decorated_function(*args, **kwargs):
if 'Authorization' not in request.headers:
return "Unable to log in with provided credentials.", 403
raw_token = request.headers.get('Authorization')
if raw_token[0:3] != 'JWT':
return generate_error_response("Unable to log in with provided credentials.", 403)
token = str.replace(str(raw_token), 'JWT ', '')
try:
data = jwt_decode_handler(token)
except (DecodeError, InvalidSignatureError):
return generate_error_response("Unable to log in with provided credentials.", 403)
user = User.query.filter_by(id=int(data['user_id'])).first()
return f(user, *args, **kwargs)
return decorated_function
and the test case from tests.py
import unittest
from app import create_app, db
from common.models import User
class TestMixin(object):
"""
Methods to help all or most Test Cases
"""
def __init__(self):
self.user = None
""" User Fixture for testing """
def user_test_setup(self):
self.user = User(
username="user1",
active_points=0
)
db.session.add(self.user)
db.session.commit()
def user_test_teardown(self):
db.session.query(User).delete()
db.session.commit()
class PointsTestCase(unittest.TestCase, TestMixin):
"""This class represents the points test case"""
def setUp(self):
"""Define test variables and initialize app."""
self.app = create_app("testing")
self.client = self.app.test_client
with self.app.app_context():
self.user_test_setup()
def test_get_points(self):
"""Test API can create a points (GET request)"""
res = self.client().get('/user/points/')
self.assertEqual(res.status_code, 200)
self.assertEquals(res.data, {"active_points": 0})
def tearDown(self):
with self.app.app_context():
self.user_test_teardown()
# Make the tests conveniently executable
if __name__ == "__main__":
unittest.main()
My authentication system work as the following:
Any service (include this one) request User service to get user JWT
token
Any service take the JWT token decoded and get the user ID
from it
Get the user object from the database by his ID
so I didn't know how to make the authentication flow in the test cases.
Here is just an example. I skipped some little things such as create_app, jwt.decode(token) etc. I'm sure you can understand the main approach. Structure:
src
├── __init__.py # empty
├── app.py
└── auth_example.py
app.py:
from flask import Flask
from src.auth_example import current_identity, authorize
app = Flask(__name__)
#app.route('/')
#authorize()
def main():
"""
You can use flask_restful - doesn't matter
Do here all what you need:
user = User.query.filter_by(id=int(current_identity['user_id'])).first()
etc..
just demo - return current user_id
"""
return current_identity['user_id']
auth_example.py:
from flask import request, _request_ctx_stack
from functools import wraps
from werkzeug.local import LocalProxy
current_identity = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_identity', None))
def jwt_decode_handler(token):
"""
just do here all what you need. Should return current user data
:param str token:
:return: dict
"""
# return jwt.decode(token), but now - just demo
raise Exception('just demo')
def authorize():
def _authorize(f):
#wraps(f)
def __authorize(*args, **kwargs):
if 'Authorization' not in request.headers:
return "Unable to log in with provided credentials.", 403
raw_token = request.headers.get('Authorization')
if raw_token[0:3] != 'JWT':
return "Unable to log in with provided credentials.", 403
token = str.replace(str(raw_token), 'JWT ', '')
try:
# I don't know do you use Flask-JWT or not
# this is doesn't matter - all what you need is just to mock jwt_decode_handler result
_request_ctx_stack.top.current_identity = jwt_decode_handler(token)
except Exception:
return "Unable to log in with provided credentials.", 403
return f(*args, **kwargs)
return __authorize
return _authorize
Our test:
import unittest
from mock import patch
from src.app import app
app.app_context().push()
class TestExample(unittest.TestCase):
def test_main_403(self):
# just a demo that #authorize works fine
result = app.test_client().get('/')
self.assertEqual(result.status_code, 403)
def test_main_ok(self):
expected = '1'
# we say that jwt_decode_handler will return {'user_id': '1'}
patcher = patch('src.auth_example.jwt_decode_handler', return_value={'user_id': expected})
patcher.start()
result = app.test_client().get(
'/',
# send a header to skip errors in the __authorize
headers={
'Authorization': 'JWT=blabla',
},
)
# as you can see current_identity['user_id'] is '1' (so, it was mocked in view)
self.assertEqual(result.data, expected)
patcher.stop()
So, in your case you need just mock jwt_decode_handler. Also I recommend do not add any additional arguments inside a decorators. It will be hard to debugging when you have more than two decorators with a different arguments, recursion, hard processing etc.
Hope this helps.
Could you create some mock tokens in your unit testing framework (that your decorator can actually decode like in a real request) and send them in with your test client? An example of how that might look can be seen here: https://github.com/vimalloc/flask-jwt-extended/blob/master/tests/test_view_decorators.py#L321
I've seen the posts on passing GET parameters and hardcoded parameters here and here.
What I am trying to do is pass POST parameters to a custom decorator. The route is not actually rendering a page but rather processing some stuff and sending the results back through an AJAX call.
The decorator looks like this:
# app/util.py
from functools import wraps
from models import data
# custom decorator to validate symbol
def symbol_valid():
def decorator(func):
#wraps(func)
def decorated_function(symbol, *args, **kwargs):
if not data.validate_symbol(symbol):
return jsonify({'status': 'fail'})
return func(*args, **kwargs)
return decorated_function
return decorator
The view looks something like this:
# app/views/matrix_blueprint.py
from flask import Blueprint, request, jsonify
from ..models import data
from ..util import symbol_valid
matrix_blueprint = Blueprint('matrix_blueprint', __name__)
# routing for the ajax call to return symbol details
#matrix_blueprint.route('/route_line', methods=['POST'])
#symbol_valid
def route_line():
symbol = request.form['symbol'].upper()
result = data.get_information(symbol)
return jsonify(**result)
I understand that I can actually call #symbol_valid() when I pass a parameter through GET like this /quote_line/<symbol> but I need to POST.
The question then is how can my decorator access the POSTed variable?
Simple solution. Imported Flask's request module into the util.py module which contains the decorator. Removed the outer function as well.
See code:
# app/util.py
from flask import request # <- added
from functools import wraps
from models import data
# custom decorator to validate symbol
def symbol_valid(func):
#wraps(func)
def decorated_function(*args, **kwargs): # <- removed symbol arg
symbol = request.form['symbol'] # <- paramter is in the request object
if not data.validate_symbol(symbol):
return jsonify({'status': 'fail'})
return func(*args, **kwargs)
return symbol_valid
The decorator accept a func parameter. You must use your decorator like #symbol_valid() or make the function symbol_valid accept a func parameter.
If you are doing it right, you can access the request object anywhere during the request cycle. It just works.
I would like to have a nose plugin that if enabled will just generate an HTML page with all the tests run which contain also the request/responses for each test.
This would be useful to generate some documentation FROM the tests.
Now I don't think it's too hard, but to do that I guess I have to intercept the self.client.get/post/whatever methods.
To do that however it looks like I have to replace the default TestCase class with something else, and I would like to not force any project that wants to use this tool to do have to do that.
This kind of works for example
import json
from mock import patch
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import Client
class DebuggingClient(Client):
def __init__(self, *args, **kwargs):
super(DebuggingClient, self).__init__(*args, **kwargs)
def get(self, url, *args, **kwargs):
print("Calling endpoint {} with args {} and kwargs {}".format(url, args, kwargs))
response = super(DebuggingClient, self).get(url, *args, **kwargs)
print("Obtained response {}:{}".format(response.status_code, response.content))
return response
class TestSimpleApi(TestCase):
# TODO: how do I make this happen inside the plugin??
def _pre_setup(self):
super(TestSimpleApi, self)._pre_setup()
self.client = DebuggingClient()
def test_getting_numbers_returns_list_of_first_numbers(self):
url = reverse('gen_numbers')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertEqual(data, list(range(10)))
But I would like do do that just depending on a nose-plugin enabled.
Any idea how? I can't find a way to change the base class just from a nose-plugin (unless maybe doing some crazy monkey patching).
The project is here by the way even if far from working yet..
https://github.com/AndreaCrotti/django-docs-from-tests