Skip to content

Commit

Permalink
[SDESK-7284] Fix async commands to build a Superdesk instance (#2680)
Browse files Browse the repository at this point in the history
* Implement new async_cli module

This module includes a custom Blueprint, AppGroup and a helper to easily create a command blueprint ready to register commands based on async function. The registered async commands will be run sync using `asgiref.sync.async_to_sync`

SDESK-7284

* Rework `app:initialize_data` command

The command was converted from the old class based commands into a function one using `Click` and our new `cli.register_async_command` decorator in order to use our new async services in the synchronous context of Quart's command line interface

SDESK-7284

* Update docker-compose.yml

* Fix type and docstrings

* Rework `users:create` command

Command converted from the old class based commands into a function one using `Click` and our new `cli.register_async_command` decorator.

SDESK-7284

* Rework `schema:migrate` command

SDESK-7284

* Update app_initialize.py

* Rework `data:upgrade` and `data:downgrade` commands

SDESK-7284

* Remove not needed false return

* Fix async commands to use proper app context

Async commands fail when using app context as they are executed in different threads or outside of the normal request context lifecycle. In order to fix that, we set the current app instance into the AsyncAppGroup so later it can be passed optionally to the command at the moment of its execution.

SDESK-7284

* Fix `schema:migrate` command

SDESK-7284

* Small improvement to use of app context in async commands

SDESK-7284
  • Loading branch information
eos87 authored Sep 4, 2024
1 parent 4690675 commit c90f21b
Show file tree
Hide file tree
Showing 18 changed files with 534 additions and 392 deletions.
2 changes: 1 addition & 1 deletion apps/auth/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .reset_password import ResetPasswordService, ResetPasswordResource, ActiveTokensResource
import superdesk
from .db import DbAuthService
from .commands import CreateUserCommand, HashUserPasswordsCommand # noqa
from .commands import create_user_command, HashUserPasswordsCommand # noqa
from superdesk.services import BaseService
from apps.auth.db.change_password import ChangePasswordService, ChangePasswordResource

Expand Down
82 changes: 42 additions & 40 deletions apps/auth/db/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,33 @@
# AUTHORS and LICENSE files distributed with this source code, or
# at https://www.sourcefabric.org/superdesk/license

import logging
import json
import csv
import json
import click
import logging

import superdesk

from pathlib import Path
from base64 import b64encode
from superdesk.core import get_app_config, get_current_app
import superdesk

from superdesk.flask import Flask
from superdesk.commands import cli
from superdesk.utils import get_hash, is_hashed


logger = logging.getLogger(__name__)
USER_FIELDS_NAMES = {"username", "email", "password", "first_name", "last_name", "sign_off", "role"}


class CreateUserCommand(superdesk.Command):
@cli.register_async_command("users:create", with_appcontext=True)
@click.option("--username", "-u", required=True, help="Username for the new user.")
@click.option("--password", "-p", required=True, help="Password for the new user.")
@click.option("--email", "-e", required=True, help="Email address for the new user.")
@click.option("--admin", "-a", is_flag=True, help="Specify if the user is an administrator.")
@click.option("--support", "-s", is_flag=True, help="Specify if the user is a support user.")
async def create_user_command(*args, **kwargs):
"""Create a user with given username, password and email.
If user with given username exists it's noop.
Expand All @@ -33,46 +45,37 @@ class CreateUserCommand(superdesk.Command):
$ python manage.py users:create -u admin -p admin -e '[email protected]' --admin
"""
return await create_user_command_handler(*args, **kwargs)

option_list = [
superdesk.Option("--username", "-u", dest="username", required=True),
superdesk.Option("--password", "-p", dest="password", required=True),
superdesk.Option("--email", "-e", dest="email", required=True),
superdesk.Option("--admin", "-a", dest="admin", required=False, action="store_true"),
superdesk.Option("--support", "-s", dest="support", required=False, action="store_true"),
]

async def run(self, username, password, email, admin=False, support=False):
# force type conversion to boolean
user_type = "administrator" if admin else "user"

userdata = {
"username": username,
"password": password,
"email": email,
"user_type": user_type,
"is_active": admin,
"is_support": support,
"needs_activation": not admin,
}

app = get_current_app().as_any()
async with app.test_request_context("/users", method="POST"):
if userdata.get("password", None) and not is_hashed(userdata.get("password")):
userdata["password"] = get_hash(
userdata.get("password"), get_app_config("BCRYPT_GENSALT_WORK_FACTOR", 12)
)
async def create_user_command_handler(username: str, password: str, email: str, admin=False, support=False):
user_type = "administrator" if admin else "user"
userdata = {
"username": username,
"password": password,
"email": email,
"user_type": user_type,
"is_active": admin,
"is_support": support,
"needs_activation": not admin,
}

app = get_current_app().as_any()

user = superdesk.get_resource_service("users").find_one(username=userdata.get("username"), req=None)
async with app.test_request_context("/users", method="POST"):
if userdata.get("password", None) and not is_hashed(userdata.get("password")):
userdata["password"] = get_hash(userdata.get("password"), get_app_config("BCRYPT_GENSALT_WORK_FACTOR", 12))

if user:
logger.info("user already exists %s" % (userdata))
else:
logger.info("creating user %s" % (userdata))
superdesk.get_resource_service("users").post([userdata])
logger.info("user saved %s" % (userdata))
user = superdesk.get_resource_service("users").find_one(username=userdata.get("username"), req=None)

if user:
logger.info("user already exists %s" % (userdata))
else:
logger.info("creating user %s" % (userdata))
superdesk.get_resource_service("users").post([userdata])
logger.info("user saved %s" % (userdata))

return userdata
return userdata


class ImportUsersCommand(superdesk.Command):
Expand Down Expand Up @@ -279,7 +282,6 @@ def run(self, username, password):
return encoded_token


superdesk.command("users:create", CreateUserCommand())
superdesk.command("users:import", ImportUsersCommand())
superdesk.command("users:hash_passwords", HashUserPasswordsCommand())
superdesk.command("users:get_auth_token", GetAuthTokenCommand())
2 changes: 1 addition & 1 deletion apps/prepopulate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from .app_prepopulate import PrepopulateService, PrepopulateResource
from .app_populate import AppPopulateCommand # NOQA
from .app_initialize import AppInitializeWithDataCommand # NOQA
from .app_initialize import app_initialize_data_command # NOQA
from .app_scaffold_data import AppScaffoldDataCommand # NOQA


Expand Down
Loading

0 comments on commit c90f21b

Please sign in to comment.