Skip to content

Commit 2dfd2a4

Browse files
authored
Don't assume username in CLI user_create. (#1080)
Some models may not have a username so only send it if the CLI user sets a username close #1078
1 parent 252d945 commit 2dfd2a4

File tree

5 files changed

+82
-6
lines changed

5 files changed

+82
-6
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Released xxx
1111
Fixes
1212
+++++
1313
- (:issue:`1077`) Fix runtime modification of a config string (TWO_FACTOR_METHODS)
14+
- (:issue:`1078`) Fix CLI user_create when model doesn't contain username
1415

1516
Version 5.6.0
1617
-------------

docs/models.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,15 @@ At the bare minimum your `User` and `Role` model must include the following fiel
117117
* ``password`` (string, nullable)
118118
* ``active`` (boolean, non-nullable)
119119
* ``fs_uniquifier`` (string, 64 bytes, unique, non-nullable)
120+
* a many-to-many relationship to table Role
120121
121122
122123
**Role**
123124
124125
* primary key
125126
* ``name`` (unique, non-nullable)
126127
* ``description`` (string)
128+
* a many-to-many relationship to table User
127129
128130
129131
Additional Functionality

flask_security/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ def users_create(attributes, password, active, username):
145145
)
146146

147147
kwargs[attr] = attrarg
148-
kwargs.update(**{"password": password, "username": username})
148+
kwargs["password"] = password
149+
if username:
150+
kwargs["username"] = username
149151

150152
# We always add password_confirm here - if user used the CLI in input mode -
151153
# it already asked for confirmation.

tests/conftest.py

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -543,12 +543,46 @@ def fsqlalite_datastore(app, tmpdir, realdburl):
543543
td()
544544

545545

546-
def fsqlalite_setup(app, tmpdir, realdburl):
546+
@pytest.fixture()
547+
def fsqlalite_min_datastore(app, tmpdir, realdburl):
548+
pytest.importorskip("flask_sqlalchemy_lite")
549+
from sqlalchemy.orm import declared_attr, mapped_column, relationship
550+
from sqlalchemy import String
551+
552+
class FsMinUserMixin(UserMixin):
553+
# flask_security basic fields
554+
id: Mapped[int] = mapped_column(primary_key=True) # type: ignore
555+
email: Mapped[str] = mapped_column(String(255), unique=True) # type: ignore
556+
password: Mapped[str | None] = mapped_column(String(255)) # type: ignore
557+
active: Mapped[bool] = mapped_column() # type: ignore
558+
fs_uniquifier: Mapped[str] = mapped_column( # type: ignore
559+
String(64), unique=True
560+
)
561+
562+
@declared_attr
563+
def roles(cls):
564+
# The first arg is a class name, the backref is a column name
565+
return relationship(
566+
"Role",
567+
secondary="roles_users",
568+
back_populates="users",
569+
)
570+
571+
ds, td = fsqlalite_setup(
572+
app, tmpdir, realdburl, usermixin=FsMinUserMixin, use_webauthn=False
573+
)
574+
yield ds
575+
td()
576+
577+
578+
def fsqlalite_setup(app, tmpdir, realdburl, usermixin=None, use_webauthn=True):
547579
pytest.importorskip("flask_sqlalchemy_lite")
548580
from flask_sqlalchemy_lite import SQLAlchemy
549581
from sqlalchemy.orm import DeclarativeBase, mapped_column
550582
from flask_security.models import sqla as sqla
551583

584+
if not usermixin:
585+
usermixin = sqla.FsUserMixin
552586
if realdburl:
553587
db_url, db_info = _setup_realdb(realdburl)
554588
else:
@@ -568,10 +602,12 @@ class Model(DeclarativeBase):
568602
class Role(Model, sqla.FsRoleMixin):
569603
__tablename__ = "role"
570604

571-
class WebAuthn(Model, sqla.FsWebAuthnMixin):
572-
__tablename__ = "webauthn"
605+
if use_webauthn:
573606

574-
class User(Model, sqla.FsUserMixin):
607+
class WebAuthn(Model, sqla.FsWebAuthnMixin):
608+
__tablename__ = "webauthn"
609+
610+
class User(Model, usermixin):
575611
__tablename__ = "user"
576612
security_number: Mapped[t.Optional[int]] = mapped_column( # type: ignore
577613
unique=True
@@ -593,7 +629,10 @@ def tear_down():
593629
if realdburl:
594630
_teardown_realdb(db_info)
595631

596-
return FSQLALiteUserDatastore(db, User, Role, WebAuthn), tear_down
632+
return (
633+
FSQLALiteUserDatastore(db, User, Role, WebAuthn if use_webauthn else None),
634+
tear_down,
635+
)
597636

598637

599638
@pytest.fixture()
@@ -1022,6 +1061,17 @@ def create_app():
10221061
return ScriptInfo(create_app=create_app)
10231062

10241063

1064+
@pytest.fixture()
1065+
def script_info_min(app, fsqlalite_min_datastore):
1066+
from flask.cli import ScriptInfo
1067+
1068+
def create_app():
1069+
app.security = Security(app, datastore=fsqlalite_min_datastore)
1070+
return app
1071+
1072+
return ScriptInfo(create_app=create_app)
1073+
1074+
10251075
def pytest_addoption(parser):
10261076
parser.addoption(
10271077
"--realdburl",

tests/test_cli.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,8 @@ def test_cli_createuserV2attr(script_info):
412412
"--password",
413413
"battery staple",
414414
"us_phone_number:5551212",
415+
"--username",
416+
"iamuser",
415417
],
416418
obj=script_info,
417419
)
@@ -421,3 +423,22 @@ def test_cli_createuserV2attr(script_info):
421423
with app.app_context():
422424
user = app.security.datastore.find_user(email="[email protected]")
423425
assert user.us_phone_number == "5551212"
426+
user = app.security.datastore.find_user(username="iamuser")
427+
assert user
428+
429+
430+
def test_cli_create_nousername(script_info_min):
431+
"""Test create user CLI passing attr that is in User but not in form."""
432+
runner = CliRunner()
433+
434+
# Create user
435+
result = runner.invoke(
436+
users_create,
437+
[
438+
439+
"--password",
440+
"battery staple",
441+
],
442+
obj=script_info_min,
443+
)
444+
assert result.exit_code == 0

0 commit comments

Comments
 (0)