So far I am using colander to validate the data in my aiohttp application.
The problem I face is that I don't know how to do "deep" validation.
Given the following schema:
import colander
class User(colander.MappingSchema):
username = colander.SchemaNode(colander.String())
password = colander.SchemaNode(colander.String())
confirmation = colander.SchemaNode(colander.String())
I do both validate that the input datastructure has all the required fields are there (constraints are minimal for the sake of clarity) but I also need
to check that:
username is not already taken by another user
password and confirmation are the same
So in my controllers, the code looks like the following pseudo code:
def create_user(request):
user = await request.json()
schema = User()
# do schema validation
try:
user = schema.deserialize(user)
except colander.Invalid, exc:
response = dict(
status='error',
errors=errors.asdict()
)
return json_response(response)
else:
# check password and confirmation are the same
if user['password'] != user['confirmation']:
response = dict(
status='error'
errors=dict(confirmation="doesn't match password")
)
return json_response(response)
# check the user is not already used by another user
# we want usernames to be unique
if user_exists(user['user']):
response = dict(
status='error',
errors=dict(username='Choose another username')
)
return json_response(response)
return json_response(dict(status='ok'))
Basically there is two kinds of validation. Is it possible to have both logic in single colander schema? Is it a good pattern?
Obviously it's a matter of taste but IMHO it's better to keep data validation separate from application logic.
You'll also run into a few problems trying to confirm that the username is unique:
Colander would need to have knowledge about your application eg. get access to the database connection to check with the database that that username doesn't exist.
Colander (AFAIK) isn't setup for asyncio programming so it'll have problems coping with an async method which checks the user exists.
You really want to user creation to be ACID, so simultaneous calls to create_user with the same username cannot possibly create two users with the same username.
Checking passwords match is another story, that doesn't require any knowledge about the rest of the world and should fairly trivial with colander. I'm not expert on colander, but it looks like you can use a deferred validator to check the two passwords match.
A few other notes on your code:
create_user should be an async method
I don't know anything about your db, but to get any advantage from async programming user_exists should be async too
The user existence check should be wrapped into the ACID user creation. Eg. you should use postgres's on conflict or equivalent to catch a duplicate user as you create them rather than checking they exist first
To be properly restful and make testing easier your view should return the correct http response code on an error (currently you return 200 for all states). You should use 201 for created, 400 for invalid date and 409 or a username conflict.
Related
I want to write a unittest for this method in Django.
def activate(request):
id = int(request.GET.get('id'))
user = User.objects.get(id=id)
user.is_active = True
user.save()
return render(request, 'user_authentication/activation.html')
I wrote sth like this:
def test_activate_view(self):
response = self.client.get('/activation', follow=True)
self.assertTemplateUsed(response, 'user_authentication/activation.html')
It doesn't work because I get an error:
id = int(request.GET.get('id'))
TypeError: int() argument must be a string or a number, not 'NoneType':
What should I change in my test ?
Your view reads data from request.GET - you need to pass this data:
response = self.client.get('/activation?id=1', follow=True)
You also fetch this user afterwards from your database. Therefor you need to load some fixture data. Create a fixture using manage.py dumpdata and load it in your unit test like that:
class UserTestCase(TestCase):
fixtures = ['fixture.json', 'users']
Read the docs about loading fixtures for detailed explanations.
Note regarding your approach
You should not use the users id for this use case. One can easily guess this id and activate accounts.
Someone might register once with a valid email address, receive your link with the id inside and can afterwards create a bunch of accounts without providing a valid email address.
Instead you might generate a unique and random secret (aka token) and associate this token with the user. Your view should accept those tokens and resolve the user based on it. This way one can no longer easily activate.
For my site for auth I'm using https://flask-httpauth.readthedocs.io/en/latest/ . Now I'm trying to make it that it's using data from database. To do that i created database named Users and created columns named username and password.
To get data from this table after defining its class as model I've made get_user functions which looks like it:
#staticmethod
def get_user():
query = (Users
.select())
user = []
for s in query:
user.append(s)
return user
(I'm not sure if it's correct)
Next I had to modify get_pw function but I also wasn't sure how to modify it so I made it look like it:
#auth.get_password
def get_pw(username):
if username in Users.get_user():
return users.get(Users.get_user())
return None
Now after running the site I get prompt to give login and password but those that I set up in my database doesn't seem to work so there must be a problem with get_pw function. Also I'm using peewee SQL to manage database : http://docs.peewee-orm.com/en/latest/peewee/querying.html
You can get rid of your get_user method since you are issuing a very large select query that fetches all records from user table. The get_pw can be redefined as:
def get_pw(username):
user = Users.get(Users.name == username) #assuming you have a username field in model
return user.password #assuming a password field
Also, its a good practice to define your model class as a singular noun rather than plural. So, its better to call it User rather than Users.
This'll help you get started in no time: http://docs.peewee-orm.com/en/latest/peewee/quickstart.html#quickstart
I'm trying to modify this existing app (miguelgrinberg/microblog) to utilize Github-flask login (cenkalti/github-flask). It uses Flask-Login to manage the user sessions (maxcountryman/flask-login).
TraceBack: https://gist.github.com/CoinGame/4a6b14d0213850b29979
Views.py: https://gist.github.com/CoinGame/22d8098ccabc255bd5cf
The login process seems to work. I'm getting the github token which lets me make API calls for the user object. Though when I try to put all the pieces together I get this flask breaking error.
I'll admit I'm somewhat new to python and totally trying to work my way from scratch through flask and its extensions. Working on an existing project has helped me understand the framework, but I feel it may also be causing me to overlook things.
Any idea what I'm missing here to get this error after the user logs in?
the id you are passing to this function
def load_user(id):
return User.query.get(int(id))
is causing the issue. It looks like it doesnt exist or it may contain chars that arent numeric. Somewhere along the line the user_id seems to be getting lost/changed and throwing the error.
File "/root/votr-blog/app/views.py", line 21, in load_user
return User.query.get(int(id))
ValueError: invalid literal for int() with base 10: 'None'
If you can traceback where the id gets lost it might help.
You could also wrap the call to int() using try/catch
try:
int_val = int(id)
except ValueError:
print("Failure w/ value " + id)
Looking into flask-github and this code
#app.route('/github-callback')
#github.authorized_handler
def authorized(oauth_token):
next_url = request.args.get('next') or url_for('index')
if oauth_token is None:
flash("Authorization failed.")
return redirect(next_url)
#this code below is getting the user by their github access token
user = User.query.filter_by(github_access_token=oauth_token).first()
if user is None:
user = User(oauth_token)
db_session.add(user)
user.github_access_token = oauth_token
db_session.commit()
return redirect(next_url)
authorises and returns users in relation to their github access token rather than a user_id, this seems to be in conflict with login_manager with returns users by id. Your issue seems to be that the two methods are trying to do the same this.
Flask-login uses user_ids to manage sessions and users within the session. A user logs in and if authenticated that users id is saved to the session. This is used to reload the user object from the user ID stored in the session.
Github-flask uses tokens to manage users. It saves the token in the session by the looks of things. The two are conflicting.
You could do a couple of things:
1) Choose one or the other to manage users.
2) Once a user has been authenticated with github-flask, get that users id and save that to the session, so that the load_user() function has something to work with. This seems contradictory though, getting the user with github-flask just so as to get the users id so you can get the user again with flask-login.
My Django application uses the Rest Framework JWT for authentication. It works great and very elegant.
However, I have a use-case which I am struggling to build. I have already coded up a working solution for the "Forgot Password" workflow. I allow an un-authenticated user to reset their password if-and-only-if they click on a secret link that I send to their email address. However, I would like to modify this solution such that after the password-reset workflow is successfully completed, the user is automatically logged in without having to retype their username and (new) password. I would like to do this to make the user's experience as frictionless as possible.
The problem is I do not know how to make this work without having the user re-type their password (or storing it in clear-text in the DB which is obviously very bad). Below is the current way I get the JWT token. You can see that in line #12, I need the user's clear password. I don't have it. I only have the encrypted password stored in my_user.password.
How can I use the encrypted password in my_user.password instead of the clear password to obtain the JWT? If I cannot use it, then how is this workflow achieved using the Rest Framework JWT?
from rest_framework_jwt.views import ObtainJSONWebToken
from rest_framework status
from django.contrib.auth.models import User
my_user = User.objects.get(pk=1)
ojwt = ObtainJSONWebToken()
if "_mutable" in dir(request.DATA):
mutable = request.DATA._mutable
request.DATA._mutable = True
request.DATA['username'] = my_user.username
request.DATA['password'] = "<my_user's clear password>"
if "_mutable" in dir(request.DATA):
request.DATA._mutable = mutable
token_response = ojwt.post(request)
if status.is_success(token_response.status_code):
# Tell the user login succeeded!!
else:
# Tell the user login failed.
# But hopefully, this shouldn't happen
When working with Django REST Framework JWT, it is typically expected that the user is generating the token on their own. Because you are generating the token on behalf of the user, you can't use any of the standard views to make it work.
You are going to need to generate the token on your own, similar to how DRF JWT does it in the views. This means using something like the following for your view code
from rest_framework_jwt.settings import api_settings
from datetime import datetime
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
my_user = User.objects.get(pk=1) # replace with your existing logic
payload = jwt_payload_handler(my_user)
# Include original issued at time for a brand new token,
# to allow token refresh
if api_settings.JWT_ALLOW_REFRESH:
payload['orig_iat'] = timegm(
datetime.utcnow().utctimetuple()
)
return {
'token': jwt_encode_handler(payload)
}
This should allow you to manually generate the token within the view, without having to know the user's password.
Deform allows to add validation on different fields of a form. However, it checks that the form is valid in itself but that does not necessarily mean that the form processing will be valid.
For example, if the form is for creating a new user with an email address. The form is valid but the form processing (which consists in inserting this new user in a database) rises up a database integrity error saying there is already a user with this email address.
I know I could add a special validator that checks the email is not already used but still, there could be another concurrent transaction with the same email that commits between the check and the commit of the first transaction, which is not 100% safe at the end.
So, how can I nicely report form post-processing errors to the user?
I could easily report the error messages next to the form (flash message or other) but I would like to know if there is a way to report the error directly in the widgets exactly like normal validation errors are handled.
I faced the same situation and this how i achieve to raise the error as normal validation error.
Validator method:
def user_DoesExist(node,appstruct):
if DBSession.query(User).filter_by(username=appstruct['username']).count() > 0:
raise colander.Invalid(node, 'Username already exist.!!')
Schema:
class UserSchema(CSRFSchema):
username = colander.SchemaNode(colander.String(),
description="Extension of the user")
name = colander.SchemaNode(colander.String(),
description='Full name')
extension = colander.SchemaNode(colander.String(),
description='Extension')
pin = colander.SchemaNode(colander.String(),
description='PIN')
View:
#view_config(route_name='add_user', permission='admin', renderer='add_user.mako')
def add_user(self):
schema = UserSchema(validator = user_DoesExist).bind(request=self.request)
form = deform.Form(schema, action=self.request.route_url('add_user'), buttons=('Add User','Cancel'))
if 'Cancel' in self.request.params:
return HTTPFound(location = self.request.route_url('home'))
if 'Add_User' in self.request.params:
appstruct = None
try:
appstruct = form.validate(self.request.POST.items())
except deform.ValidationFailure, e:
log.exception('in form validated')
return {'form':e.render()}
Hope this will help you.
Thanks.