Models
Permission
class
GENERAL = 0x01
ADMINISTER = 0xff
Okay so here is a seemingly simple piece of code I really think is really cool! First of all we are setting up two enums here. But they are set to weird hexadecimal numbers 0x01 and 0xff. If you stick these into a hexadecimal -> decimal converter you'll find that they represent 1 and 255 respectively. But in binary they come out to 00000001 and 11111111 (8 ones). If we do a binary and (&) on these two numbers, we can actually get some unique properties from these. So if we do GENERAL & ADMINSTER, it will come out to the following
00000001
& 11111111
----------
00000001
We get back the exact same value as GENERAL! Similarly if we do ADMINSTER & GENERAL we get back GENERAL. This is useful for checking user roles and who is exactly who in this system. So we can create a method 'check(input, checker)' that will take an input hex to test and one to text against. We only need to do '(input & checker) == checker'. But there are some more interesting applications for this. Let us define, for example, a set of enums CAN_LIKE = 0x01, CAN_POST = 0x02, CAN_EDIT = 0x04 and CAN_REMOVE = 0x08. These are respectively in binary 00000001, 00000010, 00000100, 00001000. We can use binary OR (|) to create composite user permissions e.g. CAN_LIKE | CAN_POST | CAN_EDIT = 0x07 = 00000111 -> NEW_ROLE. We can run 'check(NEW_ROLE, CAN_LIKE)' or 'check(NEW_ROLE, CAN_POST)' or 'check(NEW_ROLE, CAN_EDIT)' and all of these will return True. For example NEW_ROLE & CAN_EDIT
00000111
& 00000001
----------
00000001 <- equivalent to CAN_EDIT enum
A function similar to the check described above can be found in as the 'can' method below in the User class. Moving on!
Role
class
The Role class instatiates Role model. This is used for the creation of users such as a general user and an administrator
COLUMN DEFINITIONS:
id
serves as the primary key (expects int).
name
is the name of the role itself (expects unique String len 64)
index
is the name of the index route for the route
default
is a T/F value that determines whether a new user created
has that permission or note (ref insert_roles()). This is indexed
meaning that a separate table has been created with default as the
first column and id as the second column. Default in this table
is sorted and a query for default performs a binary search rather
than a linear search (reduces search time complexity from O(N)
to
O(log n)
permissions
contains the permissions enum (see Permissions class)
users
is not a column but it sets up a database relation. This case
is a one-to-many relationship in that for ONE Role record, there are
MANY associated User objects. The backref
param specifies a
bi-directional relationship between the two tables in that there is
a new property on both a given Role and User object. E.g. Role.users
will refer to the User object (i.e. the user table). and User.role
(role being the string specified with backref) will refer to the
Role object. Lazy = dynamic specifies to return a Query object
instead of actually asking the relationship to load all of its child
elements upon creating the relationship. It is best practice to
include lazy=dynamic upon the establishment of a relationship.
Sub-note on lazy-dynamic and backref:
Currently, lazy-dynamic will make the User collection to be loaded in as a Query object (so not everything is loaded at once). Simiarly (as mentioned above), the User object can reference the Role object by doing User.role however, this uses the default collection loading behavior (i.e. load the entire collection at once). It is fine in this case since the amount of Roles in the Role collection will be much less than the amount of entries in the User collection. However, we can specify that User.role uses the lazy-dynamic loading scheme. Simply redefine users here to
users = db.relationship('User', backref=db.backref('role',
lazy='dynamic'), lazy='dynamic')
insert_roles() and SQLAlchemy Sessions
The staticmethod decorator specifies that insert_roles() must be be called with a instance of the Role class. E.g. role_obj.insert_roles() This method is fairly self-explanatory. It specifies a 'roles' dict This is then iterated through and foreach role in the 'roles' dict we check to see if it already exists (by name) in the Role object i.e. the Roles table. If not, then a new Role object is instantiated After that, the perms, index, default props are set and the the role object is now added to the db session and then committed.
A note about sqlalchemy if you haven't noticed already: All changes are added to a Session object (handled by SQLAlchemy). Unless specified otherwise, the session object has a merge operation that finds the difs between the new object (that was created and added to the session object) and the currently existing (corresponding) object existing in the table right now. Then a commit() propegates these changes into the database making as little changes as possible (i.e. every time we update a record, the record's attribute is changed 'in place' rather than being deleted and then replaced. Neat :)
__repr__
def __repr__(self):
return '<Role \'%s\'>' % self.name
this repr method is pretty much optional, but it is helpful in that it will allow the program to pretty print the user object when you come across an error
User
Model
The class User represents users... it extends db.Model and UserMixin. Per the flask-login documentation, the User class needs to implement is_authenticated (returns True if the user is authenticated and in turn fulfill login_required), is_active (returns True if the user has been activated i.e. confirmed by email in our case), is_anonymous (returns if a user is Anonymous i.e. is_active = is_authenticated = False, is_anonymous = True, and get_id() = None), get_id() (returns a UNICODE that has the id of the user NOT an int).
Column Descriptions:
id
- primary key for the table. Id of the user. i.e. the
unique identifier for the collection
confirmed
- boolean val (default value = False) that is
an indication of whether the user has confirmed their
account via email.
first_name
- ... string self explanatory
last_name
- ... string self explanatory
email
- string self explanatory. But we impose the uniqueness
constraint on this column. It is necessary to check for this
on the backend before entering an email into the table,
else there will be some nasty errors produced when the user
tries to add an existing email into the table.
Note: first_name, last_name, email form an index table for easy lookup. See Role for more info
password_hash
is a 128 char long string containing the hashed
password. As always, it is best practice to never include the
plaintext password on the server. This hashed password is
checked against when authenticating users.
role_id
is the id of the role the user is. It is a foreign key
and relates to the id's in the Role collection. By default
the general user is role.id = 1, and role.id = 2 is the
admin. Also note that we refer to the Role collection with
'roles' rather than the assigned backref 'role' since we
are referring to an individual column.
Other User Class Variables and Methods
Note that the following methods are actually available in your Jinja templates since they are attached to the user instance.
full_name
provides the full name of the user given a first and last
name
can
provides a really cool way of determining whether a user has
given permissions. See the Permissions class for more info.
is_admin
is an implementation of can
to test a user against
admin permissions.
password
This does not give a password if a user just
calls the method and throws an AttributeError. However
if someone chooses to set a password e.g.
u = User( password = test
) the second definition of
password method is run, taking the keyword arg (kwarg) as the
password to then call the generate_password_hash method and
set the password_hash property of the user to the generated
password.
verify_password
well...verifies a provided user plaintext password
against the password_hash in the user record. Uses the
check_password_hash method.
generate_confirmation_token
returns a cryptographically signed
string with encrypted user id under key confirm
. This will
expire in 7 days. Note that Serializer is actually
TimedJSONWebSerializer when looking for documentation.
generate_changed_email_token
also returns a cryptographically
signed string with encrypted user id under key change_email
and a encrypted new_email parameter password into the method
containing the desired new email the user wants to replace the
old email with.
generate_password_reset_token
operates similarly to generate_
confirmation_token
. Generates token for password reset
NOTE: For context, the generate_..._token methods are used to create a random string that will be later added to an email (usually) to the requesting user.
confirm_account
The confirm_account method will take in a token (which was presumably generated from the generate_confirmation_token method) and then return True if the provided token is valid (and can be decrypted with the SECRET_KEY and has not expired) AND the decrypted token has the key 'confirm' with the id of the requesting user. If so, it flips the 'confirmed' attribute of the requesting user to True. Will throw BadSignature of the token is invalid, will throw SignatureExpired if the token is past the expiration time.
change_email
The change_email method will take in a token (which was presumably generated from the generate_email_token method) and then return True True if the token is valid (see above method for explanation of 'valid') and contains the key 'change_email' with value = user id in addition to the key 'new_email' with the new email address the user wants to change their email to. Before the new_email is committed to the session, a query is performed on the User collection on all the emails to maintain the unique constraint on the email columns. Then the user's 'email' attribute is set to the 'new_email' specified in the decrypted token. will throw BadSignature if invalid token and SignatureExpired if the token is expired.
AnonymousUser
We define a custom AnonymousUser class that represents a non-logged user. It extends the AnonymousUserMixing provided by flask-loginmanager we deny all permissions and affirm that this user is not an admin
class AnonymousUser(AnonymousUserMixin):
def can(self, _):
return False
def is_admin(self):
return False
login_manager.anonymous_user = AnonymousUser
We then register our custom AnonymousUser class as the default login_manager anonymous user class
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
This is the default user_loader method for login_manager. This method defines how to query for a user given a user_id from the user SESSION object. It is pretty straightforward, it will query the User table and find the user with ID equal to the user_id provided in the user SESSION