codekingpro/portable-devtools
114k
1"""2Copyright 2021-2022 by J. Christopher Wagner (jwag). All rights reserved.3:license: MIT, see LICENSE for more details.4 5 6Complete models for all features when using Flask-SqlAlchemy7 8BE AWARE: Once any version of this is shipped no changes can be made - instead9a new version needs to be created.10 11This is Version 3:12 - Add support for webauthn.13 - Add support for 2FA recovery codes.14 - password can be null.15 - us_phone_number must be unique.16 - Add support for list types.17"""18 19from sqlalchemy import (20 Boolean,21 Column,22 DateTime,23 ForeignKey,24 Integer,25 LargeBinary,26 String,27)28from sqlalchemy.ext.declarative import declared_attr29from sqlalchemy.ext.mutable import MutableList30from sqlalchemy.sql import func31 32 33from .fsqla_v2 import FsModels as FsModelsV234from .fsqla_v2 import FsUserMixin as FsUserMixinV235from .fsqla_v2 import FsRoleMixin as FsRoleMixinV236from flask_security import AsaList, WebAuthnMixin37 38 39class FsModels(FsModelsV2):40 fs_model_version = 341 42 43class FsRoleMixin(FsRoleMixinV2):44 pass45 46 47class FsUserMixin(FsUserMixinV2):48 """User information"""49 50 try:51 import webauthn as webauthn_pkg52 53 # List of WebAuthn registrations54 @declared_attr55 def webauthn(cls):56 return FsModels.db.relationship(57 "WebAuthn", backref="users", cascade="all, delete"58 )59 60 except ImportError:61 pass62 63 # The user handle as required during registration.64 # Note max length 64 as specified in spec.65 fs_webauthn_user_handle = Column(String(64), unique=True, nullable=True)66 67 # MFA - one time recovery codes - comma separated.68 mf_recovery_codes = Column(MutableList.as_mutable(AsaList()), nullable=True)69 70 # Change password to nullable so we can tell after registration whether71 # a user has a password or not.72 password = Column(String(255), nullable=True)73 74 # since phone can be used to authenticate - must be unique.75 us_phone_number = Column(String(128), nullable=True, unique=True)76 77 # This is repeated since I couldn't figure out how to have it reference the78 # new version of FsModels.79 @declared_attr80 def roles(cls):81 return FsModels.db.relationship(82 "Role",83 secondary=FsModels.roles_users,84 backref=FsModels.db.backref(85 "users", lazy="dynamic", cascade_backrefs=False86 ),87 )88 89 90class FsWebAuthnMixin(WebAuthnMixin):91 """WebAuthn"""92 93 id = Column(Integer, primary_key=True)94 credential_id = Column(LargeBinary(1024), index=True, unique=True, nullable=False)95 public_key = Column(LargeBinary, nullable=False)96 sign_count = Column(Integer, default=0)97 transports = Column(MutableList.as_mutable(AsaList()), nullable=True)98 backup_state = Column(Boolean, nullable=False) # Upcoming post V3 spec99 device_type = Column(String(64), nullable=False)100 101 # a JSON string as returned from registration102 extensions = Column(String(255), nullable=True)103 create_datetime = Column(type_=DateTime, nullable=False, server_default=func.now())104 lastuse_datetime = Column(type_=DateTime, nullable=False)105 # name is provided by user - we make sure is unique per user106 name = Column(String(64), nullable=False)107 108 # Usage - a credential can EITHER be for first factor or secondary factor109 usage = Column(String(64), nullable=False)110 111 @declared_attr112 def user_id(cls):113 return Column(114 Integer,115 ForeignKey(f"{FsModels.user_table_name}.id", ondelete="CASCADE"),116 nullable=False,117 )118 