Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix MongoDB support. #809

Merged
merged 1 commit into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Fixes
++++++

- (:pr:`xxx`) Webauthn Updates to handling of transport.
- (:pr:`xxx`) Fix MongoDB support by eliminating dependency on flask-mongoengine.
Improve MongoDB quickstart.

Backwards Compatibility Concerns
+++++++++++++++++++++++++++++++++
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
nitpicky = True
nitpick_ignore = [
("py:attr", "LoginManager.unauthorized"),
("py:class", "flask_mongoengine.MongoEngine"),
("py:class", "mongoengine.connection"),
("py:class", "ResponseValue"),
("py:class", "function"),
("py:class", "AuthenticatorSelectionCriteria"),
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ connections and model definitions. Flask-Security supports the following Flask
extensions out of the box for data persistence:

1. `Flask-SQLAlchemy <https://pypi.python.org/pypi/flask-sqlalchemy/>`_
2. `Flask-MongoEngine <https://pypi.python.org/pypi/flask-mongoengine/>`_
2. `MongoEngine <https://pypi.python.org/pypi/mongoengine/>`_
3. `Peewee Flask utils <https://docs.peewee-orm.com/en/latest/peewee/playhouse.html#flask-utils>`_
4. `PonyORM <https://pypi.python.org/pypi/pony/>`_ - NOTE: not currently supported.
5. `SQLAlchemy sessions <https://docs.sqlalchemy.org/en/14/orm/session_basics.html>`_
Expand Down
4 changes: 4 additions & 0 deletions docs/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ the User record (since we need to look up the ``User`` based on a WebAuthn ``cre

webauthn = ListField(ReferenceField(WebAuthn, reverse_delete_rule=PULL), default=[])

To make sure all WebAuthn objects are deleted if the User is deleted:

User.register_delete_rule(WebAuthn, "user", CASCADE)


**For peewee**::

Expand Down
67 changes: 43 additions & 24 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ MongoEngine Install requirements

$ python3 -m venv pymyenv
$ . pymyenv/bin/activate
$ pip install flask-security-too[common] flask-mongoengine
$ pip install flask-security-too[common] mongoengine

MongoEngine Application
~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -285,9 +285,18 @@ local MongoDB instance):
import os

from flask import Flask, render_template_string
from flask_mongoengine import MongoEngine
from mongoengine import Document, connect
from mongoengine.fields import (
BinaryField,
BooleanField,
DateTimeField,
IntField,
ListField,
ReferenceField,
StringField,
)
from flask_security import Security, MongoEngineUserDatastore, \
UserMixin, RoleMixin, auth_required, hash_password
UserMixin, RoleMixin, auth_required, hash_password, permissions_accepted

# Create app
app = Flask(__name__)
Expand All @@ -298,27 +307,27 @@ local MongoDB instance):
# Bcrypt is set as default SECURITY_PASSWORD_HASH, which requires a salt
# Generate a good salt using: secrets.SystemRandom().getrandbits(128)
app.config['SECURITY_PASSWORD_SALT'] = os.environ.get("SECURITY_PASSWORD_SALT", '146585145368132386173505678016728509634')

# MongoDB Config
app.config['MONGODB_DB'] = 'mydatabase'
app.config['MONGODB_HOST'] = 'localhost'
app.config['MONGODB_PORT'] = 27017
# Don't worry if email has findable domain
app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": False}

# Create database connection object
db = MongoEngine(app)

class Role(db.Document, RoleMixin):
name = db.StringField(max_length=80, unique=True)
description = db.StringField(max_length=255)
permissions = db.StringField(max_length=255)

class User(db.Document, UserMixin):
email = db.StringField(max_length=255, unique=True)
password = db.StringField(max_length=255)
active = db.BooleanField(default=True)
fs_uniquifier = db.StringField(max_length=64, unique=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
db_name = "mydatabase"
db = connect(alias=db_name, db=db_name, host="mongodb://localhost", port=27017)

class Role(Document, RoleMixin):
name = StringField(max_length=80, unique=True)
description = StringField(max_length=255)
permissions = ListField(required=False)
meta = {"db_alias": db_name}

class User(Document, UserMixin):
email = StringField(max_length=255, unique=True)
password = StringField(max_length=255)
active = BooleanField(default=True)
fs_uniquifier = StringField(max_length=64, unique=True)
confirmed_at = DateTimeField()
roles = ListField(ReferenceField(Role), default=[])
meta = {"db_alias": db_name}

# Setup Flask-Security
user_datastore = MongoEngineUserDatastore(db, User, Role)
Expand All @@ -330,11 +339,21 @@ local MongoDB instance):
def home():
return render_template_string("Hello {{ current_user.email }}")

@app.route("/user")
@auth_required()
@permissions_accepted("user-read")
def user_home():
return render_template_string("Hello {{ current_user.email }} you are a user!")

# one time setup
with app.app_context():
# Create a user to test with
# Create a user and role to test with
app.security.datastore.find_or_create_role(
name="user", permissions={"user-read", "user-write"}
)
if not app.security.datastore.find_user(email="test@me.com"):
app.security.datastore.create_user(email="test@me.com", password=hash_password("password"))
app.security.datastore.create_user(email="test@me.com",
password=hash_password("password"), roles=["user"])

if __name__ == '__main__':
# run application (can also use flask run)
Expand Down
8 changes: 4 additions & 4 deletions flask_security/datastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
This module contains an user datastore classes.

