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