goosebit 0.2.5__py3-none-any.whl → 0.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- goosebit/__init__.py +41 -7
- goosebit/api/telemetry/metrics.py +1 -5
- goosebit/api/v1/devices/device/responses.py +1 -0
- goosebit/api/v1/devices/device/routes.py +8 -8
- goosebit/api/v1/devices/requests.py +20 -0
- goosebit/api/v1/devices/routes.py +68 -8
- goosebit/api/v1/download/routes.py +14 -3
- goosebit/api/v1/rollouts/routes.py +5 -4
- goosebit/api/v1/routes.py +2 -1
- goosebit/api/v1/settings/routes.py +14 -0
- goosebit/api/v1/settings/users/__init__.py +1 -0
- goosebit/api/v1/settings/users/requests.py +16 -0
- goosebit/api/v1/settings/users/responses.py +7 -0
- goosebit/api/v1/settings/users/routes.py +56 -0
- goosebit/api/v1/software/routes.py +18 -14
- goosebit/auth/__init__.py +49 -13
- goosebit/auth/permissions.py +80 -0
- goosebit/db/config.py +57 -1
- goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
- goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
- goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
- goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
- goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
- goosebit/db/models.py +19 -8
- goosebit/db/pg_ssl_context.py +51 -0
- goosebit/device_manager.py +262 -0
- goosebit/plugins/__init__.py +32 -0
- goosebit/schema/devices.py +8 -5
- goosebit/schema/plugins.py +67 -0
- goosebit/schema/updates.py +15 -0
- goosebit/schema/users.py +9 -0
- goosebit/settings/__init__.py +0 -3
- goosebit/settings/schema.py +60 -14
- goosebit/storage/__init__.py +62 -0
- goosebit/storage/base.py +14 -0
- goosebit/storage/filesystem.py +111 -0
- goosebit/storage/s3.py +104 -0
- goosebit/ui/bff/common/columns.py +50 -0
- goosebit/ui/bff/common/responses.py +1 -0
- goosebit/ui/bff/devices/device/__init__.py +1 -0
- goosebit/ui/bff/devices/device/routes.py +17 -0
- goosebit/ui/bff/devices/requests.py +1 -0
- goosebit/ui/bff/devices/routes.py +49 -46
- goosebit/ui/bff/download/routes.py +14 -3
- goosebit/ui/bff/rollouts/routes.py +32 -4
- goosebit/ui/bff/routes.py +2 -1
- goosebit/ui/bff/settings/__init__.py +1 -0
- goosebit/ui/bff/settings/routes.py +20 -0
- goosebit/ui/bff/settings/users/__init__.py +1 -0
- goosebit/ui/bff/settings/users/responses.py +33 -0
- goosebit/ui/bff/settings/users/routes.py +80 -0
- goosebit/ui/bff/software/routes.py +40 -12
- goosebit/ui/nav.py +12 -2
- goosebit/ui/routes.py +66 -13
- goosebit/ui/static/js/devices.js +32 -24
- goosebit/ui/static/js/login.js +21 -5
- goosebit/ui/static/js/logs.js +7 -22
- goosebit/ui/static/js/rollouts.js +31 -30
- goosebit/ui/static/js/settings.js +322 -0
- goosebit/ui/static/js/setup.js +28 -0
- goosebit/ui/static/js/software.js +127 -121
- goosebit/ui/static/js/util.js +25 -4
- goosebit/ui/templates/__init__.py +10 -1
- goosebit/ui/templates/login.html.jinja +5 -0
- goosebit/ui/templates/nav.html.jinja +13 -5
- goosebit/ui/templates/rollouts.html.jinja +4 -22
- goosebit/ui/templates/settings.html.jinja +88 -0
- goosebit/ui/templates/setup.html.jinja +71 -0
- goosebit/ui/templates/software.html.jinja +0 -11
- goosebit/updater/controller/v1/routes.py +119 -77
- goosebit/updater/routes.py +83 -8
- goosebit/updates/__init__.py +24 -31
- goosebit/updates/swdesc.py +15 -8
- goosebit/users/__init__.py +63 -0
- goosebit/util/__init__.py +0 -0
- goosebit/util/path.py +42 -0
- goosebit/util/version.py +92 -0
- goosebit-0.2.6.dist-info/METADATA +280 -0
- goosebit-0.2.6.dist-info/RECORD +133 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.6.dist-info}/WHEEL +1 -1
- goosebit/realtime/logs.py +0 -42
- goosebit/realtime/routes.py +0 -13
- goosebit/updater/manager.py +0 -325
- goosebit-0.2.5.dist-info/METADATA +0 -189
- goosebit-0.2.5.dist-info/RECORD +0 -99
- /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.6.dist-info}/LICENSE +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.6.dist-info}/entry_points.txt +0 -0
goosebit/__init__.py
CHANGED
@@ -13,13 +13,14 @@ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor as Instrum
|
|
13
13
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
14
14
|
from tortoise.exceptions import ValidationError
|
15
15
|
|
16
|
-
from goosebit import api, db,
|
17
|
-
from goosebit.api.telemetry import metrics
|
16
|
+
from goosebit import api, db, plugins, ui, updater
|
18
17
|
from goosebit.auth import get_user_from_request, login_user, redirect_if_authenticated
|
19
|
-
from goosebit.
|
18
|
+
from goosebit.device_manager import DeviceManager
|
19
|
+
from goosebit.settings import PWD_CXT, config
|
20
20
|
from goosebit.ui.nav import nav
|
21
21
|
from goosebit.ui.static import static
|
22
22
|
from goosebit.ui.templates import templates
|
23
|
+
from goosebit.users import create_initial_user
|
23
24
|
|
24
25
|
logger = getLogger(__name__)
|
25
26
|
|
@@ -29,7 +30,9 @@ async def lifespan(_: FastAPI):
|
|
29
30
|
db_ready = await db.init()
|
30
31
|
if not db_ready:
|
31
32
|
logger.exception("DB does not exist, try running `poetry run aerich upgrade`.")
|
32
|
-
|
33
|
+
|
34
|
+
logger.debug(f"Initialized storage backend: {config.storage.backend}")
|
35
|
+
|
33
36
|
if db_ready:
|
34
37
|
yield
|
35
38
|
await db.close()
|
@@ -57,10 +60,30 @@ app = FastAPI(
|
|
57
60
|
app.include_router(updater.router)
|
58
61
|
app.include_router(ui.router)
|
59
62
|
app.include_router(api.router)
|
60
|
-
app.include_router(realtime.router)
|
61
63
|
app.mount("/static", static, name="static")
|
62
64
|
Instrumentor.instrument_app(app)
|
63
65
|
|
66
|
+
for plugin in plugins.load():
|
67
|
+
if plugin.middleware is not None:
|
68
|
+
logger.info(f"Adding middleware for plugin: {plugin.name}")
|
69
|
+
app.add_middleware(plugin.middleware)
|
70
|
+
if plugin.router is not None:
|
71
|
+
logger.info(f"Adding routing handler for plugin: {plugin.name}")
|
72
|
+
app.include_router(router=plugin.router, prefix=plugin.url_prefix)
|
73
|
+
if plugin.db_model_path is not None:
|
74
|
+
logger.info(f"Adding db handler for plugin: {plugin.name}")
|
75
|
+
db.config.add_models(plugin.db_model_path)
|
76
|
+
if plugin.static_files is not None:
|
77
|
+
logger.info(f"Adding static files handler for plugin: {plugin.name}")
|
78
|
+
app.mount(f"{plugin.url_prefix}/static", plugin.static_files, name=plugin.static_files_name)
|
79
|
+
if plugin.templates is not None:
|
80
|
+
logger.info(f"Adding template handler for plugin: {plugin.name}")
|
81
|
+
templates.add_template_handler(plugin.templates)
|
82
|
+
if plugin.update_source_hook is not None:
|
83
|
+
DeviceManager.add_update_source(plugin.update_source_hook)
|
84
|
+
if plugin.config_data_hook is not None:
|
85
|
+
DeviceManager.add_config_callback(plugin.config_data_hook)
|
86
|
+
|
64
87
|
|
65
88
|
# Custom exception handler for Tortoise ValidationError
|
66
89
|
@app.exception_handler(ValidationError)
|
@@ -105,7 +128,18 @@ async def login_get(request: Request):
|
|
105
128
|
|
106
129
|
@app.post("/login", tags=["login"])
|
107
130
|
async def login_post(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
108
|
-
return {"access_token": login_user(form_data.username, form_data.password), "token_type": "bearer"}
|
131
|
+
return {"access_token": await login_user(form_data.username, form_data.password), "token_type": "bearer"}
|
132
|
+
|
133
|
+
|
134
|
+
@app.get("/setup", include_in_schema=False)
|
135
|
+
async def setup_get(request: Request):
|
136
|
+
return templates.TemplateResponse(request, "setup.html.jinja")
|
137
|
+
|
138
|
+
|
139
|
+
@app.post("/setup", include_in_schema=False)
|
140
|
+
async def setup_post(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
141
|
+
await create_initial_user(form_data.username, PWD_CXT.hash(form_data.password))
|
142
|
+
return {"access_token": await login_user(form_data.username, form_data.password), "token_type": "bearer"}
|
109
143
|
|
110
144
|
|
111
145
|
@app.get("/logout", include_in_schema=False)
|
@@ -115,7 +149,7 @@ async def logout(request: Request):
|
|
115
149
|
return resp
|
116
150
|
|
117
151
|
|
118
|
-
@app.get("/docs")
|
152
|
+
@app.get("/docs", include_in_schema=False)
|
119
153
|
async def swagger_docs(request: Request):
|
120
154
|
return get_swagger_ui_html(
|
121
155
|
title="gooseBit docs",
|
@@ -2,7 +2,7 @@ from opentelemetry import metrics
|
|
2
2
|
from opentelemetry.sdk.metrics import MeterProvider
|
3
3
|
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
4
4
|
|
5
|
-
from goosebit.settings import
|
5
|
+
from goosebit.settings import config
|
6
6
|
|
7
7
|
from . import prometheus
|
8
8
|
|
@@ -28,7 +28,3 @@ users_count = meter.create_gauge(
|
|
28
28
|
"users.count",
|
29
29
|
description="The number of registered users",
|
30
30
|
)
|
31
|
-
|
32
|
-
|
33
|
-
async def init():
|
34
|
-
users_count.set(len(USERS))
|
@@ -5,17 +5,18 @@ from fastapi.requests import Request
|
|
5
5
|
|
6
6
|
from goosebit.api.v1.devices.device.responses import DeviceLogResponse, DeviceResponse
|
7
7
|
from goosebit.auth import validate_user_permissions
|
8
|
-
from goosebit.
|
8
|
+
from goosebit.auth.permissions import GOOSEBIT_PERMISSIONS
|
9
|
+
from goosebit.db import Device
|
10
|
+
from goosebit.device_manager import get_device
|
9
11
|
|
10
12
|
router = APIRouter(prefix="/{dev_id}")
|
11
13
|
|
12
14
|
|
13
15
|
@router.get(
|
14
16
|
"",
|
15
|
-
dependencies=[Security(validate_user_permissions, scopes=["device
|
17
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["device"]["read"]()])],
|
16
18
|
)
|
17
|
-
async def device_get(_: Request,
|
18
|
-
device = await updater.get_device()
|
19
|
+
async def device_get(_: Request, device: Device = Depends(get_device)) -> DeviceResponse:
|
19
20
|
if device is None:
|
20
21
|
raise HTTPException(404)
|
21
22
|
await device.fetch_related("assigned_software", "hardware")
|
@@ -24,10 +25,9 @@ async def device_get(_: Request, updater: UpdateManager = Depends(get_update_man
|
|
24
25
|
|
25
26
|
@router.get(
|
26
27
|
"/log",
|
27
|
-
dependencies=[Security(validate_user_permissions, scopes=["device
|
28
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["device"]["read"]()])],
|
28
29
|
)
|
29
|
-
async def device_logs(_: Request,
|
30
|
-
device = await updater.get_device()
|
30
|
+
async def device_logs(_: Request, device: Device = Depends(get_device)) -> DeviceLogResponse:
|
31
31
|
if device is None:
|
32
32
|
raise HTTPException(404)
|
33
|
-
return DeviceLogResponse(log=device.last_log)
|
33
|
+
return DeviceLogResponse(log=device.last_log, progress=device.progress)
|
@@ -5,3 +5,23 @@ from pydantic import BaseModel
|
|
5
5
|
|
6
6
|
class DevicesDeleteRequest(BaseModel):
|
7
7
|
devices: list[str]
|
8
|
+
|
9
|
+
|
10
|
+
class DevicesPatchRequest(BaseModel):
|
11
|
+
devices: list[str]
|
12
|
+
software: str | None = None
|
13
|
+
name: str | None = None
|
14
|
+
pinned: bool | None = None
|
15
|
+
feed: str | None = None
|
16
|
+
force_update: bool | None = None
|
17
|
+
auth_token: str | None = None
|
18
|
+
|
19
|
+
|
20
|
+
class DevicesPutRequest(BaseModel):
|
21
|
+
devices: list[str]
|
22
|
+
software: str | None = None
|
23
|
+
name: str | None = None
|
24
|
+
pinned: bool | None = None
|
25
|
+
feed: str | None = None
|
26
|
+
force_update: bool | None = None
|
27
|
+
auth_token: str | None = None
|
@@ -1,19 +1,21 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
+
from http.client import HTTPException
|
4
5
|
|
5
6
|
from fastapi import APIRouter, Security
|
6
7
|
from fastapi.requests import Request
|
7
8
|
|
8
9
|
from goosebit.api.responses import StatusResponse
|
9
10
|
from goosebit.auth import validate_user_permissions
|
10
|
-
from goosebit.
|
11
|
+
from goosebit.auth.permissions import GOOSEBIT_PERMISSIONS
|
12
|
+
from goosebit.db.models import Device, Software, UpdateModeEnum
|
13
|
+
from goosebit.device_manager import DeviceManager, get_device
|
11
14
|
from goosebit.schema.devices import DeviceSchema
|
12
15
|
from goosebit.schema.software import SoftwareSchema
|
13
|
-
from goosebit.updater.manager import delete_devices, get_update_manager
|
14
16
|
|
15
17
|
from . import device
|
16
|
-
from .requests import DevicesDeleteRequest
|
18
|
+
from .requests import DevicesDeleteRequest, DevicesPatchRequest, DevicesPutRequest
|
17
19
|
from .responses import DevicesResponse
|
18
20
|
|
19
21
|
router = APIRouter(prefix="/devices", tags=["devices"])
|
@@ -21,15 +23,15 @@ router = APIRouter(prefix="/devices", tags=["devices"])
|
|
21
23
|
|
22
24
|
@router.get(
|
23
25
|
"",
|
24
|
-
dependencies=[Security(validate_user_permissions, scopes=["device
|
26
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["device"]["read"]()])],
|
25
27
|
)
|
26
28
|
async def devices_get(_: Request) -> DevicesResponse:
|
27
29
|
devices = await Device.all().prefetch_related("hardware", "assigned_software", "assigned_software__compatibility")
|
28
30
|
response = DevicesResponse(devices=devices)
|
29
31
|
|
30
32
|
async def set_assigned_sw(d: DeviceSchema):
|
31
|
-
|
32
|
-
_, target = await
|
33
|
+
device = await get_device(d.id)
|
34
|
+
_, target = await DeviceManager.get_update(device)
|
33
35
|
if target is not None:
|
34
36
|
await target.fetch_related("compatibility")
|
35
37
|
d.assigned_software = SoftwareSchema.model_validate(target)
|
@@ -41,10 +43,68 @@ async def devices_get(_: Request) -> DevicesResponse:
|
|
41
43
|
|
42
44
|
@router.delete(
|
43
45
|
"",
|
44
|
-
dependencies=[Security(validate_user_permissions, scopes=["device
|
46
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["device"]["delete"]()])],
|
45
47
|
)
|
46
48
|
async def devices_delete(_: Request, config: DevicesDeleteRequest) -> StatusResponse:
|
47
|
-
await delete_devices(config.devices)
|
49
|
+
await DeviceManager.delete_devices(config.devices)
|
50
|
+
return StatusResponse(success=True)
|
51
|
+
|
52
|
+
|
53
|
+
@router.patch(
|
54
|
+
"",
|
55
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["device"]["write"]()])],
|
56
|
+
)
|
57
|
+
async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusResponse:
|
58
|
+
for dev_id in config.devices:
|
59
|
+
if await Device.get_or_none(id=dev_id) is None:
|
60
|
+
raise HTTPException(404, f"Device with ID {dev_id} not found")
|
61
|
+
device = await DeviceManager.get_device(dev_id)
|
62
|
+
if config.feed is not None:
|
63
|
+
await DeviceManager.update_feed(device, config.feed)
|
64
|
+
if config.software is not None:
|
65
|
+
if config.software == "rollout":
|
66
|
+
await DeviceManager.update_update(device, UpdateModeEnum.ROLLOUT, None)
|
67
|
+
elif config.software == "latest":
|
68
|
+
await DeviceManager.update_update(device, UpdateModeEnum.LATEST, None)
|
69
|
+
else:
|
70
|
+
software = await Software.get_or_none(id=config.software)
|
71
|
+
await DeviceManager.update_update(device, UpdateModeEnum.ASSIGNED, software)
|
72
|
+
if config.pinned is not None:
|
73
|
+
await DeviceManager.update_update(device, UpdateModeEnum.PINNED, None)
|
74
|
+
if config.name is not None:
|
75
|
+
await DeviceManager.update_name(device, config.name)
|
76
|
+
if config.force_update is not None:
|
77
|
+
await DeviceManager.update_force_update(device, config.force_update)
|
78
|
+
if config.auth_token is not None:
|
79
|
+
await DeviceManager.update_auth_token(device, config.auth_token)
|
80
|
+
return StatusResponse(success=True)
|
81
|
+
|
82
|
+
|
83
|
+
@router.put(
|
84
|
+
"",
|
85
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["device"]["write"]()])],
|
86
|
+
)
|
87
|
+
async def devices_put(_: Request, config: DevicesPutRequest) -> StatusResponse:
|
88
|
+
for dev_id in config.devices:
|
89
|
+
device = await DeviceManager.get_device(dev_id)
|
90
|
+
if config.feed is not None:
|
91
|
+
await DeviceManager.update_feed(device, config.feed)
|
92
|
+
if config.software is not None:
|
93
|
+
if config.software == "rollout":
|
94
|
+
await DeviceManager.update_update(device, UpdateModeEnum.ROLLOUT, None)
|
95
|
+
elif config.software == "latest":
|
96
|
+
await DeviceManager.update_update(device, UpdateModeEnum.LATEST, None)
|
97
|
+
else:
|
98
|
+
software = await Software.get_or_none(id=config.software)
|
99
|
+
await DeviceManager.update_update(device, UpdateModeEnum.ASSIGNED, software)
|
100
|
+
if config.pinned is not None:
|
101
|
+
await DeviceManager.update_update(device, UpdateModeEnum.PINNED, None)
|
102
|
+
if config.name is not None:
|
103
|
+
await DeviceManager.update_name(device, config.name)
|
104
|
+
if config.force_update is not None:
|
105
|
+
await DeviceManager.update_force_update(device, config.force_update)
|
106
|
+
if config.auth_token is not None:
|
107
|
+
await DeviceManager.update_auth_token(device, config.auth_token)
|
48
108
|
return StatusResponse(success=True)
|
49
109
|
|
50
110
|
|
@@ -1,8 +1,9 @@
|
|
1
1
|
from fastapi import APIRouter, HTTPException
|
2
2
|
from fastapi.requests import Request
|
3
|
-
from fastapi.responses import FileResponse, RedirectResponse
|
3
|
+
from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
|
4
4
|
|
5
5
|
from goosebit.db.models import Software
|
6
|
+
from goosebit.storage import storage
|
6
7
|
|
7
8
|
router = APIRouter(prefix="/download", tags=["download"])
|
8
9
|
|
@@ -18,5 +19,15 @@ async def download_file(_: Request, file_id: int):
|
|
18
19
|
media_type="application/octet-stream",
|
19
20
|
filename=software.path.name,
|
20
21
|
)
|
21
|
-
|
22
|
-
|
22
|
+
|
23
|
+
try:
|
24
|
+
url = await storage.get_download_url(software.uri)
|
25
|
+
return RedirectResponse(url=url)
|
26
|
+
except Exception:
|
27
|
+
# Fallback to streaming if redirect fails.
|
28
|
+
file_stream = storage.get_file_stream(software.uri)
|
29
|
+
return StreamingResponse(
|
30
|
+
file_stream,
|
31
|
+
media_type="application/octet-stream",
|
32
|
+
headers={"Content-Disposition": f"attachment; filename={software.path.name}"},
|
33
|
+
)
|
@@ -3,6 +3,7 @@ from fastapi.requests import Request
|
|
3
3
|
|
4
4
|
from goosebit.api.responses import StatusResponse
|
5
5
|
from goosebit.auth import validate_user_permissions
|
6
|
+
from goosebit.auth.permissions import GOOSEBIT_PERMISSIONS
|
6
7
|
from goosebit.db.models import Rollout, Software
|
7
8
|
|
8
9
|
from .requests import RolloutsDeleteRequest, RolloutsPatchRequest, RolloutsPutRequest
|
@@ -13,7 +14,7 @@ router = APIRouter(prefix="/rollouts", tags=["rollouts"])
|
|
13
14
|
|
14
15
|
@router.get(
|
15
16
|
"",
|
16
|
-
dependencies=[Security(validate_user_permissions, scopes=["rollout
|
17
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["rollout"]["read"]()])],
|
17
18
|
)
|
18
19
|
async def rollouts_get(_: Request) -> RolloutsResponse:
|
19
20
|
rollouts = await Rollout.all().prefetch_related("software", "software__compatibility")
|
@@ -22,7 +23,7 @@ async def rollouts_get(_: Request) -> RolloutsResponse:
|
|
22
23
|
|
23
24
|
@router.post(
|
24
25
|
"",
|
25
|
-
dependencies=[Security(validate_user_permissions, scopes=["rollout
|
26
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["rollout"]["write"]()])],
|
26
27
|
)
|
27
28
|
async def rollouts_put(_: Request, rollout: RolloutsPutRequest) -> RolloutsPutResponse:
|
28
29
|
software = await Software.filter(id=rollout.software_id)
|
@@ -38,7 +39,7 @@ async def rollouts_put(_: Request, rollout: RolloutsPutRequest) -> RolloutsPutRe
|
|
38
39
|
|
39
40
|
@router.patch(
|
40
41
|
"",
|
41
|
-
dependencies=[Security(validate_user_permissions, scopes=["rollout
|
42
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["rollout"]["write"]()])],
|
42
43
|
)
|
43
44
|
async def rollouts_patch(_: Request, rollouts: RolloutsPatchRequest) -> StatusResponse:
|
44
45
|
await Rollout.filter(id__in=rollouts.ids).update(paused=rollouts.paused)
|
@@ -47,7 +48,7 @@ async def rollouts_patch(_: Request, rollouts: RolloutsPatchRequest) -> StatusRe
|
|
47
48
|
|
48
49
|
@router.delete(
|
49
50
|
"",
|
50
|
-
dependencies=[Security(validate_user_permissions, scopes=["rollout
|
51
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["rollout"]["delete"]()])],
|
51
52
|
)
|
52
53
|
async def rollouts_delete(_: Request, rollouts: RolloutsDeleteRequest) -> StatusResponse:
|
53
54
|
await Rollout.filter(id__in=rollouts.ids).delete()
|
goosebit/api/v1/routes.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
from fastapi import APIRouter
|
2
2
|
|
3
|
-
from . import devices, download, rollouts, software
|
3
|
+
from . import devices, download, rollouts, settings, software
|
4
4
|
|
5
5
|
router = APIRouter(prefix="/v1")
|
6
6
|
router.include_router(software.router)
|
7
7
|
router.include_router(devices.router)
|
8
8
|
router.include_router(rollouts.router)
|
9
9
|
router.include_router(download.router)
|
10
|
+
router.include_router(settings.router)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from fastapi import APIRouter
|
2
|
+
|
3
|
+
from goosebit.auth.permissions import GOOSEBIT_PERMISSIONS, Permission
|
4
|
+
|
5
|
+
from . import users
|
6
|
+
|
7
|
+
router = APIRouter(prefix="/settings", tags=["settings"])
|
8
|
+
|
9
|
+
router.include_router(users.router)
|
10
|
+
|
11
|
+
|
12
|
+
@router.get("/permissions", response_model_exclude_none=True)
|
13
|
+
async def settings_permissions_get() -> Permission:
|
14
|
+
return GOOSEBIT_PERMISSIONS
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa: F401
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
|
3
|
+
|
4
|
+
class UsersPutRequest(BaseModel):
|
5
|
+
username: str
|
6
|
+
password: str
|
7
|
+
permissions: list[str]
|
8
|
+
|
9
|
+
|
10
|
+
class UsersPatchRequest(BaseModel):
|
11
|
+
usernames: list[str]
|
12
|
+
enabled: bool
|
13
|
+
|
14
|
+
|
15
|
+
class UsersDeleteRequest(BaseModel):
|
16
|
+
usernames: list[str]
|
@@ -0,0 +1,56 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from fastapi import APIRouter, HTTPException, Security
|
4
|
+
from fastapi.requests import Request
|
5
|
+
|
6
|
+
from goosebit.api.responses import StatusResponse
|
7
|
+
from goosebit.auth import validate_user_permissions
|
8
|
+
from goosebit.auth.permissions import GOOSEBIT_PERMISSIONS
|
9
|
+
from goosebit.db.models import User
|
10
|
+
from goosebit.users import UserManager, create_user
|
11
|
+
|
12
|
+
from .requests import UsersDeleteRequest, UsersPatchRequest, UsersPutRequest
|
13
|
+
from .responses import SettingsUsersResponse
|
14
|
+
|
15
|
+
router = APIRouter(prefix="/users")
|
16
|
+
|
17
|
+
|
18
|
+
@router.get(
|
19
|
+
"",
|
20
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["settings"]["users"]["read"]()])],
|
21
|
+
)
|
22
|
+
async def settings_users_get(_: Request) -> SettingsUsersResponse:
|
23
|
+
users = await User.all()
|
24
|
+
return SettingsUsersResponse(users=users)
|
25
|
+
|
26
|
+
|
27
|
+
@router.post(
|
28
|
+
"",
|
29
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["settings"]["users"]["write"]()])],
|
30
|
+
)
|
31
|
+
async def settings_users_put(_: Request, user: UsersPutRequest) -> StatusResponse:
|
32
|
+
await create_user(username=user.username, password=user.password, permissions=user.permissions)
|
33
|
+
return StatusResponse(success=True)
|
34
|
+
|
35
|
+
|
36
|
+
@router.delete(
|
37
|
+
"",
|
38
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["settings"]["users"]["delete"]()])],
|
39
|
+
)
|
40
|
+
async def settings_users_delete(_: Request, config: UsersDeleteRequest) -> StatusResponse:
|
41
|
+
await UserManager.delete_users(config.usernames)
|
42
|
+
return StatusResponse(success=True)
|
43
|
+
|
44
|
+
|
45
|
+
@router.patch(
|
46
|
+
"",
|
47
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["settings"]["users"]["delete"]()])],
|
48
|
+
)
|
49
|
+
async def settings_users_patch(_: Request, config: UsersPatchRequest) -> StatusResponse:
|
50
|
+
for username in config.usernames:
|
51
|
+
if await User.get_or_none(username=username) is None:
|
52
|
+
raise HTTPException(404, f"User with username {username} not found")
|
53
|
+
|
54
|
+
user = await UserManager.get_user(username)
|
55
|
+
await UserManager.update_enabled(user, config.enabled)
|
56
|
+
return StatusResponse(success=True)
|
@@ -9,9 +9,11 @@ from fastapi.requests import Request
|
|
9
9
|
|
10
10
|
from goosebit.api.responses import StatusResponse
|
11
11
|
from goosebit.auth import validate_user_permissions
|
12
|
+
from goosebit.auth.permissions import GOOSEBIT_PERMISSIONS
|
12
13
|
from goosebit.db.models import Rollout, Software
|
13
|
-
from goosebit.
|
14
|
+
from goosebit.storage import storage
|
14
15
|
from goosebit.updates import create_software_update
|
16
|
+
from goosebit.util.path import validate_filename
|
15
17
|
|
16
18
|
from .requests import SoftwareDeleteRequest
|
17
19
|
from .responses import SoftwareResponse
|
@@ -21,7 +23,7 @@ router = APIRouter(prefix="/software", tags=["software"])
|
|
21
23
|
|
22
24
|
@router.get(
|
23
25
|
"",
|
24
|
-
dependencies=[Security(validate_user_permissions, scopes=["software
|
26
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["software"]["read"]()])],
|
25
27
|
)
|
26
28
|
async def software_get(_: Request) -> SoftwareResponse:
|
27
29
|
software = await Software.all().prefetch_related("compatibility")
|
@@ -30,7 +32,7 @@ async def software_get(_: Request) -> SoftwareResponse:
|
|
30
32
|
|
31
33
|
@router.delete(
|
32
34
|
"",
|
33
|
-
dependencies=[Security(validate_user_permissions, scopes=["software
|
35
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["software"]["delete"]()])],
|
34
36
|
)
|
35
37
|
async def software_delete(_: Request, delete_req: SoftwareDeleteRequest) -> StatusResponse:
|
36
38
|
success = False
|
@@ -44,10 +46,11 @@ async def software_delete(_: Request, delete_req: SoftwareDeleteRequest) -> Stat
|
|
44
46
|
if rollout_count > 0:
|
45
47
|
raise HTTPException(409, "Software is referenced by rollout")
|
46
48
|
|
47
|
-
if software.
|
48
|
-
|
49
|
-
|
50
|
-
|
49
|
+
if software.uri:
|
50
|
+
try:
|
51
|
+
await storage.delete_file(software.uri)
|
52
|
+
except ValueError:
|
53
|
+
pass
|
51
54
|
|
52
55
|
await software.delete()
|
53
56
|
success = True
|
@@ -56,7 +59,7 @@ async def software_delete(_: Request, delete_req: SoftwareDeleteRequest) -> Stat
|
|
56
59
|
|
57
60
|
@router.post(
|
58
61
|
"",
|
59
|
-
dependencies=[Security(validate_user_permissions, scopes=["software
|
62
|
+
dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["software"]["write"]()])],
|
60
63
|
)
|
61
64
|
async def post_update(_: Request, file: UploadFile | None = File(None), url: str | None = Form(None)):
|
62
65
|
if url is not None:
|
@@ -72,16 +75,17 @@ async def post_update(_: Request, file: UploadFile | None = File(None), url: str
|
|
72
75
|
software = await create_software_update(url, None)
|
73
76
|
elif file is not None:
|
74
77
|
# local file
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
78
|
+
temp_dir = Path(storage.get_temp_dir())
|
79
|
+
try:
|
80
|
+
file_path = await validate_filename(file.filename, temp_dir)
|
81
|
+
except ValueError as e:
|
82
|
+
raise HTTPException(400, f"Invalid filename: {e}")
|
83
|
+
tmp_file_path = temp_dir.joinpath("".join(random.choices(string.ascii_lowercase, k=12)) + ".tmp")
|
79
84
|
file_absolute_path = await file_path.absolute()
|
80
|
-
tmp_file_absolute_path = await tmp_file_path.absolute()
|
81
85
|
try:
|
82
86
|
async with await open_file(tmp_file_path, "w+b") as f:
|
83
87
|
await f.write(await file.read())
|
84
|
-
software = await create_software_update(file_absolute_path.as_uri(),
|
88
|
+
software = await create_software_update(file_absolute_path.as_uri(), tmp_file_path)
|
85
89
|
finally:
|
86
90
|
await tmp_file_path.unlink(missing_ok=True)
|
87
91
|
else:
|