diff --git a/Pipfile b/Pipfile index 0a88eac..911224a 100644 --- a/Pipfile +++ b/Pipfile @@ -19,6 +19,7 @@ coverage = "*" flask-testing = "*" flask-pyoidc = "*" python-jose = "*" +flask-jwt-extended = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 1a8bde7..11072e3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e688fa75b0dde0fed147314666df07f60d9a4abf5fb8646ce3f2b85adf744a36" + "sha256": "bde392334efe0c90dfcb180d0fc07549827e819343af266b4fa43be0bc1c0596" }, "pipfile-spec": 6, "requires": { @@ -38,10 +38,10 @@ }, "apispec": { "hashes": [ - "sha256:9300142aa93e0c020e6b223a196cd2103ac4a61bcceea7dba894c0959b72e327", - "sha256:bcfe21887ba7c6e94c4be00f10564478a0d9109bb8e574aae97442909fd69b31" + "sha256:6746a57f1395fc201d06b051e43e8a7fdd23138607ebbeabdfcc6477bc3cc956", + "sha256:f0a5ddaee255eebeda8e84659028c3523b7801c7023f9364972035d36b23d734" ], - "version": "==1.1.0" + "version": "==1.1.1" }, "asn1crypto": { "hashes": [ @@ -208,6 +208,13 @@ "index": "pypi", "version": "==3.2.4" }, + "flask-jwt-extended": { + "hashes": [ + "sha256:68035dba637fd58c0f0b308e49103cba7f6977aa301d958ddbd7b811a91c6dec" + ], + "index": "pypi", + "version": "==3.18.0" + }, "flask-login": { "hashes": [ "sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec" @@ -309,9 +316,9 @@ }, "mako": { "hashes": [ - "sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae" + "sha256:04092940c0df49b01f43daea4f5adcecd0e50ef6a4b222be5ac003d5d84b2843" ], - "version": "==1.0.7" + "version": "==1.0.8" }, "markupsafe": { "hashes": [ @@ -348,10 +355,10 @@ }, "marshmallow": { "hashes": [ - "sha256:01412e979b45c003aeb3632718780b15b01566ae0182cc9232434b30f6b85e1b", - "sha256:8a1a2e13c6a621f4970faf21e5d9b146e451e779d0f334a96eae4fcdef53455f" + "sha256:0e497a6447ffaad55578138ca512752de7a48d12f444996ededc3d6bf8a09ca2", + "sha256:e21a4dea20deb167c723e0ffb13f4cf33bcbbeb8a334e92406a3308cedea2826" ], - "version": "==2.19.1" + "version": "==2.19.2" }, "oic": { "hashes": [ @@ -389,36 +396,36 @@ }, "pycryptodomex": { "hashes": [ - "sha256:0bda549e20db1eb8e29fb365d10acf84b224d813b1131c828fc830b2ce313dcd", - "sha256:1210c0818e5334237b16d99b5785aa0cee815d9997ee258bd5e2936af8e8aa50", - "sha256:2090dc8cd7843eae75bd504b9be86792baa171fc5a758ea3f60188ab67ca95cf", - "sha256:22e6784b65dfdd357bf9a8a842db445192b227103e2c3137a28c489c46742135", - "sha256:2edb8c3965a77e3092b5c5c1233ffd32de083f335202013f52d662404191ac79", - "sha256:310fe269ac870135ff610d272e88dcb594ee58f40ac237a688d7c972cbca43e8", - "sha256:456136b7d459f000794a67b23558351c72e21f0c2d4fcaa09fc99dae7844b0ef", - "sha256:463e49a9c5f1fa7bd36aff8debae0b5c487868c1fb66704529f2ad7e92f0cc9f", - "sha256:4a33b2828799ef8be789a462e6645ea6fe2c42b0df03e6763ccbfd1789c453e6", - "sha256:5ff02dff1b03929e6339226b318aa59bd0b5c362f96e3e0eb7f3401d30594ed3", - "sha256:6b1db8234b8ee2b30435d9e991389c2eeae4d45e09e471ffe757ba1dfae682bb", - "sha256:6eb67ee02de143cd19e36a52bd3869a9dc53e9184cd6bed5c39ff71dee2f6a45", - "sha256:6f42eea5afc7eee29494fdfddc6bb7173953d4197d9200e4f67096c2a24bc21b", - "sha256:87bc8082e2de2247df7d0b161234f8edb1384294362cc0c8db9324463097578b", - "sha256:8df93d34bc0e3a28a27652070164683a07d8a50c628119d6e0f7710f4d01b42f", - "sha256:989952c39e8fef1c959f0a0f85656e29c41c01162e33a3f5fd8ce71e47262ae9", - "sha256:a4a203077e2f312ec8677dde80a5c4e6fe5a82a46173a8edc8da668602a3e073", - "sha256:a793c1242dffd39f585ae356344e8935d30f01f6be7d4c62ffc87af376a2f5f9", - "sha256:b70fe991564e178af02ccf89435a8f9e8d052707a7c4b95bf6027cb785da3175", - "sha256:b83594196e3661cb78c97b80a62fbfbba2add459dfd532b58e7a7c62dd06aab4", - "sha256:ba27725237d0a3ea66ec2b6b387259471840908836711a3b215160808dffed0f", - "sha256:d1ab8ad1113cdc553ca50c4d5f0142198c317497364c0c70443d69f7ad1c9288", - "sha256:dce039a8a8a318d7af83cae3fd08d58cefd2120075dfac0ae14d706974040f63", - "sha256:e3213037ea33c85ab705579268cbc8a4433357e9fb99ec7ce9fdcc4d4eec1d50", - "sha256:ec8d8023d31ef72026d46e9fb301ff8759eff5336bcf3d1510836375f53f96a9", - "sha256:ece65730d50aa57a1330d86d81582a2d1587b2ca51cb34f586da8551ddc68fee", - "sha256:ed21fc515e224727793e4cc3fb3d00f33f59e3a167d3ad6ac1475ab3b05c2f9e", - "sha256:eec1132d878153d61a05424f35f089f951bd6095a4f6c60bdd2ef8919d44425e" + "sha256:00e0a7c992756c8d6d63f2cf992276ca62e702629bfcb54f57a192624c22c3d9", + "sha256:04689a34f5cf54bd049371c6b16ca44ed4c4202f3f9d58ff25e75016808d076e", + "sha256:088689b91b8dc0df52710e88ef447c6ca087c1f82398b8a41537c2a907f03e6a", + "sha256:1607d36788ff97b43abb896f7f81a2eed1bdc86e982e953e36b9afeb01942c9c", + "sha256:184356bb5039fc24c337dbe485b406425d6f0ee2659d59c313e69703063bf518", + "sha256:21cde61813059437206f745b620068d22cfe5059134eefe858ba7e4b639c410f", + "sha256:27a7968b231c1dec2a3d4cc66621c97ee623abda65fc1df392aa19a764f2e803", + "sha256:42c089411f55663c08e04c5bb8ce51d18002534ae76e8d2e1dd560b3d44dec5c", + "sha256:42e20e201267726c2d0687886d08f3e97192e0fa456535bb62274dd4b190dc0a", + "sha256:47ac3a9e87e559d1bfd60ebb5a81eac66f080ec5593c77d9e33b811bee929c87", + "sha256:73f72f7670d2182077fc36420e651ddc6eaa7c8478bf586719f051ecafec533e", + "sha256:84182dd94089287269a93f4d49226915b3aab386cba587dd8ec68da01bfa3e25", + "sha256:898c311e6fc8df2c4aba67c554276e5e798f8fb65d1854e26494f3d809314b91", + "sha256:96352d121e5a750b006e2a357d0a9913f12c6652ca07a5e61c74f63d8be0394f", + "sha256:9a18a7078cfdf61e020f0705c6b7188513973af68a26df42b7aaaa395b5e3e2f", + "sha256:9ecce5ab26737dde0beeff894344d56758e0711f2883619f10625d6a46d8116c", + "sha256:a4bac3507bcefd43e26a92182bcde27c4e37dbe4385b12ba04025a8bf93be5e7", + "sha256:ae044ff318656aaacdfce4d91c369ead871d170d03dfd44ee37e46734f06d24e", + "sha256:c8186e82d2854738ae7c18c9683bf023aa13d564b5c845a4cfd83063cf44e182", + "sha256:d2bc76e6edbcc8b32465c7e47f045398bb857390aca0782bfc6985919cf9d27c", + "sha256:d89db6097c644f814c9f8995a6a6e4b13dffcc82dcdb6ca06dc8a12545d36319", + "sha256:deb56cc3b9381b0d5a2060edb6e2ea31351c4b569abf52216d05c91a16214ac3", + "sha256:e3fe77a6558d21481d4c1290462eb3c9498bad46ce4cd979ff9dd2dd76b5700f", + "sha256:f04359d03506c4a8b0a7d178afda8dca78405bf6f8e8f2080d5206fc7733631a", + "sha256:f066c42bbc84c658d81aaecf36a14b71ef89a0353b17dc5c50f50fd7fb92fc15", + "sha256:f26d76c5430efc44b7a3ca4f5f661f3cb314a33bc2a9656f89f44bf73514446c", + "sha256:fab723651dfb40d25d8557bc0230133b29cf12835875cd56f77138ee467408cb", + "sha256:feb7673ee7981cee624c9cdb3446a7d909e99e7296eedef973e766a2bd5128ff" ], - "version": "==3.7.3" + "version": "==3.8.0" }, "pyjwkest": { "hashes": [ @@ -500,10 +507,10 @@ }, "sqlalchemy": { "hashes": [ - "sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b" + "sha256:d5432832f91d200c3d8b473a266d59442d825f9ea744c467e68c5d9a9479fbce" ], "index": "pypi", - "version": "==1.3.1" + "version": "==1.3.2" }, "sqlalchemy-migrate": { "hashes": [ @@ -543,10 +550,10 @@ }, "werkzeug": { "hashes": [ - "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", - "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + "sha256:0a73e8bb2ff2feecfc5d56e6f458f5b99290ef34f565ffb2665801ff7de6af7a", + "sha256:7fad9770a8778f9576693f0cc29c7dcc36964df916b83734f4431c0e612a7fbc" ], - "version": "==0.14.1" + "version": "==0.15.2" } }, "develop": {} diff --git a/__init__.py b/__init__.py index 0383e90..51040ea 100644 --- a/__init__.py +++ b/__init__.py @@ -3,8 +3,10 @@ Backend base module """ -from flask import Flask +import jwt +from flask import Flask, jsonify from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth +from flask_jwt_extended import JWTManager, decode_token from flask_login import LoginManager from flask_sqlalchemy import SQLAlchemy @@ -16,9 +18,32 @@ db = SQLAlchemy(app) login_manager = LoginManager() login_manager.init_app(app) -jwt_auth = HTTPTokenAuth() +# flask_jwt_extended: to be used usually by API +jwt_extended = JWTManager(app) +# +jwt_auth = HTTPTokenAuth('Bearer') + + +@jwt_auth.verify_token +def verify_token(token): + """This function (and HTTPTokenAuth('Bearer')) has been defined to be used together with MultiAuth. For API calls + solely using JWT authentication, jwt_required of flask_jwt_extended should be used directly.""" + app.logger.info(token) + try: + decoded = decode_token(token) + except jwt.exceptions.DecodeError as e: + app.logger.warn("Could not verify token: {}".format(str(e))) + return False + except jwt.exceptions.ExpiredSignatureError as e: + app.logger.warn("Could not verify token: {}".format(str(e))) + return False + app.logger.info(decoded) + return True + + basic_auth = HTTPBasicAuth() multi_auth = MultiAuth(basic_auth, jwt_auth) + from backend.auth import oidc_auth, auth_bp oidc_auth.init_app(app) @@ -26,11 +51,13 @@ oidc_auth.init_app(app) # oidc_multi_auth = MultiAuth(oidc_auth, jwt_auth) <- can't work as OIDCAuthentication not implementing HTTPAuth from .serve_frontend import fe_bp -from .api import auth_api_bp, api_bp +from .api import auth_api_bp, api_v1, api_bp app.register_blueprint(auth_bp) app.register_blueprint(auth_api_bp) app.register_blueprint(api_bp) app.register_blueprint(fe_bp) +# Fix flask-restplus by duck typing error handlers +jwt_extended._set_error_handler_callbacks(api_v1) diff --git a/api/__init__.py b/api/__init__.py index 7dae563..a8584fd 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from flask import Blueprint -from flask_restplus import Api +from flask_restplus import Api, Namespace api_authorizations = { 'apikey': { @@ -25,7 +25,15 @@ api_bp = Blueprint('api', __name__, url_prefix='/api') api_v1 = Api(api_bp, prefix="/v1", version='0.1', title='Vue Test API', description='The Vue Test API', doc='/v1/doc/', authorizations=api_authorizations, security='bearerAuth') +api_user = Namespace('user', description="User management namespace", authorizations=api_authorizations) + +api_v1.add_namespace(api_user) + auth_api_bp = Blueprint('auth_api', __name__, url_prefix='/api/auth') +user_api_bp = Blueprint('user_api', __name__, url_prefix='/api/user') +group_api_bp = Blueprint('group_api', __name__, url_prefix='/api/group') from .example_api import * from .auth_api import * +from .user_api import * +from .group_api import * diff --git a/api/auth_api.py b/api/auth_api.py index 845048a..805c62e 100644 --- a/api/auth_api.py +++ b/api/auth_api.py @@ -5,10 +5,14 @@ For example: listing of available auth providers or registration of users. Login through API does not start a new session, but instead returns JWT. """ +import base64 +import json + import flask from datetime import datetime, timedelta import jwt -from flask import request, jsonify, current_app, url_for +from flask import request, jsonify, current_app, url_for, Response, session, redirect, make_response +from flask_jwt_extended import create_access_token, create_refresh_token, jwt_refresh_token_required, get_jwt_identity from functools import wraps from random import randint @@ -22,14 +26,6 @@ from backend.auth import AUTH_PROVIDERS, oidc_auth from backend.models.user_model import User, Group -def create_jwt(user: User, validity_min=30): - return jwt.encode({ - 'sub': user.email, - 'iat': datetime.utcnow(), - 'exp': datetime.utcnow() + timedelta(minutes=validity_min)}, - current_app.config['SECRET_KEY']) - - @auth_api_bp.route('/providers', methods=('GET',)) def get_auth_providers(): providers = dict() @@ -65,8 +61,11 @@ def login(): if not user: return jsonify({'message': 'Invalid credentials', 'authenticated': False}), 401 - token = create_jwt(user) - return jsonify({'token': token.decode('UTF-8')}) + token = { + 'access_token': create_access_token(identity=user.email, fresh=True), + 'refresh_token': create_refresh_token(identity=user.email) + } + return jsonify(token), 200 def check_and_create_groups(groups: Iterable[str]): @@ -112,17 +111,38 @@ def create_or_retrieve_user_from_userinfo(userinfo): @auth_api_bp.route('/oidc', methods=['GET']) +@auth_api_bp.route('/oidc/', methods=['GET']) @oidc_auth.oidc_auth() -def oidc(): - +def oidc(redirect_url=None): user = create_or_retrieve_user_from_userinfo(flask.session['userinfo']) - - #return jsonify(user.to_dict()) - return user.toJSON() if user is None: return "Could not authenticate: could not find or create user.", 401 if current_app.config.get("AUTH_RETURN_EXTERNAL_JWT", False): token = jwt.encode(flask.session['id_token'], current_app.config['SECRET_KEY']) else: - token = create_jwt(user) - return token + token = json.dumps({ + 'access_token': create_access_token(identity=user.email, fresh=True), + 'refresh_token': create_refresh_token(identity=user.email) + }) + if redirect_url is None: + redirect_url = request.headers.get("Referer") + if redirect_url is None: + redirect_url = request.args.get('redirect_url') + if redirect_url is None: + redirect_url = "/" + app.logger.info("Token: {}".format(token)) + response = make_response(redirect(redirect_url)) + response.set_cookie('tokens', base64.b64encode(token.encode('utf-8'))) + return response + + +@app.route('/refresh', methods=['POST']) +@jwt_refresh_token_required +def refresh(): + """Refresh token endpoint. This will generate a new access token from + the refresh token, but will mark that access token as non-fresh, + as we do not actually verify a password in this endpoint.""" + current_user = get_jwt_identity() + new_token = create_access_token(identity=current_user, fresh=False) + ret = {'access_token': new_token} + return jsonify(ret), 200 diff --git a/api/group_api.py b/api/group_api.py new file mode 100644 index 0000000..60390ee --- /dev/null +++ b/api/group_api.py @@ -0,0 +1,40 @@ +# Copyright (c) 2019. Tobias Kurze +""" +This module provides functions related to authentication through the API. +For example: listing of available auth providers or registration of users. + +Login through API does not start a new session, but instead returns JWT. +""" +import flask +from datetime import datetime, timedelta +import jwt +from flask import request, jsonify, current_app, url_for +from flask_jwt_extended import jwt_required +from functools import wraps +from random import randint + +from flask_login import logout_user, login_user +from typing import Iterable +from werkzeug.routing import BuildError + +from backend import db, app +from backend.api import auth_api_bp, group_api_bp +from backend.auth import AUTH_PROVIDERS, oidc_auth +from backend.models.user_model import User, Group + + + +@group_api_bp.route('/', methods=['GET']) +@jwt_required +def get_group(): + + user = create_or_retrieve_user_from_userinfo(flask.session['userinfo']) + + return jsonify(user.to_dict()) + if user is None: + return "Could not authenticate: could not find or create user.", 401 + if current_app.config.get("AUTH_RETURN_EXTERNAL_JWT", False): + token = jwt.encode(flask.session['id_token'], current_app.config['SECRET_KEY']) + else: + token = create_jwt(user) + return token diff --git a/api/user_api.py b/api/user_api.py new file mode 100644 index 0000000..4c0aafd --- /dev/null +++ b/api/user_api.py @@ -0,0 +1,58 @@ +# Copyright (c) 2019. Tobias Kurze +""" +This module provides functions related to authentication through the API. +For example: listing of available auth providers or registration of users. + +Login through API does not start a new session, but instead returns JWT. +""" +import flask +import jwt +from flask import request, jsonify, current_app, url_for +from flask_jwt_extended import get_jwt_identity, jwt_optional, jwt_required +from flask_restplus import Resource, fields + +from backend import db, app, jwt_auth +from backend.api import user_api_bp, api_bp, api_user +from backend.auth import oidc_auth +from backend.models.user_model import User, Group + +user = api_user.model('User', { + 'id': fields.String(required=True, description='The user\'s identifier'), + 'first_name': fields.String(required=True, description='The user\'s first name'), +}) + + +@api_user.route('/') +class UserList(Resource): + """ + This is a test class. + """ + #@jwt_auth.login_required + @jwt_required + @api_user.doc('users') + @api_user.marshal_list_with(user) + def get(self): + """ + just a test! + :return: Hello: World + """ + current_user = get_jwt_identity() + app.logger.info(current_user) + return User.get_all() + + +@api_user.route('/') +@api_user.param('id', 'The user identifier') +@api_user.response(404, 'User not found') +class UserResource(Resource): + @jwt_auth.login_required + @api_user.doc('get_user') + @api_user.marshal_with(user) + def get(self, id): + """Fetch a user given its identifier""" + user = User.get_by_id(id) + if user is not None: + return user + api_user.abort(404) + +# api_user.add_resource(UserResource, '/') diff --git a/app.db b/app.db index c8eb8e4..7f0f8df 100644 Binary files a/app.db and b/app.db differ diff --git a/auth/oidc.py b/auth/oidc.py index f69fd10..77011e8 100644 --- a/auth/oidc.py +++ b/auth/oidc.py @@ -4,7 +4,7 @@ OIDC login auth module """ import flask -from flask import jsonify +from flask import jsonify, redirect, url_for from flask_login import login_user from flask_pyoidc.flask_pyoidc import OIDCAuthentication from flask_pyoidc.user_session import UserSession @@ -15,17 +15,28 @@ from . import auth_bp from .oidc_config import PROVIDER_NAME, OIDC_PROVIDERS +OIDCAuthentication.oidc_auth_orig = OIDCAuthentication.oidc_auth +OIDCAuthentication.oidc_logout_orig = OIDCAuthentication.oidc_logout + + def oidc_auth_default_provider(self): + """monkey patch oidc_auth""" return self.oidc_auth_orig(PROVIDER_NAME) -OIDCAuthentication.oidc_auth_orig = OIDCAuthentication.oidc_auth +def oidc_logout_default_provider(self): + """monkey patch oidc_logout""" + return self.oidc_logout_orig(PROVIDER_NAME) + + OIDCAuthentication.oidc_auth = oidc_auth_default_provider +OIDCAuthentication.oidc_logout = oidc_logout_default_provider oidc_auth = OIDCAuthentication(OIDC_PROVIDERS) def create_or_retrieve_user_from_userinfo(userinfo): + """Updates and returns ar creates a user from userinfo (part of OIDC token).""" try: email = userinfo["email"] except KeyError: @@ -34,6 +45,7 @@ def create_or_retrieve_user_from_userinfo(userinfo): if user is not None: app.logger.info("user found") + #TODO: update user! return user user = User(email=email, first_name=userinfo.get("given_name", ""), @@ -57,3 +69,9 @@ def oidc(): return jsonify(id_token=user_session.id_token, access_token=flask.session['access_token'], userinfo=user_session.userinfo) + + +@auth_bp.route('/oidc_logout', methods=['GET']) +def oidc_logout(): + oidc_auth.oidc_logout() + return redirect('/') diff --git a/auth/utils.py b/auth/utils.py new file mode 100644 index 0000000..8a12ca4 --- /dev/null +++ b/auth/utils.py @@ -0,0 +1,42 @@ +import flask_jwt_extended +from flask_jwt_extended import jwt_optional, get_jwt_identity +from functools import wraps + +from backend import jwt_auth +from backend.models.user_model import User + + +def requires_permission_level(permission_level): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if flask_jwt_extended.verify_jwt_in_request(): + current_user_id = get_jwt_identity() + user = User.get_by_identifier(current_user_id) + if user is not None: + if user.has_permission(permission_level): + #for g in user.groups: + # if g.permissions + #TODO + pass + else: + pass + # return FALSE + #if not session.get('email'): + # return redirect(url_for('users.login')) + + #user = User.find_by_email(session['email']) + #elif not user.allowed(access_level): + # return redirect(url_for('users.profile', message="You do not have access to that page. Sorry!")) + return f(*args, **kwargs) + return decorated_function + return decorator + + +def require_jwt(): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + return jwt_auth.login_required(jwt_optional(f(*args, **kwargs))) + return decorated_function + return decorator diff --git a/config.py b/config.py index 28b41ec..f8ecac8 100644 --- a/config.py +++ b/config.py @@ -64,9 +64,11 @@ class Config(): #ASSETS_DEBUG = True - JWT_SECRET = "abcxyz" - JWT_ALGORITHM = "HS256" - JWT_EXP_DELTA_SECONDS = 5 * 60 + #JWT_SECRET = "abcxyz" + #JWT_ALGORITHM = "HS256" + #JWT_EXP_DELTA_SECONDS = 5 * 60 + + JWT_SECRET_KEY = "abcxyz" AUTH_RETURN_EXTERNAL_JWT = False diff --git a/models/user_model.py b/models/user_model.py index a564bf4..53a8b66 100644 --- a/models/user_model.py +++ b/models/user_model.py @@ -46,23 +46,38 @@ user_group_table = db.Table('user_group', primary_key=True)) +# This is the association table for the many-to-many relationship between +# groups and permissions. +group_permission_table = db.Table('group_permission', + db.Column('group_id', db.Integer, + db.ForeignKey('group.id', + onupdate="CASCADE", + ondelete="CASCADE"), + primary_key=True), + db.Column('permission_id', db.Integer, + db.ForeignKey('permission.id', + onupdate="CASCADE", + ondelete="CASCADE"), + primary_key=True)) + + class User(UserMixin, db.Model): """ Example user model representation. """ id = db.Column(db.Integer, primary_key=True) - social_id = db.Column(db.String(64), nullable=True, unique=True) - nickname = db.Column(db.String(64), index=True, unique=True) - first_name = db.Column(db.String(64), index=True, nullable=True) - last_name = db.Column(db.String(64), index=True, nullable=True) + social_id = db.Column(db.Unicode(63), nullable=True, unique=True) + nickname = db.Column(db.Unicode(63), index=True, unique=True) + first_name = db.Column(db.Unicode(63), index=True, nullable=True) + last_name = db.Column(db.Unicode(63), index=True, nullable=True) email = db.Column(db.String(120), nullable=False, index=True, unique=True) - lang = db.Column(db.String(16), index=False, unique=False) - timezone = db.Column(db.String(32), index=False, unique=False) + lang = db.Column(db.Unicode(32), index=False, unique=False) + timezone = db.Column(db.Unicode(63), index=False, unique=False) posts = db.relationship('Post', backref='author', lazy='dynamic') example_data_item = db.relationship('ExampleDataItem', backref='owner') example_data_item_id = db.Column(db.ForeignKey(ExampleDataItem.id)) - about_me = db.Column(db.String(140)) - role = db.Column(db.String(64)) + about_me = db.Column(db.Unicode(255)) + role = db.Column(db.Unicode(63)) groups = db.relationship('Group', secondary=user_group_table, back_populates='users') password = db.Column(db.String(255), nullable=True) registered_on = db.Column(db.DateTime, nullable=False, default=datetime.utcnow()) @@ -110,6 +125,16 @@ class User(UserMixin, db.Model): User.email == identifier, User.id == identifier)).first() + @staticmethod + @login_manager.user_loader + def get_by_id(identifier): + """ + Find user by ID. + :param identifier: + :return: + """ + return User.query.filter(User.id == identifier).first() + @staticmethod def get_all(): """ @@ -396,8 +421,9 @@ class Group(db.Model): super(Group, self).__init__(**kwargs) id = db.Column(db.Integer, autoincrement=True, primary_key=True) - name = db.Column(db.Unicode(64), unique=True, nullable=False) + name = db.Column(db.Unicode(63), unique=True, nullable=False) users = db.relationship('User', secondary=user_group_table, back_populates='groups') + permissions = db.relationship('Permission', secondary=group_permission_table, back_populates='groups') @staticmethod def get_by_name(name): @@ -417,3 +443,12 @@ class Group(db.Model): def toJSON(self): return json.dumps(self.to_dict(), default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class Permission(db.Model): + """Table containing permissions associated with groups.""" + id = db.Column(db.Integer, autoincrement=True, primary_key=True) + name = db.Column(db.Unicode(63), unique=True, nullable=False) + description = db.Column(db.Unicode(255)) + groups = db.relationship(Group, secondary=group_permission_table, + back_populates='permissions')