Skip to content

Commit

Permalink
Add auth to CLI commands
Browse files Browse the repository at this point in the history
  • Loading branch information
Krisjanis Veinbahs committed Mar 14, 2022
1 parent 2d0d9e3 commit 6f34235
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 71 deletions.
4 changes: 2 additions & 2 deletions backend/domain/src/main/scala/diptestbed/domain/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ case class User(
isLabOwner: Boolean, // Lab owners can create and manage hardware
isDeveloper: Boolean // Developers can upload their own software
) {
def canAccessHardware: Boolean = isLabOwner || isDeveloper
def canAccessSoftware: Boolean = isLabOwner || isDeveloper
def canInteractHardware: Boolean = isManager || isLabOwner || isDeveloper
def canCreateSoftware: Boolean = isManager || isLabOwner || isDeveloper
}
24 changes: 11 additions & 13 deletions backend/web/app/diptestbed/web/control/ApiHardwareController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ class ApiHardwareController(
def getHardwares: Action[AnyContent] =
IOActionAny(withRequestAuthnOrFail(_)((_, user) =>
for {
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessHardware, (), permissionErrorResult("Hardware access")))
hardwares <- EitherT(hardwareService.getHardwares(Some(user), write = false)).leftMap(databaseErrorResult)
result = Success(hardwares).withHttpStatus(OK)
} yield result
Expand All @@ -75,8 +73,6 @@ class ApiHardwareController(
def getHardware(hardwareId: HardwareId): Action[AnyContent] =
IOActionAny(withRequestAuthnOrFail(_)((_, user) =>
for {
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessHardware, (), permissionErrorResult("Hardware access")))
hardware <- EitherT(hardwareService.getHardware(Some(user), hardwareId, write = false)).leftMap(databaseErrorResult)
_ <- EitherT.fromEither[IO](hardware.toRight(unknownIdErrorResult))
result = Success(hardware).withHttpStatus(OK)
Expand All @@ -101,7 +97,7 @@ class ApiHardwareController(
hardware <- EitherT(hardwareService.getHardware(Some(user), hardwareId, write = false)).leftMap(databaseErrorResult)
_ <- EitherT.fromEither[IO](hardware.toRight(unknownIdErrorResult))
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessHardware, (), permissionErrorResult("Hardware access")))
user.canInteractHardware, (), permissionErrorResult("Hardware access")))
uploadResult <- HardwareControlActor.requestSoftwareUpload(hardwareId, softwareId).bimap(
errorMessage => Failure(errorMessage).withHttpStatus(BAD_REQUEST),
result => Success(result.toString).withHttpStatus(OK),
Expand Down Expand Up @@ -146,14 +142,16 @@ class ApiHardwareController(
hardware <- EitherT(hardwareService.getHardware(Some(user), hardwareId, write = false)).leftMap(databaseErrorResult)
_ <- EitherT.fromEither[IO](hardware.toRight(unknownIdErrorResult))
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessHardware, (), permissionErrorResult("Hardware access")))
source <- HardwareCameraListenerActor.spawnCameraSource(pubSubMediator, hardwareId)
.leftMap(Failure(_).withHttpStatus(BAD_REQUEST))
content = request.headers.get("Range") match {
case None => Ok("")
case Some(_) => Ok.chunked(source)
}
result <- EitherT.fromEither[IO](Right(withStreamHeaders(content)))
user.canInteractHardware, (), permissionErrorResult("Hardware access")))
result <-
request.headers.get("Range") match {
case None => EitherT.fromEither[IO](Right[Result, Result](withStreamHeaders(Ok(""))))
case Some(_) => HardwareCameraListenerActor.spawnCameraSource(pubSubMediator, hardwareId).bimap[Result, Result](
errorMessage => Failure(errorMessage).withHttpStatus(BAD_REQUEST),
source =>
withStreamHeaders(Ok.chunked(source)),
)
}
} yield result
}))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class ApiSoftwareController(
maybeUser
.leftMap(databaseErrorResult)
.flatMap(_.toRight(authorizationErrorResult)))
_ <- EitherT.fromEither[IO](Either.cond(
user.canCreateSoftware, (), permissionErrorResult("Software access")))
software <- EitherT.fromEither[IO](
(request.body.file("software"), request.body.dataParts.get("name").flatMap(_.headOption))
.tupled
Expand All @@ -66,8 +68,6 @@ class ApiSoftwareController(
def getSoftwareMetas: Action[AnyContent] =
IOActionAny(withRequestAuthnOrFail(_)((_, user) =>
for {
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessSoftware, (), permissionErrorResult("Software access")))
result <- EitherT(softwareService.getSoftwareMetas(Some(user), write = false))
.leftMap(databaseErrorResult)
.map(softwareMetas => Success(softwareMetas).withHttpStatus(OK))
Expand All @@ -77,8 +77,6 @@ class ApiSoftwareController(
def getSoftware(softwareId: SoftwareId): Action[AnyContent] =
IOActionAny(withRequestAuthnOrFail(_)((_, user) =>
for {
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessSoftware, (), permissionErrorResult("Software access")))
software <- EitherT(softwareService.getSoftware(Some(user), softwareId, write = false)).leftMap(databaseErrorResult)
existingSoftware <- EitherT.fromEither[IO](software.toRight(unknownIdErrorResult))
result = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class AppHardwareController(

def dbHardware(sessionUser: User): IO[List[Hardware]] =
hardwareService.getHardwares(Some(sessionUser), write = false)
.map(_.toOption.sequence.flatten.toList.filter(_ => sessionUser.canAccessHardware))
.map(_.toOption.sequence.flatten.toList)

def list: Action[AnyContent] =
Action { implicit request =>
Expand All @@ -58,7 +58,6 @@ class AppHardwareController(
}.unsafeRunSync()
}


def publicRequest: Action[AnyContent] =
Action { implicit request =>
withRequestAuthnOrLoginRedirect[AnyContent] { case (_, user) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class AppSoftwareController(

def dbSoftware(sessionUser: User): IO[List[SoftwareMeta]] =
softwareService.getSoftwareMetas(Some(sessionUser), write = false)
.map(_.toOption.sequence.flatten.toList.filter(_ => sessionUser.canAccessSoftware))
.map(_.toOption.sequence.flatten.toList)

def list: Action[AnyContent] =
Action { implicit r =>
Expand Down
6 changes: 6 additions & 0 deletions backend/web/app/diptestbed/web/views/hardwareList.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
@if(user.map(_.id).contains(hardware.ownerId) || user.exists(_.isManager)) {
<button type="submit" class="btn btn-sm btn-primary mx-2">Save publicity</button>
}
<a target="_blank" href="@(appConfig.withApiPath(s"/hardware/video/sink/${hardware.id.value}.ogg"))">
<button type="button" class="btn btn-sm btn-primary">
<span>Video stream</span>
<i class="mx-2 fa-solid fa-arrow-up-right-from-square"></i>
</button>
</a>
</div>
</td>
}
Expand Down
21 changes: 13 additions & 8 deletions client/src/service/backend.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Module for backend management service definitions"""
import os
import urllib.request
from typing import List, TypeVar, Dict, Optional
from dataclasses import dataclass
import base64
Expand Down Expand Up @@ -134,13 +135,15 @@ def hardware_video_source_url(self, hardware_id: ManagedUUID) -> Result[ManagedU
return self.control_url(f"{self.config.api_prefix}/hardware/video/source?hardware={hardware_id.value}")

def hardware_video_sink_url(self, hardware_id: ManagedUUID) -> Result[ManagedURL, BackendManagementError]:
return self.static_url(f"{self.config.api_prefix}/hardware/video/sink/{hardware_id.value}.ogg")
url_result = self.static_url(f"{self.config.api_prefix}/hardware/video/sink/{hardware_id.value}.ogg")
if isinstance(url_result, Err):
return Err(BackendManagementError("Control server URL build failed", exception=url_result.value))
return url_result.value.with_basic_auth(self.config.auth.username, self.config.auth.password) # Bad hack

def hardware_serial_monitor_url(self, hardware_id: ManagedUUID) -> Result[ManagedURL, BackendManagementError]:
"""Build hardware serial monitor URL"""
return self.control_url(f"{self.config.api_prefix}/hardware/{hardware_id.value}/monitor/serial")


@staticmethod
def response_to_result(
response: Response,
Expand Down Expand Up @@ -236,7 +239,7 @@ def user_list(self) -> Result[List[User], BackendManagementError]:
"""Fetch a list of users"""
path = f"{self.config.api_prefix}/user"
decoder = s11n_json.list_decoder_json(s11n_json.USER_DECODER_JSON)
return self.static_get_json_result(path, decoder)
return self.static_get_json_result(path, decoder, headers=self.config.auth.auth_headers())

def user_create(self, username: str, password: str) -> Result[User, str]:
"""Create a new user"""
Expand All @@ -251,7 +254,7 @@ def hardware_list(self) -> Result[List[Hardware], BackendManagementError]:
"""Fetch a list of hardware"""
path = f"{self.config.api_prefix}/hardware"
decoder = s11n_json.list_decoder_json(s11n_json.HARDWARE_DECODER_JSON)
return self.static_get_json_result(path, decoder)
return self.static_get_json_result(path, decoder, headers=self.config.auth.auth_headers())

def hardware_create(
self,
Expand All @@ -273,7 +276,7 @@ def hardware_software_upload(
) -> Optional[BackendManagementError]:
"""Upload a given software to a given hardware"""
path = f"{self.config.api_prefix}/hardware/{hardware_id.value}/upload/software/{software_id.value}"
result = self.static_post_json_result(path)
result = self.static_post_json_result(path, headers=self.config.auth.auth_headers())
if isinstance(result, Err):
return result.value
return None
Expand All @@ -283,7 +286,7 @@ def software_list(self) -> Result[List[Software], str]:
"""Fetch a list of software"""
path = f"{self.config.api_prefix}/software"
decoder = s11n_json.list_decoder_json(s11n_json.SOFTWARE_DECODER_JSON)
return self.static_get_json_result(path, decoder)
return self.static_get_json_result(path, decoder, headers=self.config.auth.auth_headers())

def software_upload(
self,
Expand All @@ -309,12 +312,14 @@ def software_download(
# Build URL
url_result = self.static_url(f"{self.config.api_prefix}/software/{software_id.value}/download")
if isinstance(url_result, Err): return Err(url_result.value)
url_with_auth_result = url_result.value.with_basic_auth(self.config.auth.username, self.config.auth.password) # Bad hack
if isinstance(url_with_auth_result, Err): return Err(url_with_auth_result.value)

# Download file
if file_path is None:
file_result = url_result.value.downloaded_file_in_temp()
file_result = url_with_auth_result.value.downloaded_file_in_temp()
else:
file_result = url_result.value.downloaded_file_in_path(file_path)
file_result = url_with_auth_result.value.downloaded_file_in_path(file_path)

# Handle file download
if isinstance(file_result, Err):
Expand Down
78 changes: 56 additions & 22 deletions client/src/service/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ async def agent_fake(
@staticmethod
def user_list(
config_path_str: Optional[str],
static_server_str: Optional[str]
static_server_str: Optional[str],
username_str: Optional[str],
password_str: Optional[str]
) -> Result[List[User], DIPClientError]:
pass

Expand All @@ -166,7 +168,9 @@ def user_create(
@staticmethod
def hardware_list(
config_path_str: Optional[str],
static_server_str: Optional[str]
static_server_str: Optional[str],
username_str: Optional[str],
password_str: Optional[str]
) -> Result[List[Hardware], DIPClientError]:
pass

Expand All @@ -180,10 +184,22 @@ def hardware_create(
) -> Result[Hardware, DIPClientError]:
pass

@staticmethod
def hardware_stream_open(
config_path_str: Optional[str],
static_server_str: Optional[str],
hardware_id_str: str,
username_str: Optional[str],
password_str: Optional[str],
) -> Optional[DIPClientError]:
pass

@staticmethod
def software_list(
config_path_str: Optional[str],
static_server_str: str
static_server_str: str,
username_str: Optional[str],
password_str: Optional[str],
) -> Result[List[Software], DIPClientError]:
pass

Expand All @@ -203,7 +219,9 @@ def software_download(
config_path_str: Optional[str],
static_server_str: Optional[str],
software_id_str: str,
file_path: str
file_path: str,
username_str: Optional[str],
password_str: Optional[str],
) -> Result[ExistingFilePath, DIPClientError]:
pass

Expand All @@ -212,7 +230,9 @@ def hardware_software_upload(
config_path_str: Optional[str],
static_server_str: Optional[str],
hardware_id_str: str,
software_id_str: str
software_id_str: str,
username_str: Optional[str],
password_str: Optional[str],
) -> Optional[DIPClientError]:
pass

Expand Down Expand Up @@ -623,9 +643,11 @@ async def agent_fake(
@staticmethod
def user_list(
config_path_str: Optional[str],
static_server_str: Optional[str]
static_server_str: Optional[str],
username_str: Optional[str],
password_str: Optional[str]
) -> Result[List[User], DIPClientError]:
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, None, None)
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, username_str, password_str)
if isinstance(backend_result, Err): return Err(backend_result.value)
return backend_result.value.user_list()

Expand All @@ -643,9 +665,11 @@ def user_create(
@staticmethod
def hardware_list(
config_path_str: Optional[str],
static_server_str: Optional[str]
static_server_str: Optional[str],
username_str: Optional[str],
password_str: Optional[str]
) -> Result[List[Hardware], DIPClientError]:
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, None, None)
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, username_str, password_str)
if isinstance(backend_result, Err): return Err(backend_result.value)
return backend_result.value.hardware_list()

Expand All @@ -665,9 +689,11 @@ def hardware_create(
def hardware_stream_open(
config_path_str: Optional[str],
static_server_str: Optional[str],
hardware_id_str: str
hardware_id_str: str,
username_str: Optional[str],
password_str: Optional[str],
) -> Optional[DIPClientError]:
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, None, None)
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, username_str, password_str)
if isinstance(backend_result, Err): return backend_result.value
hardware_id_result = ManagedUUID.build(hardware_id_str)
if isinstance(hardware_id_result, Err): return hardware_id_result.value.of_type("hardware")
Expand All @@ -682,9 +708,11 @@ def hardware_stream_open(
@staticmethod
def software_list(
config_path_str: Optional[str],
static_server_str: Optional[str]
static_server_str: Optional[str],
username_str: Optional[str],
password_str: Optional[str],
) -> Result[List[Software], DIPClientError]:
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, None, None)
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, username_str, password_str)
if isinstance(backend_result, Err): return Err(backend_result.value)
return backend_result.value.software_list()

Expand All @@ -708,22 +736,28 @@ def software_download(
config_path_str: Optional[str],
static_server_str: Optional[str],
software_id_str: str,
file_path: str
) -> Result[ExistingFilePath, DIPClientError]:
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, None, None)
if isinstance(backend_result, Err): return Err(backend_result.value)
file_path: str,
username_str: Optional[str],
password_str: Optional[str],
) -> Optional[DIPClientError]:
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, username_str, password_str)
if isinstance(backend_result, Err): return backend_result.value
software_id_result = ManagedUUID.build(software_id_str)
if isinstance(software_id_result, Err): return Err(software_id_result.value.of_type("software"))
return backend_result.value.software_download(software_id_result.value, file_path)
if isinstance(software_id_result, Err): return software_id_result.value.of_type("software")
download_result = backend_result.value.software_download(software_id_result.value, file_path)
if isinstance(download_result, Err): return download_result.value
return None

@staticmethod
def hardware_software_upload(
config_path_str: Optional[str],
static_server_str: Optional[str],
hardware_id_str: str,
software_id_str: str
software_id_str: str,
username_str: Optional[str],
password_str: Optional[str],
) -> Optional[DIPClientError]:
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, None, None)
backend_result = CLI.parsed_backend(config_path_str, None, static_server_str, username_str, password_str)
if isinstance(backend_result, Err): return backend_result.value
software_id_result = ManagedUUID.build(software_id_str)
if isinstance(software_id_result, Err): return software_id_result.value.of_type("software")
Expand Down Expand Up @@ -790,7 +824,7 @@ async def quick_run(
# Forward software to board
LOGGER.info("Forwarding software to board")
forward_error = CLI.hardware_software_upload(
config_path_str, static_server_str, hardware_id_str, str(software.id.value))
config_path_str, static_server_str, hardware_id_str, str(software.id.value), username_str, password_str)
if forward_error is not None: return Err(forward_error)
# Create serial monitor connection to board
LOGGER.info("Configuring serial connection monitor with board")
Expand Down
Loading

0 comments on commit 6f34235

Please sign in to comment.