Skip to content

Commit 4021c03

Browse files
[DOP-19931] Add manage_superusers script (#137)
1 parent 9ede4d5 commit 4021c03

File tree

19 files changed

+414
-113
lines changed

19 files changed

+414
-113
lines changed

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jobs:
5858
- name: Generate OpenAPI Schema
5959
run: |
6060
source .env.local
61-
poetry run python -m syncmaster.backend.export_openapi_schema openapi.json
61+
poetry run python -m syncmaster.backend.scripts.export_openapi_schema openapi.json
6262
6363
- name: Fix logo in Readme
6464
run: |

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ repos:
4040
- id: chmod
4141
args: ['644']
4242
exclude_types: [shell]
43-
exclude: ^(.*__main__\.py|syncmaster/backend/export_openapi_schema\.py)$
43+
exclude: ^(.*__main__\.py|syncmaster/backend/scripts/export_openapi_schema\.py)$
4444
- id: chmod
4545
args: ['755']
4646
types: [shell]
4747
- id: chmod
4848
args: ['755']
49-
files: ^(.*__main__\.py|syncmaster/backend/export_openapi_schema\.py)$
49+
files: ^(.*__main__\.py|syncmaster/backend/scripts/export_openapi_schema\.py)$
5050
- id: insert-license
5151
files: .*\.py$
5252
exclude: ^(syncmaster/backend/dependencies/stub.py|docs/.*\.py|tests/.*\.py)$

.readthedocs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ build:
1717
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH python -m poetry install --no-root --all-extras --with docs --without dev,test
1818
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH python -m poetry show -v
1919
- python -m pip list -v
20-
- SYNCMASTER__DATABASE__URL=postgresql+psycopg://fake:[email protected]:5432/fake SYNCMASTER__SERVER__SESSION__SECRET_KEY=session_secret_key SYNCMASTER__BROKER__URL=amqp://fake:faket@fake:5672/ SYNCMASTER__CRYPTO_KEY=crypto_key SYNCMASTER__AUTH__ACCESS_TOKEN__SECRET_KEY=fakepython python -m syncmaster.backend.export_openapi_schema docs/_static/openapi.json
20+
- SYNCMASTER__DATABASE__URL=postgresql+psycopg://fake:[email protected]:5432/fake SYNCMASTER__SERVER__SESSION__SECRET_KEY=session_secret_key SYNCMASTER__BROKER__URL=amqp://fake:faket@fake:5672/ SYNCMASTER__CRYPTO_KEY=crypto_key SYNCMASTER__AUTH__ACCESS_TOKEN__SECRET_KEY=fakepython python -m syncmaster.backend.scripts.export_openapi_schema docs/_static/openapi.json
2121

2222
sphinx:
2323
configuration: docs/conf.py

docker-compose.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ services:
3636
context: .
3737
ports:
3838
- 8000:8000
39-
# PROMETHEUS_MULTIPROC_DIR is required for multiple workers, see:
40-
# https://prometheus.github.io/client_python/multiprocess/
4139
environment:
40+
# list here usernames which should be assigned SUPERUSER role on application start
41+
SYNCMASTER__ENTRYPOINT__SUPERUSERS: admin
42+
# PROMETHEUS_MULTIPROC_DIR is required for multiple workers, see:
43+
# https://prometheus.github.io/client_python/multiprocess/
4244
PROMETHEUS_MULTIPROC_DIR: /tmp/prometheus-metrics
4345
# tmpfs dir is cleaned up each container restart
4446
tmpfs:

docker/entrypoint_backend.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,11 @@ set -e
33

44
python -m syncmaster.db.migrations upgrade head
55

6+
if [[ "x${SYNCMASTER__ENTRYPOINT__SUPERUSERS}" != "x" ]]; then
7+
superusers=$(echo "${SYNCMASTER__ENTRYPOINT__SUPERUSERS}" | tr "," " ")
8+
python -m syncmaster.backend.scripts.manage_superusers add ${superusers}
9+
python -m syncmaster.backend.scripts.manage_superusers list
10+
fi
11+
612
# exec is required to forward all signals to the main process
713
exec python -m syncmaster.backend --host 0.0.0.0 --port 8000 "$@"

docs/backend/install.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,29 @@ Options can be set via ``.env`` file or ``environment`` section in ``docker-comp
2828

2929
After container is started and ready, open http://localhost:8000/docs.
3030

31+
Managing superusers
32+
^^^^^^^^^^^^^^^^^^^
33+
34+
Users listed in ``SYNCMASTER__ENTRYPOINT__SUPERUSERS`` env variable will be automatically promoted to ``SUPERUSER`` role.
35+
36+
Adding superusers:
37+
38+
.. code-block:: console
39+
40+
$ python -m syncmaster.backend.scripts.manage_superusers add <username1> <username2>
41+
42+
Removing superusers:
43+
44+
.. code-block:: console
45+
46+
$ python -m syncmaster.backend.scripts.manage_superusers remove <username1> <username2>
47+
48+
Viewing list of superusers:
49+
50+
.. code-block:: console
51+
52+
$ python -m syncmaster.backend.scripts.manage_superusers list
53+
3154
Without docker
3255
--------------
3356

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add new environment variable ``SYNCMASTER__ENTRYPOINT__SUPERUSERS`` to Docker image entrypoint. Here you can pass usernames which should be automatically promoted to SUPERUSER role during backend startup.

syncmaster/backend/handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def http_exception_handler(request: Request, exc: HTTPException) -> Response:
6363
return exception_json_response(
6464
status=exc.status_code,
6565
content=content,
66-
headers=exc.headers,
66+
headers=exc.headers, # type: ignore[arg-type]
6767
)
6868

6969

File renamed without changes.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/bin/env python3
2+
3+
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
4+
# SPDX-License-Identifier: Apache-2.0
5+
from __future__ import annotations
6+
7+
import argparse
8+
import asyncio
9+
import logging
10+
11+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
12+
from sqlalchemy.future import select
13+
14+
from syncmaster.backend.middlewares import setup_logging
15+
from syncmaster.backend.settings import BackendSettings as Settings
16+
from syncmaster.db.models.user import User
17+
18+
19+
async def add_superusers(session: AsyncSession, usernames: list[str]) -> None:
20+
logging.info("Adding superusers:")
21+
result = await session.execute(select(User).where(User.username.in_(usernames)).order_by(User.username))
22+
users = result.scalars().all()
23+
24+
not_found = set(usernames)
25+
for user in users:
26+
user.is_superuser = True
27+
logging.info(" %r", user.username)
28+
not_found.discard(user.username)
29+
30+
if not_found:
31+
for username in not_found:
32+
session.add(User(username=username, email=f"{username}@mts.ru", is_active=True, is_superuser=True))
33+
logging.info(" %r (new user)", username)
34+
35+
await session.commit()
36+
logging.info("Done.")
37+
38+
39+
async def remove_superusers(session: AsyncSession, usernames: list[str]) -> None:
40+
logging.info("Removing superusers:")
41+
result = await session.execute(select(User).where(User.username.in_(usernames)).order_by(User.username))
42+
users = result.scalars().all()
43+
44+
not_found = set(usernames)
45+
for user in users:
46+
logging.info(" %r", user.username)
47+
user.is_superuser = False
48+
not_found.discard(user.username)
49+
50+
if not_found:
51+
logging.info("Not found:")
52+
for username in not_found:
53+
logging.info(" %r", username)
54+
55+
await session.commit()
56+
logging.info("Done.")
57+
58+
59+
async def list_superusers(session: AsyncSession) -> None:
60+
result = await session.execute(select(User).filter_by(is_superuser=True).order_by(User.username))
61+
superusers = result.scalars().all()
62+
logging.info("Listing users with SUPERUSER role:")
63+
for superuser in superusers:
64+
logging.info(" %r", superuser.username)
65+
logging.info("Done.")
66+
67+
68+
def create_parser() -> argparse.ArgumentParser:
69+
parser = argparse.ArgumentParser(description="Manage superusers.")
70+
subparsers = parser.add_subparsers(dest="command", required=True)
71+
72+
parser_add = subparsers.add_parser("add", help="Add superuser privileges to users")
73+
parser_add.add_argument("usernames", nargs="+", help="Usernames to add as superusers")
74+
parser_add.set_defaults(func=add_superusers)
75+
76+
parser_remove = subparsers.add_parser("remove", help="Remove superuser privileges from users")
77+
parser_remove.add_argument("usernames", nargs="+", help="Usernames to remove from superusers")
78+
parser_remove.set_defaults(func=remove_superusers)
79+
80+
parser_list = subparsers.add_parser("list", help="List all superusers")
81+
parser_list.set_defaults(func=list_superusers)
82+
83+
return parser
84+
85+
86+
async def main(args: argparse.Namespace, session: AsyncSession) -> None:
87+
async with session:
88+
if args.command == "list":
89+
# 'list' command does not take additional arguments
90+
await args.func(session)
91+
else:
92+
await args.func(session, args.usernames)
93+
94+
95+
if __name__ == "__main__":
96+
settings = Settings()
97+
if settings.logging.setup:
98+
setup_logging(settings.logging.get_log_config_path())
99+
100+
engine = create_async_engine(settings.database.url)
101+
SessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
102+
parser = create_parser()
103+
args = parser.parse_args()
104+
session = SessionLocal()
105+
asyncio.run(main(args, session))

0 commit comments

Comments
 (0)