Skip to content

Commit 4ec3987

Browse files
authored
Merge pull request #40 from nebulabroadcast/develop
Nebula 6.0.3
2 parents fa70b0f + 1cf91bd commit 4ec3987

31 files changed

+276
-77
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ NEBULA
22
======
33

44
![GitHub release (latest by date)](https://img.shields.io/github/v/release/nebulabroadcast/nebula?style=for-the-badge)
5-
![Maintenance](https://img.shields.io/maintenance/yes/2023?style=for-the-badge)
5+
![Maintenance](https://img.shields.io/maintenance/yes/2024?style=for-the-badge)
66
![Last commit](https://img.shields.io/github/last-commit/nebulabroadcast/nebula?style=for-the-badge)
77
![Python version](https://img.shields.io/badge/python-3.10-blue?style=for-the-badge)
88

backend/api/auth.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import time
2+
13
from fastapi import Header, Request, Response
24
from pydantic import Field
35

46
import nebula
7+
from server.clientinfo import get_real_ip
58
from server.dependencies import CurrentUser
69
from server.models import RequestModel, ResponseModel
710
from server.request import APIRequest
@@ -47,6 +50,39 @@ class PasswordRequestModel(RequestModel):
4750
#
4851

4952

53+
async def check_failed_login(ip_address: str) -> None:
54+
banned_until = await nebula.redis.get("banned-ip-until", ip_address)
55+
if banned_until is None:
56+
return
57+
58+
if float(banned_until) > time.time():
59+
nebula.log.warn(
60+
f"Attempt to login from banned IP {ip_address}. "
61+
f"Retry in {float(banned_until) - time.time():.2f} seconds."
62+
)
63+
await nebula.redis.delete("login-failed-ip", ip_address)
64+
raise nebula.LoginFailedException("Too many failed login attempts")
65+
66+
67+
async def set_failed_login(ip_address: str):
68+
ns = "login-failed-ip"
69+
failed_attempts = await nebula.redis.incr(ns, ip_address)
70+
await nebula.redis.expire(
71+
ns, ip_address, 600
72+
) # this is just for the clean-up, it cannot be used to reset the counter
73+
74+
if failed_attempts > nebula.config.max_failed_login_attempts:
75+
await nebula.redis.set(
76+
"banned-ip-until",
77+
ip_address,
78+
time.time() + nebula.config.failed_login_ban_time,
79+
)
80+
81+
82+
async def clear_failed_login(ip_address: str):
83+
await nebula.redis.delete("login-failed-ip", ip_address)
84+
85+
5086
class LoginRequest(APIRequest):
5187
"""Login using a username and password"""
5288

@@ -58,7 +94,20 @@ async def handle(
5894
request: Request,
5995
payload: LoginRequestModel,
6096
) -> LoginResponseModel:
61-
user = await nebula.User.login(payload.username, payload.password)
97+
if request is not None:
98+
await check_failed_login(get_real_ip(request))
99+
100+
try:
101+
user = await nebula.User.login(payload.username, payload.password)
102+
except nebula.LoginFailedException as e:
103+
if request is not None:
104+
await set_failed_login(get_real_ip(request))
105+
# re-raise the exception
106+
raise e
107+
108+
if request is not None:
109+
await clear_failed_login(get_real_ip(request))
110+
62111
session = await Session.create(user, request)
63112
return LoginResponseModel(access_token=session.token)
64113

backend/api/browse.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class BrowseResponseModel(ResponseModel):
103103

104104

105105
def sanitize_value(value: Any) -> Any:
106-
if type(value) is str:
106+
if isinstance(value, str):
107107
value = value.replace("'", "''")
108108
return str(value)
109109

@@ -116,7 +116,7 @@ def build_conditions(conditions: list[ConditionModel]) -> list[str]:
116116
), f"Invalid meta key {condition.key}"
117117
condition.value = normalize_meta(condition.key, condition.value)
118118
if condition.operator in ["IN", "NOT IN"]:
119-
assert type(condition.value) is list, "Value must be a list"
119+
assert isinstance(condition.value, list), "Value must be a list"
120120
values = sql_list([sanitize_value(v) for v in condition.value], t="str")
121121
cond_list.append(f"meta->>'{condition.key}' {condition.operator} {values}")
122122
elif condition.operator in ["IS NULL", "IS NOT NULL"]:
@@ -185,14 +185,14 @@ def build_query(
185185

186186
if request.view is None:
187187
try:
188-
request.view = nebula.settings.views[0]
188+
request.view = nebula.settings.views[0].id
189189
except IndexError as e:
190190
raise NebulaException("No views defined") from e
191191

192192
# Process views
193193

194194
if request.view is not None and not request.ignore_view_conditions:
195-
assert type(request.view) is int, "View must be an integer"
195+
assert isinstance(request.view, int), "View must be an integer"
196196
if (view := nebula.settings.get_view(request.view)) is not None:
197197
if view.folders:
198198
cond_list.append(f"id_folder IN {sql_list(view.folders)}")
@@ -222,7 +222,7 @@ def build_query(
222222
c2 = f"meta->'assignees' @> '[{user.id}]'::JSONB"
223223
cond_list.append(f"({c1} OR {c2})")
224224

225-
if (can_view := user["can/asset_view"]) and type(can_view) is list:
225+
if (can_view := user["can/asset_view"]) and isinstance(can_view, list):
226226
cond_list.append(f"id_folder IN {sql_list(can_view)}")
227227

228228
# Build conditions
@@ -260,10 +260,9 @@ async def handle(
260260
request: BrowseRequestModel,
261261
user: CurrentUser,
262262
) -> BrowseResponseModel:
263-
264263
columns: list[str] = ["title", "duration"]
265264
if request.view is not None and not request.columns:
266-
assert type(request.view) is int, "View must be an integer"
265+
assert isinstance(request.view, int), "View must be an integer"
267266
if (view := nebula.settings.get_view(request.view)) is not None:
268267
if view.columns is not None:
269268
columns = view.columns

backend/api/jobs/actions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ async def handle(
5858

5959
if allow_if_elm := action_settings.findall("allow_if"):
6060
allow_if_cond = allow_if_elm[0].text
61+
if not allow_if_cond:
62+
continue
6163

6264
for id_asset in request.ids:
6365
asset = await nebula.Asset.load(id_asset)

backend/api/proxy.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,21 @@ async def send_bytes_range_requests(file_name: str, start: int, end: int):
1616
"""
1717
CHUNK_SIZE = 1024 * 8
1818

19-
async with aiofiles.open(file_name, mode="rb") as f:
20-
await f.seek(start)
21-
chs = await f.tell()
22-
while (pos := chs) <= end:
23-
read_size = min(CHUNK_SIZE, end + 1 - pos)
24-
data = await f.read(read_size)
25-
yield data
19+
sent_bytes = 0
20+
try:
21+
async with aiofiles.open(file_name, mode="rb") as f:
22+
await f.seek(start)
23+
pos = start
24+
while pos < end:
25+
read_size = min(CHUNK_SIZE, end - pos + 1)
26+
data = await f.read(read_size)
27+
yield data
28+
pos += len(data)
29+
sent_bytes += len(data)
30+
finally:
31+
nebula.log.trace(
32+
f"Finished sending file {start}-{end}. Sent {sent_bytes} bytes. Expected {end-start+1} bytes"
33+
)
2634

2735

2836
def _get_range_header(range_header: str, file_size: int) -> tuple[int, int]:

backend/api/scheduler/scheduler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async def create_new_event(
4848
if event_data.items:
4949
for item_data in event_data.items:
5050
if item_data.get("id"):
51-
assert type(item_data["id"]) == int, "Invalid item ID"
51+
assert isinstance(item_data["id"], int), "Invalid item ID"
5252
item = await nebula.Item.load(item_data["id"], connection=conn)
5353
else:
5454
item = nebula.Item(connection=conn)

backend/api/set.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async def can_modify_object(obj, user: nebula.User):
121121
acl = user.get("can/asset_edit", False)
122122
if not acl:
123123
raise nebula.ForbiddenException("You are not allowed to edit assets")
124-
elif type(acl) == list and obj["id_folder"] not in acl:
124+
elif isinstance(acl, list) and obj["id_folder"] not in acl:
125125
raise nebula.ForbiddenException(
126126
"You are not allowed to edit assets in this folder"
127127
)
@@ -130,7 +130,7 @@ async def can_modify_object(obj, user: nebula.User):
130130
acl = user.get("can/scheduler_edit", False)
131131
if not acl:
132132
raise nebula.ForbiddenException("You are not allowed to edit schedule")
133-
elif type(acl) == list and obj["id_channel"] not in acl:
133+
elif isinstance(acl, list) and obj["id_channel"] not in acl:
134134
raise nebula.ForbiddenException(
135135
"You are not allowed to edit schedule for this channel"
136136
)

backend/api/upload.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ async def handle(
5151
else:
5252
direct = True
5353
storage = nebula.storages[asset["id_storage"]]
54-
assert asset.local_path, f"{asset} does not have path set"
55-
bname = os.path.splitext(asset.local_path)[0]
54+
assert asset.path, f"{asset} does not have path set"
55+
bname = os.path.splitext(asset.path)[0]
5656
target_path = f"{bname}.{extension}"
5757

5858
nebula.log.debug(f"Uploading media file for {asset}", user=user.name)

backend/api/users.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ async def handle(self, user: CurrentUser) -> UserListResponseModel:
5757
async for row in nebula.db.iterate(query):
5858
meta = {}
5959
for key, value in row["meta"].items():
60-
if key == "password":
60+
if key == "api_key_preview":
61+
continue
62+
if key == "api_key":
63+
meta[key] = row["meta"].get("api_key_preview", "*****")
64+
elif key == "password":
6165
continue
6266
elif key.startswith("can/"):
6367
meta[key.replace("can/", "can_")] = value
@@ -76,7 +80,7 @@ class SaveUserRequest(APIRequest):
7680
title: str = "Save user data"
7781
responses = [204, 201]
7882

79-
async def handle(self, user: CurrentUser, payload: UserModel) -> None:
83+
async def handle(self, user: CurrentUser, payload: UserModel) -> Response:
8084
new_user = payload.id is None
8185

8286
if not user.is_admin:

backend/nebula/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
__version__ = "6.0.2"
2-
31
__all__ = [
42
"config",
53
"settings",
@@ -33,6 +31,8 @@
3331

3432
import sys
3533

34+
from nebula.version import __version__
35+
3636
if "--version" in sys.argv:
3737
print(__version__)
3838
sys.exit(0)
@@ -53,7 +53,7 @@
5353
UnauthorizedException,
5454
ValidationException,
5555
)
56-
from .log import log
56+
from .log import LogLevel, log
5757
from .messaging import msg
5858
from .objects.asset import Asset
5959
from .objects.bin import Bin
@@ -65,6 +65,9 @@
6565
from .settings import load_settings, settings
6666
from .storages import Storage, storages
6767

68+
log.user = "nebula"
69+
log.level = LogLevel[config.log_level.upper()]
70+
6871

6972
def run(entrypoint):
7073
"""Run a coroutine in the event loop.

0 commit comments

Comments
 (0)