:copyright: (c) 2012 by Matt Wright.
:copyright: (c) 2019-2022 by J. Christopher Wagner (jwag).
:copyright: (c) 2019-2023 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
"""
import datetime
Expand All @@ -17,7 +17,7 @@

if t.TYPE_CHECKING: # pragma: no cover
import flask_sqlalchemy
import flask_mongoengine
import mongoengine
import sqlalchemy.orm.scoping


Expand Down Expand Up @@ -863,7 +863,7 @@ def commit(self):
class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
"""A UserDatastore implementation that assumes the
use of
`Flask-MongoEngine <https://pypi.python.org/pypi/flask-mongoengine/>`_
`MongoEngine <https://pypi.org/project/mongoengine/>`_
for datastore transactions.

:param db:
Expand All @@ -874,7 +874,7 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):

def __init__(
self,
db: "flask_mongoengine.MongoEngine",
db: "mongoengine.connection",
user_model: t.Type["User"],
role_model: t.Type["Role"],
webauthn_model: t.Optional[t.Type["WebAuthn"]] = None,
Expand Down
1 change: 0 additions & 1 deletion requirements/tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ Flask-Babel
Babel
Flask-Login
Flask-Mailman
Flask-Mongoengine
Flask-Principal
peewee
Flask-SQLAlchemy
Expand Down
1 change: 0 additions & 1 deletion requirements_low/tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ Flask==2.3.2
Flask-SQLAlchemy==3.0.3
Flask-Babel==3.1.0
Flask-Mailman==0.3.0
Flask-Mongoengine==1.0.0
Flask-Login==0.6.2
Flask-WTF==1.1.1
peewee==3.16.2
Expand Down
46 changes: 18 additions & 28 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,9 @@ def mongoengine_datastore(request, app, tmpdir, realmongodburl):

def mongoengine_setup(request, app, tmpdir, realmongodburl):
# To run against a realdb: mongod --dbpath <somewhere>
pytest.importorskip("flask_mongoengine")
from flask_mongoengine import MongoEngine
import pymongo
import mongomock
from mongoengine import Document, connect
from mongoengine.fields import (
BinaryField,
BooleanField,
Expand All @@ -312,34 +313,23 @@ def mongoengine_setup(request, app, tmpdir, realmongodburl):
from mongoengine import PULL, CASCADE, disconnect_all

db_name = "flask_security_test"
app.config["MONGODB_SETTINGS"] = {
"db": db_name,
"host": realmongodburl if realmongodburl else "mongodb://localhost",
"port": 27017,
"alias": db_name,
}
if realmongodburl:
db = MongoEngine(app)
else:
# mongoengine 0.27 requires setting mongo_client_class
import mongomock

try:
app.config["MONGODB_SETTINGS"]["host"] = "mongomock://localhost"
db = MongoEngine(app)
except Exception:
# new way
app.config["MONGODB_SETTINGS"]["host"] = "mongodb://localhost"
app.config["MONGODB_SETTINGS"]["mongo_client_class"] = mongomock.MongoClient
db = MongoEngine(app)

class Role(db.Document, RoleMixin):
db_host = realmongodburl if realmongodburl else "mongodb://localhost"
db_client_class = pymongo.MongoClient if realmongodburl else mongomock.MongoClient
db = connect(
alias=db_name,
db=db_name,
host=db_host,
port=27017,
mongo_client_class=db_client_class,
)

class Role(Document, RoleMixin):
name = StringField(required=True, unique=True, max_length=80)
description = StringField(max_length=255)
permissions = ListField(required=False)
meta = {"db_alias": db_name}

class WebAuthn(db.Document, WebAuthnMixin):
class WebAuthn(Document, WebAuthnMixin):
credential_id = BinaryField(primary_key=True, max_bytes=1024, required=True)
public_key = BinaryField(required=True)
sign_count = IntField(default=0)
Expand All @@ -364,7 +354,7 @@ def get_user_mapping(self) -> t.Dict[str, str]:
"""
return dict(id=self.user.id)

class User(db.Document, UserMixin):
class User(Document, UserMixin):
email = StringField(unique=True, max_length=255)
fs_uniquifier = StringField(unique=True, max_length=64, required=True)
fs_webauthn_user_handle = StringField(unique=True, max_length=64)
Expand Down Expand Up @@ -395,14 +385,14 @@ class User(db.Document, UserMixin):
def get_security_payload(self):
return {"email": str(self.email)}

db.Document.register_delete_rule(WebAuthn, "user", CASCADE)
User.register_delete_rule(WebAuthn, "user", CASCADE)

def tear_down():
with app.app_context():
User.drop_collection()
Role.drop_collection()
WebAuthn.drop_collection()
db.connection.drop_database(db_name)
db.drop_database(db_name)
disconnect_all()

request.addfinalizer(tear_down)
Expand Down
4 changes: 3 additions & 1 deletion tests/test_confirmable.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ def on_instructions_sent(app, **kwargs):
with app.app_context():
app.security.datastore.delete(user)
app.security.datastore.commit()
if hasattr(app.security.datastore.db, "close_db"):
if hasattr(app.security.datastore.db, "close_db") and callable(
app.security.datastore.db.close_db
):
app.security.datastore.db.close_db(None)

response = clients.get("/confirm/" + token, follow_redirects=True)
Expand Down
4 changes: 3 additions & 1 deletion tests/test_webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,9 @@ def authned(myapp, user, **extra_args):
cred.lastuse_datetime = fake_dt
app.security.datastore.put(cred)
app.security.datastore.commit()
if hasattr(app.security.datastore.db, "close_db"):
if hasattr(app.security.datastore.db, "close_db") and callable(
app.security.datastore.db.close_db
):
app.security.datastore.db.close_db(None)

response = clients.get("/wan-register", headers=headers)
Expand Down