diff --git a/backend/api/auth.py b/backend/api/auth.py index 0e166939..0c136235 100644 --- a/backend/api/auth.py +++ b/backend/api/auth.py @@ -66,16 +66,20 @@ async def check_failed_login(ip_address: str) -> None: async def set_failed_login(ip_address: str): ns = "login-failed-ip" - failed_attempts = await nebula.redis.incr(ns, ip_address) + failed_attempts_str = await nebula.redis.incr(ns, ip_address) + failed_attempts = int(failed_attempts_str) if failed_attempts_str else 0 + await nebula.redis.expire( ns, ip_address, 600 ) # this is just for the clean-up, it cannot be used to reset the counter if failed_attempts > nebula.config.max_failed_login_attempts: + ban_time = nebula.config.failed_login_ban_time or 0 await nebula.redis.set( "banned-ip-until", ip_address, - str(int(time.time() + nebula.config.failed_login_ban_time)), + + str(time.time() + ban_time), ) @@ -84,7 +88,15 @@ async def clear_failed_login(ip_address: str): class LoginRequest(APIRequest): - """Login using a username and password""" + """Login using a username and password + + This request will return an access token that can be used in the + Authorization header for the subsequent requests. + If the login fails, request will return 401 Unauthorized. + + If the login fails too many (configurable) times, + the IP address will be banned for a certain amount of time (configurable). + """ name: str = "login" response_model = LoginResponseModel @@ -94,6 +106,7 @@ async def handle( request: Request, payload: LoginRequestModel, ) -> LoginResponseModel: + if request is not None: await check_failed_login(get_real_ip(request)) @@ -113,7 +126,10 @@ async def handle( class LogoutRequest(APIRequest): - """Log out the current user""" + """Log out the current user. + + This request will invalidate the access token used in the Authorization header. + """ name: str = "logout" title: str = "Logout" @@ -144,7 +160,7 @@ async def handle( self, request: PasswordRequestModel, user: CurrentUser, - ): + ) -> Response: if request.login: if not user.is_admin: raise nebula.UnauthorizedException( diff --git a/backend/api/delete.py b/backend/api/delete.py index 18f14436..ef811efc 100644 --- a/backend/api/delete.py +++ b/backend/api/delete.py @@ -69,7 +69,7 @@ async def handle( case _: # do not delete bins directly raise nebula.NotImplementedException( - f"Deleting {request.obejct_type} is not implemented" + f"Deleting {request.object_type} is not implemented" ) # Delete simple objects diff --git a/backend/api/jobs/jobs.py b/backend/api/jobs/jobs.py index 24293304..27cb7d75 100644 --- a/backend/api/jobs/jobs.py +++ b/backend/api/jobs/jobs.py @@ -182,10 +182,10 @@ async def set_priority(id_job: int, priority: int, user: nebula.User) -> None: class JobsRequest(APIRequest): - """List and control jobs""" + """Get list of jobs, abort or restart them""" name: str = "jobs" - title: str = "Get list of jobs, abort or restart them" + title: str = "List and control jobs" response_model = JobsResponseModel async def handle( diff --git a/backend/api/services.py b/backend/api/services.py index 4ea905e1..b93f7b51 100644 --- a/backend/api/services.py +++ b/backend/api/services.py @@ -46,7 +46,7 @@ class ServicesResponseModel(ResponseModel): class Request(APIRequest): - """Get a list of objects""" + """List and control installed services.""" name: str = "services" title: str = "Service control" @@ -57,8 +57,6 @@ async def handle( request: ServiceRequestModel, user: CurrentUser, ) -> ServicesResponseModel: - """List and control installed services.""" - if request.stop: nebula.log.info(f"Stopping service {request.stop}", user=user.name) await nebula.db.execute( diff --git a/backend/api/sessions.py b/backend/api/sessions.py index 42800d7f..c598e3ea 100644 --- a/backend/api/sessions.py +++ b/backend/api/sessions.py @@ -12,6 +12,8 @@ class SessionsRequest(RequestModel): class Sessions(APIRequest): + """List user sessions.""" + name = "sessions" title = "List sessions" response_model = list[SessionModel] @@ -21,7 +23,6 @@ async def handle( request: SessionsRequest, user: CurrentUser, ) -> list[SessionModel]: - """Create or update an object.""" id_user = request.id_user @@ -46,16 +47,22 @@ class InvalidateSessionRequest(RequestModel): class InvalidateSession(APIRequest): + """Invalidate a user session. + + This endpoint is used to invalidate an user session. It can be used + to remotely log out a user. If the user is an admin, it can also be + used to log out other users. + """ + name = "invalidate_session" - title = "Invalidate session" + title = "Invalidate a session" responses = [204, 201] async def handle( self, payload: InvalidateSessionRequest, user: CurrentUser, - ) -> None: - """Create or update an object.""" + ) -> Response: session = await Session.check(payload.token) if session is None: diff --git a/backend/api/set.py b/backend/api/set.py index 25934bd3..ac7bafc5 100644 --- a/backend/api/set.py +++ b/backend/api/set.py @@ -146,8 +146,10 @@ async def can_modify_object(obj, user: nebula.User): class OperationsRequest(APIRequest): + """Create or update multiple objects in one requests.""" + name: str = "ops" - title: str = "Create / update multiple objects at once" + title: str = "Save multiple objects" response_model = OperationsResponseModel async def handle( @@ -155,7 +157,6 @@ async def handle( request: OperationsRequestModel, user: CurrentUser, ) -> OperationsResponseModel: - """Create or update multiple objects in one requests.""" pool = await nebula.db.pool() result = [] @@ -264,8 +265,10 @@ async def handle( class SetRequest(APIRequest): + """Create or update an object.""" + name = "set" - title = "Create or update an object" + title = "Save an object" response_model = OperationResponseModel async def handle( @@ -273,7 +276,6 @@ async def handle( request: OperationModel, user: CurrentUser, ) -> OperationResponseModel: - """Create or update an object.""" operation = OperationsRequest() result = await operation.handle( diff --git a/backend/api/solve.py b/backend/api/solve.py index 074fb81c..aef79a1a 100644 --- a/backend/api/solve.py +++ b/backend/api/solve.py @@ -53,7 +53,7 @@ class SolveRequestModel(RequestModel): class Request(APIRequest): - """Browse the assets database.""" + """Solve a rundown placeholder""" name: str = "solve" responses: list[int] = [200] @@ -63,6 +63,8 @@ async def handle( request: SolveRequestModel, user: CurrentUser, ) -> Response: + # TODO: check permissions + assert user is not None solver = get_solver(request.solver) diff --git a/backend/nebula/version.py b/backend/nebula/version.py index bb788580..98665744 100644 --- a/backend/nebula/version.py +++ b/backend/nebula/version.py @@ -1 +1 @@ -__version__ = "6.0.4" +__version__ = "6.0.4" \ No newline at end of file diff --git a/backend/server/endpoints.py b/backend/server/endpoints.py index c51195e9..fee326cf 100644 --- a/backend/server/endpoints.py +++ b/backend/server/endpoints.py @@ -99,7 +99,7 @@ def install_endpoints(app: fastapi.FastAPI): app.router.add_api_route( route, endpoint.handle, # type: ignore - name=endpoint.name, + name=endpoint.title or endpoint.name, operation_id=slugify(endpoint.name, separator="_"), methods=endpoint.methods, description=docstring,