Skip to content

Commit

Permalink
LGA-A3459 - Add user creation/update/removal management commands (#67)
Browse files Browse the repository at this point in the history
Add user creation/update/removal management commands
  • Loading branch information
said-moj authored Dec 18, 2024
1 parent 1659e02 commit f56d142
Show file tree
Hide file tree
Showing 10 changed files with 433 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Scopes
---

## Scopes
# Scopes
There are four scopes which each correspond to a http method:
- create
- read
Expand All @@ -11,14 +11,12 @@ There are four scopes which each correspond to a http method:

Scopes are only assignable by the CLA team and you need to request which scopes you api requires as part of your account creation process

### Adding/updating user scopes
## Adding/updating user scopes
The following overwrites the current user scopes with the ones given
`python manage.py user-scopes-add <username> --scope=create --scope=read --scope=update`
`python manage.py set-user-scopes <username> --scope=create --scope=read --scope=update`

### Listing user scopes
To get a list of current scopes assign to a user do
`python manage.py user-scopes-list <username>`
## Listing user scopes
To get a list of current scopes assign to a user do `python manage.py list-user-scopes <username>`

### List routes with scopes
To get a list of all the routes which includes their scopes do
`python manage.py routes-list`
## List routes with scopes
To get a list of all the routes which includes their scopes do `python manage.py list-routes`
64 changes: 64 additions & 0 deletions docs/source/documentation/user-management.html.md.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
title: User Management
---

# User Management

## Adding users
Use `./manage.py add-user` to add users

```
Usage: manage.py add-user [OPTIONS] USERNAME

Arguments
* username TEXT [default: None] [required]

Options
* --email TEXT [default: None] [required]
* --full-name TEXT [default: None] [required]
* --password TEXT [default: None] [required]
--disable --no-disable [default: no-disable]
--help Show this message and exit.


Example: ./manage.py add-user test_user --email [email protected] --full-name "Test User" --password "password123"
```

## Updating users
Use `./manage.py update-user` to update users

```
Usage: manage.py update-user [OPTIONS] USERNAME

Arguments
* username TEXT [default: None] [required]

Options
--email TEXT [default: None]
--full-name TEXT [default: None]
--password TEXT [default: None]
--disable --no-disable [default: no-disable]
--enable --no-enable [default: no-enable]
--help Show this message and exit.


Example: ./manage.py update--user test_user --email [email protected] --full-name "Sir Test User"
```

## Delete users

Use `./manage.py delete-user` to delete users

```
Usage: manage.py delete-user [OPTIONS] USERNAME

Arguments
* username TEXT [default: None] [required]

Options
--help Show this message and exit.

```
## List users

Use `./manage.py list-user` to get a list of all users
153 changes: 137 additions & 16 deletions manage.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,176 @@
#!/usr/bin/env python

import json

import tabulate
import typer
from typing import List
from typing import List, Optional
from typing_extensions import Annotated
from sqlmodel import Session
from sqlmodel.sql.expression import select
from fastapi import Depends
from fastapi.params import Security
from app.models.users import User, UserScopes
from app.auth.security import get_password_hash
from app.db import get_session
from app.main import create_app


app = typer.Typer()

session: Session = next(get_session())

def init_session(typer_app: typer.Typer, session: Session) -> None:
typer_app.db_session = session


@app.command()
def user_scopes_add(
def set_user_scopes(
username: str, scope: Annotated[List[UserScopes], typer.Option()]
) -> None:
statement = select(User).where(User.username == username)
user: User = session.exec(statement).first()
user: User = app.db_session.exec(statement).first()
if not user:
print(f"User {user} does not exist")
return

new_scope_names = [item.value for item in scope]
previous_scope_names = [str(item) for item in user.scopes]
print(
f"Replacing user {user.username} current scopes {user.scopes} with new scopes {scope}..."
f"Replacing user {user.username} current scopes {previous_scope_names} with new scopes {new_scope_names}",
end="...",
)
user.scopes = scope
session.add(user)
session.commit()
print("Done")
app.db_session.add(user)
app.db_session.commit()
print("done")


@app.command()
def user_scopes_list(username: str) -> None:
def list_user_scopes(username: str) -> None:
statement = select(User).where(User.username == username)
user: User = session.exec(statement).first()
user: User = app.db_session.exec(statement).first()
if not user.scopes:
print(f"{user.username} has no scopes")
return
print(f"{user.username} has scopes {user.scopes}")


@app.command()
def routes_list():
def list_routes():
fastapi_app = create_app()
routes = {}
headers = ["Path", "Scopes"]
table = []
for route in fastapi_app.routes:
dependencies = getattr(route, "dependencies", [])
routes[route.path] = {"scopes": get_scopes_from_dependencies(dependencies)}
scopes = get_scopes_from_dependencies(dependencies)
table.append([route.path, scopes])

print(tabulate.tabulate(table, headers=headers, tablefmt="fancy_grid"))


@app.command()
def add_user(
username: Annotated[str, typer.Argument()],
email: Annotated[str, typer.Option()],
full_name: Annotated[str, typer.Option()],
password: Annotated[str, typer.Option()],
disable: Annotated[Optional[bool], typer.Option()] = False,
) -> None:
statement = select(User).where(User.username == username)
user: User = app.db_session.exec(statement).first()
if user:
print(f"{user.username} already exists")
return
user = User(
username=username,
hashed_password=get_password_hash(password),
full_name=full_name,
email=email,
disabled=disable,
)
app.db_session.add(user)
app.db_session.commit()
print("User has been added")


@app.command()
def update_user(
username: Annotated[Optional[str], typer.Argument()],
email: Annotated[Optional[str], typer.Option()] = None,
full_name: Annotated[Optional[str], typer.Option()] = None,
password: Annotated[str, typer.Option()] = None,
disable: Annotated[Optional[bool], typer.Option()] = None,
enable: Annotated[Optional[bool], typer.Option()] = None,
):
statement = select(User).where(User.username == username)
user: User = app.db_session.exec(statement).first()
if not user:
print(f"{username} does not exist")
comparison_table = []
headers = ["Previous value", "New value"]
if full_name:
comparison_table.append(
("Full-name:" + user.full_name, "Full-name:" + full_name)
)
user.full_name = full_name
if email:
comparison_table.append(("E-mail:" + user.email, "E-mail:" + email))
user.email = email

if disable:
disabled_str = "Yes" if user.disabled else "No"
comparison_table.append(("Disabled:" + disabled_str, "Disabled:Yes"))
user.disabled = True
elif enable:
disabled_str = "Yes" if user.disabled else "No"
comparison_table.append(("Disabled:" + disabled_str, "Disabled:No"))
user.disabled = False

if password:
comparison_table.append(("Password:************", "Password:************"))
user.hashed_password = get_password_hash(password)

output = json.dumps(routes, indent=4)
print(output)
print(tabulate.tabulate(comparison_table, headers=headers, tablefmt="fancy_grid"))
confirm = input("Do you wish to continue(y/n)? ")
if confirm == "y":
app.db_session.add(user)
app.db_session.commit()
print("User has been updated")
else:
print("Aborted")


@app.command()
def delete_user(username: Annotated[Optional[str], typer.Argument()]):
statement = select(User).where(User.username == username)
user: User = app.db_session.exec(statement).first()
if not user:
print(f"User {username} does not exist")
return

confirmed_username = input("Enter the name of the user to remove: ")
if username != confirmed_username:
print(f"{username} does match {confirmed_username}")
return

confirm = input(f"Are you sure you want to remove the user {username}?(y/n): ")
if confirm == "y":
app.db_session.delete(user)
app.db_session.commit()
print("User has been removed")
else:
print("User removal operation cancelled")


@app.command()
def list_users():
users: List[User] = app.db_session.execute(select(User)).all()
headers = ["Username", "Email", "Full Name", "Disabled", "Scopes"]
table = []
for user in users:
user = user[0]
disabled = "Y" if user.disabled else "N"
table.append([user.username, user.email, user.full_name, disabled, user.scopes])
print(tabulate.tabulate(table, headers=headers, tablefmt="fancy_grid"))


def get_scopes_from_dependencies(dependencies: List[Depends]):
Expand All @@ -65,4 +184,6 @@ def get_scopes_from_dependencies(dependencies: List[Depends]):


if __name__ == "__main__":
session = next(get_session())
init_session(app, session)
app()
3 changes: 2 additions & 1 deletion requirements/generated/requirements-development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pyjwt==2.9.0
pytest==7.4.4
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-multipart==0.0.12
python-multipart==0.0.19
pyyaml==6.0.1
rich==13.9.2
ruff==0.6.2
Expand All @@ -65,6 +65,7 @@ sqlalchemy[asyncio]==2.0.31
sqlmodel==0.0.22
starlette==0.40.0
structlog==24.4.0
tabulate==0.9.0
trufflehog3==3.0.10
typer==0.12.5
typing-extensions==4.12.2
Expand Down
3 changes: 2 additions & 1 deletion requirements/generated/requirements-production.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pydantic-core==2.20.1
pygments==2.18.0
pyjwt==2.9.0
python-dotenv==1.0.1
python-multipart==0.0.12
python-multipart==0.0.19
pyyaml==6.0.2
rich==13.9.2
sentry-sdk[fastapi]==2.11.0
Expand All @@ -46,6 +46,7 @@ sqlalchemy[asyncio]==2.0.31
sqlmodel==0.0.22
starlette==0.40.0
structlog==24.4.0
tabulate==0.9.0
typer==0.12.5
typing-extensions==4.12.2
urllib3==2.2.2
Expand Down
3 changes: 2 additions & 1 deletion requirements/generated/requirements-testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pyjwt==2.9.0
pytest==7.4.4
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-multipart==0.0.12
python-multipart==0.0.19
pyyaml==6.0.2
rich==13.9.2
sentry-sdk[fastapi]==2.11.0
Expand All @@ -53,6 +53,7 @@ sqlalchemy[asyncio]==2.0.31
sqlmodel==0.0.22
starlette==0.40.0
structlog==24.4.0
tabulate==0.9.0
typer==0.12.5
typing-extensions==4.12.2
urllib3==2.2.2
Expand Down
5 changes: 4 additions & 1 deletion requirements/source/requirements-base.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
fastapi[standard]==0.115.2
# Version of python-multipart installed by fastapi has a securiy issue https://avd.aquasec.com/nvd/cve-2024-53981
python-multipart>=0.0.19
starlette>=0.40.0
typing-extensions>=4.0
sqlalchemy[asyncio]
Expand All @@ -11,4 +13,5 @@ pyjwt
passlib
argon2_cffi
structlog
typer
typer
tabulate==0.9.0
Empty file.
Loading

0 comments on commit f56d142

Please sign in to comment.