Is there a recommended way to test the security setup in a Pyramid application? More specifically I'm using routes and custom routes factories. With fine grained ACLs the security setup is splitted in different spots: the config setup, factories, permission set in the #view_config, and event explicit check of permissions inside views.
The page on unit and functionnal testing (http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/testing.html) does not seem to indicate a way to test if user A can only see and modify data he is allowed to.
This is functional testing. Webtest can preserve session cookies so you can use it to login and visit various pages as a user.
myapp = pyramid.paster.get_app('testing.ini')
app = TestApp(myapp)
resp = app.post('/login', params={'login': 'foo', 'password': 'seekrit'})
# this may be a redirect in which case you may want to follow it
resp = app.get('/protected/resource')
assert resp.status_code == 200
As far as testing just certain parts of your app, you can override the authentication policy with something custom (or just use a custom groupfinder).
def make_test_groupfinder(principals=None):
def _groupfinder(u, r):
return principals
return _groupfinder
You can then use this function to simulate various principals. This doesn't handle the userid though, if your app also relies on authenticated_userid(request) anywhere. For that, you'll have to replace the authentication policy with a dummy one.
class DummyAuthenticationPolicy(object):
def __init__(self, userid, extra_principals=()):
self.userid = userid
self.extra_principals = extra_principals
def authenticated_userid(self, request):
return self.userid
def effective_principals(self, request):
principals = [Everyone]
if self.userid:
principals += [Authenticated]
principals += list(self.extra_principals)
return principals
def remember(self, request, userid, **kw):
return []
def forget(self, request):
return []
I think both the question and answer might be old at this point: with current versions of Pyramid, there's a testing_securitypolicy method (docs here) that allows easy access to setting things like authenticated_userid, effective_principals, results of remember and forget, etc.
Here's an example of usage if need was to set authenticated_userid on a request.
from pyramid.testing import (setUp, tearDown, DummyRequest)
def test_some_view():
config = setUp()
config.testing_securitypolicy(userid='mock_user') # Sets authenticated_userid
dummy_request = DummyRequest()
print(dummy_request.authenticated_userid) # Prints 'mock_user'
# Now ready to test something that uses request.authenticated_userid
from mypyramidapp.views.secure import some_auth_view
result = some_auth_view(dummy_request)
expected = 'Hello mock_user!'
assert result == expected
# Finally, to avoid security changes leaking to other tests, use tearDown
tearDown() # Undo the effects of pyramid.testing.setUp()
Related
class User(BaseModel):
name: str
token: str
fake_db = [
User(name='foo', token='a1'),
User(name='bar', token='a2')
]
async def get_user_by_token(token: str = Header()):
for user in fake_db:
if user.token == token:
return user
else:
raise HTTPException(status_code=401, detail='Invalid token')
#router.get(path='/test_a', summary='Test route A')
async def test_route_a(user: User = Depends(get_user_by_token)):
return {'name': user.name}
#router.get(path='/test_b', summary='Test route B')
async def test_route_a(user: User = Depends(get_user_by_token)):
return {'name': user.name}
I would like to avoid code duplication. Is it possible to somehow set the line user: User = Depends(get_user_by_token) for the entire router? At the same time, I need the user object to be available in each method.
It is very important that the openapi says that you need to specify a header with a token for the method.
You can use the dependencies parameter to add global dependencies when creating the router instance:
router = APIRouter(dependencies=[Depends(get_user_by_token)])
or, when adding the router to the app instance:
app.include_router(router, dependencies=[Depends(get_user_by_token)])
Please have a look at FastAPI's documentation on Dependencies for more details.
As for getting the return value of a global dependency, you can't really do that. The way around this issue is to store the returned value to request.state (as described here), which is used to store arbitrary state (see the implementation of State as well). Hence, you could have something like this:
def get_user_by_token(request: Request, token: str = Header()):
for user in fake_db:
if user.token == token:
request.state.user = user
# ...
Then, inside your endpoint, you could retrieve the user object using request.state.user, as described in this answer.
I'm using Django 3.2 and djangorestframework==3.12.2. I recently added this to my settings file because I want to add some secured endpoints to my application ...
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.IsAdminUser',
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
)
}
JWT_AUTH = {
# how long the original token is valid for
'JWT_EXPIRATION_DELTA': datetime.timedelta(hours=1),
}
However, this seems to have caused all my endpoints to require authentication. For example, I had this view set up in my views.py file
class CoopList(APIView):
"""
List all coops, or create a new coop.
"""
def get(self, request, format=None):
contains = request.GET.get("contains", "")
if contains:
coops = Coop.objects.find(
partial_name=contains,
enabled=True
)
else:
partial_name = request.GET.get("name", "")
enabled_req_param = request.GET.get("enabled", None)
enabled = enabled_req_param.lower() == "true" if enabled_req_param else None
city = request.GET.get("city", None)
zip = request.GET.get("zip", None)
street = request.GET.get("street", None)
state = request.GET.get("state", None)
coop_types = request.GET.get("coop_type", None)
types_arr = coop_types.split(",") if coop_types else None
coops = Coop.objects.find(
partial_name=partial_name,
enabled=enabled,
street=street,
city=city,
zip=zip,
state_abbrev=state,
types_arr=types_arr
)
serializer = CoopSearchSerializer(coops, many=True)
return Response(serializer.data)
accessible in my urls.py file using
path('coops/', views.CoopList.as_view()),
But now when I try and call that I get the below response
{"detail":"Authentication credentials were not provided."}
I only want certain views/endpoints secured. How do I make the default that all views are accessible and only specify some views/endpoints to be validated using a provided JWT?
'DEFAULT_PERMISSION_CLASSES' is conventiently applied to all views, unless manually overridden. In your case both listed permissions require the user to be authenticated. FYI, the list is evaluated in an OR fashion.
If you want to allow everyone by default and only tighten down specific views, you want to set
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.AllowAny']
which does not require the user to be authenticated. Then set more strict permissions explicitly on the view (e.g. permissions_classes = [IsAuthenticated]) The DEFAULT_AUTHENTICATION_CLASS can stay as is.
NOTE: It is generally advisable to do it the other way round. It's very easy to accidentally expose an unsecured endpoint like this and potentially create a security breach in your API. The default should be secure and then exceptions should be be manually lifted.
Set the below configuration in settings.py
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
For class based views you can set permission class to empty list.
class CoopList(APIView):
permission_classes = []
def get(self, request, format=None):
pass
For Function based views add the decorator #permission_classes
from rest_framework.decorators import permission_classes
#permission_classes([])
def CoopList(request, format=None):
pass
I am following the tutorial by http://www.patricksoftwareblog.com/flask-tutorial/, which I believe is based on https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world. Great stuff for a beginner.
I am getting different results when testing my code through frontend manually (which works fine) v.s. through pytest.
My test tries to show the "groups" endpoint which requires a login (standard #login_required decorator).
I initially test the user getting a login page ("Knock knock") when trying to get the endpoint without a login. This works manually and through pytest.
I login a user. If I inspect the response from the login I can clearly see a "Welcome back Pete!" success message.
My second assert receives a response from URL /login?next=%2Fgroups indicating the /groups endpoint is called without a login/authentication preceding it and the assert fails. Testing this manually works as expected. Why is that single test not using the same user/session combination in the next step(s)?
Test with the problem is the first snippet below:
def test_groups(app):
assert b'Knock knock' in get(app, "/groups").data
login(app, "pete#testmail.com", "pete123")
assert b'Test group 1' in get(app, "/groups").data
My "get" function for reference:
def get(app, endpoint: str):
return app.test_client().get(endpoint, follow_redirects=True)
My "login" function for reference:
def login(app, email="testuser#testmail.com", password="testing"):
return app.test_client().post('/login', data=dict(email=email, password=password), follow_redirects=True)
The app (from a conftest fixture imported in the test module by #pytest.mark.usefixtures('app')) for reference:
#pytest.fixture
def app():
"""An application for the tests."""
_app = create_app(DevConfig)
ctx = _app.test_request_context()
ctx.push()
yield _app
ctx.pop()
The login route for reference:
#app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm(request.form)
if request.method == 'POST':
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.is_correct_password(form.password.data):
user.authenticated = True
user.last_login = user.current_login
user.current_login = datetime.now()
user.insert_user()
login_user(user)
flash(f'Welcome back {user.name}!', 'success')
return redirect(url_for('our_awesome_group.index'))
else:
flash('Incorrect credentials! Did you already register?', 'error')
else:
flash_errors(form)
return render_template('login.html', form=form)
The groups route for reference:
#app.route('/groups')
#login_required
def groups():
groups_and_users = dict()
my_group_uuids = Membership.list_groups_per_user(current_user)
my_groups = [Group.query.filter_by(uuid=group).first() for group in my_group_uuids]
for group in my_groups:
user_uuids_in_group = Membership.list_users_per_group(group)
users_in_group = [User.query.filter_by(uuid=user).first() for user in user_uuids_in_group]
groups_and_users[group] = users_in_group
return render_template('groups.html', groups_and_users=groups_and_users)
Im going to sum up the comments I made that gave the answer on how to solve this issue.
When creating a test app using Pytest and Flask there are a few different ways to go about it.
The suggested way to create a test client with proper app context is to use something like:
#pytest.fixture
def client():
""" Creates the app from testconfig, activates test client and context then makes the db and allows the test client
to be used """
app = create_app(TestConfig)
client = app.test_client()
ctx = app.app_context()
ctx.push()
db.create_all()
yield client
db.session.close()
db.drop_all()
ctx.pop()
That creates the client while pushing the app context so you can register things like your database and create the tables to the test client.
The second way is show in OP's question where use app.test_request context
#pytest.fixture
def app():
"""An application for the tests."""
_app = create_app(DevConfig)
ctx = _app.test_request_context()
ctx.push()
yield _app
ctx.pop()
and then create the test client in another pytest fixture
#pytest.fixture
def client(app):
return app.test_client()
Creating a test client allows you to use various testing features and gives access to flask requests with the proper app context.
I need to connect to azure service bus using SAS token(generate and connect).
I don't see anything for the python implementation.
This link provides the implementation for Eventhubs -
https://learn.microsoft.com/en-us/rest/api/eventhub/generate-sas-token#python
Not sure where I can find the python implementation for servicebus.
I have found a way where you can do it for the ServiceBusService Class.
After running "pip install azure.servicebus", I imported it as:
from azure.servicebus.control_client import ServiceBusService
The ServiceBusService constructor takes an argument called "authentication", which isn't specified by default.
If you got into the ServiceBusService init file, you can see how authentication is handled in more detail.
if authentication:
self.authentication = authentication
else:
if not account_key:
account_key = os.environ.get(AZURE_SERVICEBUS_ACCESS_KEY)
if not issuer:
issuer = os.environ.get(AZURE_SERVICEBUS_ISSUER)
if shared_access_key_name and shared_access_key_value:
self.authentication = ServiceBusSASAuthentication(
shared_access_key_name,
shared_access_key_value)
elif account_key and issuer:
self.authentication = ServiceBusWrapTokenAuthentication(
account_key,
issuer)
If you don't pass a custom authentication object, it will then try to use the ServiceBusSASAuthentication class, which is the default if you populate the shared_access_key_name and shared_access_key_value.
So if you jump into the ServiceBusSASAuthentication class, you'll notice something useful.
class ServiceBusSASAuthentication:
def __init__(self, key_name, key_value):
self.key_name = key_name
self.key_value = key_value
self.account_key = None
self.issuer = None
def sign_request(self, request, httpclient):
request.headers.append(
('Authorization', self._get_authorization(request, httpclient)))
def _get_authorization(self, request, httpclient):
uri = httpclient.get_uri(request)
uri = url_quote(uri, '').lower()
expiry = str(self._get_expiry())
to_sign = uri + '\n' + expiry
signature = url_quote(_sign_string(self.key_value, to_sign, False), '')
auth_format = 'SharedAccessSignature sig={0}&se={1}&skn={2}&sr={3}' # <----awww, yeah
auth = auth_format.format(signature, expiry, self.key_name, uri)
return auth # <--after inserting values into string, the SAS Token is just returned.
def _get_expiry(self): # pylint: disable=no-self-use
'''Returns the UTC datetime, in seconds since Epoch, when this signed
request expires (5 minutes from now).'''
return int(round(time.time() + 300))
The sign_request function is the only function that will be directly referenced by the ServiceBusService class when it does authentication, but you'll notice that all its doing is adding an authentication header to a request that...IS IN THE FORMAT OF A SAS TOKEN.
So at this point I had all the information I needed to make my own authentication class. I made one that looked exactly like this.
class ServiceBusSASTokenAuthentication:
def __init__(self, sas_token):
self.sas_token = sas_token
# this method is the one used by ServiceBusService for authentication, need to leave signature as is
# even though we don't use httpClient like the original.
def sign_request(self, request, httpclient):
request.headers.append(
('Authorization', self._get_authorization())
)
def _get_authorization(self):
return self.sas_token
I probably could get rid of the _get_auth function all together, but I haven't polished everything up yet.
So now if you call this class like so in the ServiceBusService constructor with a valid SAS Token, it should work.
subscription_client = ServiceBusService(
authentication=ServiceBusSASTokenAuthentication(sas_token=sas_token),
service_namespace=service_namespace
)
Once you create the Service Bus using Azure Portal, ServiceBusService object enables you to work with queues.
Follow this document for more information on creating the queue, sending message to queue, receiving message from a queue using python to programmatically access the Service Bus.
I'm writing a flask app that streams content out to the user, and I'm trying to manipulate the db while this streaming is happening. Here's some example code (simplified):
def work_hard(obj):
yield 'About to do a lot of work...'
obj.status = do_a_lot_of_work_very_slowly()
yield obj.status
db.session.commit()
obj.more = more_slow_stuff()
yield obj.more
db.session.commit()
yield 'Hard work is done!'
#app.route('/log/<int:objid>/work_hard', methods=['POST'])
def perform_action(objid):
obj = MyModel.query.get(objid)
return Response(work_hard(obj), mimetype='text/html')
This code gives the error Instance <MyModel at 0x7f5555f046a0> is not bound to a Session; attribute refresh operation cannot proceed, but if I call db.session.commit() inside perform_action() instead of work_hard(), it works. Similarly, if I try to access flask's request instance, it works in perform_action() but not work_hard() (it complains that I'm trying to access the request outside of a request context).
I assume that these are both because work_hard() is executing after perform_action() has returned. Is is possible to somehow prolong the request context to include work_hard()? So far I've just been passing individual values from request to work_hard() and that worked to a point, but now I need to commit to the db and I'm not sure how to fix the db session here.
I can't just call db.session.commit() from perform_action(), I really do need to be able to make multiple updates in the db in real-time as the output is streaming to the HTTP client.
If this turns out not to be possible, my backup plan is to stream output from a subprocess, and then in the subprocess I'll connect to the db from there, but I'd prefer to do it all within the same process if possible.
Thanks!
You can use stream_with_context() to keep the context (and hence the session) around while the generator runs:
return Response(stream_with_context(work_hard(obj)), ...)
Access the session with application context: with app.app_context
def work_hard(obj):
yield 'About to do a lot of work...'
obj.status = "blub"
yield obj.status
with app.app_context():
db.session.commit()
obj.more = "fish"
yield obj.more
with app.app_context():
db.session.commit()
yield 'Hard work is done!'
#app.route('/log/<int:objid>/work_hard', methods=['POST'])
def perform_action(objid):
def perform_action(objid):
obj = MyModel.query.get(objid)
return Response(work_hard(obj), mimetype='text/html')