Skip to content

Commit

Permalink
feat(api): restrict page streaming and file download with roles
Browse files Browse the repository at this point in the history
also add the ability to edit user roles

related to gotson#146
  • Loading branch information
gotson committed Jun 10, 2020
1 parent 327ed00 commit 6291dab
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
alter table user
add column role_file_download boolean default true;
alter table user
add column role_page_streaming boolean default true;
17 changes: 15 additions & 2 deletions komga/src/main/kotlin/org/gotson/komga/domain/model/KomgaUser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import java.time.LocalDateTime
import javax.validation.constraints.Email
import javax.validation.constraints.NotBlank

const val ROLE_USER = "USER"
const val ROLE_ADMIN = "ADMIN"
const val ROLE_FILE_DOWNLOAD = "FILE_DOWNLOAD"
const val ROLE_PAGE_STREAMING = "PAGE_STREAMING"

data class KomgaUser(
@Email
@NotBlank
val email: String,
@NotBlank
val password: String,
val roleAdmin: Boolean,
val roleFileDownload: Boolean = true,
val rolePageStreaming: Boolean = true,
val sharedLibrariesIds: Set<Long> = emptySet(),
val sharedAllLibraries: Boolean = true,
val id: Long = 0,
Expand All @@ -19,8 +26,10 @@ data class KomgaUser(
) : Auditable() {

fun roles(): Set<String> {
val roles = mutableSetOf("USER")
if (roleAdmin) roles.add("ADMIN")
val roles = mutableSetOf(ROLE_USER)
if (roleAdmin) roles.add(ROLE_ADMIN)
if (roleFileDownload) roles.add(ROLE_FILE_DOWNLOAD)
if (rolePageStreaming) roles.add(ROLE_PAGE_STREAMING)
return roles
}

Expand Down Expand Up @@ -52,4 +61,8 @@ data class KomgaUser(
fun canAccessLibrary(library: Library): Boolean {
return sharedAllLibraries || sharedLibrariesIds.any { it == library.id }
}

override fun toString(): String {
return "KomgaUser(email='$email', roleAdmin=$roleAdmin, roleFileDownload=$roleFileDownload, rolePageStreaming=$rolePageStreaming, sharedLibrariesIds=$sharedLibrariesIds, sharedAllLibraries=$sharedAllLibraries, id=$id, createdDate=$createdDate, lastModifiedDate=$lastModifiedDate)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class KomgaUserDao(
email = ur.email,
password = ur.password,
roleAdmin = ur.roleAdmin,
roleFileDownload = ur.roleFileDownload,
rolePageStreaming = ur.rolePageStreaming,
sharedLibrariesIds = ulr.mapNotNull { it.libraryId }.toSet(),
sharedAllLibraries = ur.sharedAllLibraries,
id = ur.id,
Expand All @@ -65,10 +67,12 @@ class KomgaUserDao(
.set(u.EMAIL, user.email)
.set(u.PASSWORD, user.password)
.set(u.ROLE_ADMIN, user.roleAdmin)
.set(u.ROLE_FILE_DOWNLOAD, user.roleFileDownload)
.set(u.ROLE_PAGE_STREAMING, user.rolePageStreaming)
.set(u.SHARED_ALL_LIBRARIES, user.sharedAllLibraries)
.set(u.LAST_MODIFIED_DATE, LocalDateTime.now())
.whenNotMatchedThenInsert(u.ID, u.EMAIL, u.PASSWORD, u.ROLE_ADMIN, u.SHARED_ALL_LIBRARIES)
.values(id, user.email, user.password, user.roleAdmin, user.sharedAllLibraries)
.whenNotMatchedThenInsert(u.ID, u.EMAIL, u.PASSWORD, u.ROLE_ADMIN, u.ROLE_FILE_DOWNLOAD, u.ROLE_PAGE_STREAMING, u.SHARED_ALL_LIBRARIES)
.values(id, user.email, user.password, user.roleAdmin, user.roleFileDownload, user.rolePageStreaming, user.sharedAllLibraries)
.execute()

deleteFrom(ul)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.gotson.komga.infrastructure.security

import mu.KotlinLogging
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.domain.model.ROLE_USER
import org.gotson.komga.infrastructure.configuration.KomgaProperties
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
Expand Down Expand Up @@ -37,16 +39,16 @@ class SecurityConfiguration(

.authorizeRequests()
// restrict all actuator endpoints to ADMIN only
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ADMIN")
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole(ROLE_ADMIN)

// restrict H2 console to ADMIN only
.requestMatchers(PathRequest.toH2Console()).hasRole("ADMIN")
.requestMatchers(PathRequest.toH2Console()).hasRole(ROLE_ADMIN)

// all other endpoints are restricted to authenticated users
.antMatchers(
"/api/**",
"/opds/**"
).hasRole("USER")
).hasRole(ROLE_USER)

// authorize frames for H2 console
.and()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import org.gotson.komga.domain.model.BookSearchWithReadProgress
import org.gotson.komga.domain.model.ImageConversionException
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.MediaNotReadyException
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
import org.gotson.komga.domain.model.ReadStatus
import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.BookRepository
Expand Down Expand Up @@ -206,6 +209,7 @@ class BookController(
"api/v1/books/{bookId}/file/*",
"opds/v1.2/books/{bookId}/file/*"
], produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE])
@PreAuthorize("hasRole('$ROLE_FILE_DOWNLOAD')")
fun getBookFile(
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable bookId: Long
Expand Down Expand Up @@ -258,6 +262,7 @@ class BookController(
"api/v1/books/{bookId}/pages/{pageNumber}",
"opds/v1.2/books/{bookId}/pages/{pageNumber}"
])
@PreAuthorize("hasRole('$ROLE_PAGE_STREAMING')")
fun getBookPage(
@AuthenticationPrincipal principal: KomgaPrincipal,
request: WebRequest,
Expand Down Expand Up @@ -345,7 +350,7 @@ class BookController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)

@PostMapping("api/v1/books/{bookId}/analyze")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
fun analyze(@PathVariable bookId: Long) {
bookRepository.findByIdOrNull(bookId)?.let { book ->
Expand All @@ -354,7 +359,7 @@ class BookController(
}

@PostMapping("api/v1/books/{bookId}/metadata/refresh")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
fun refreshMetadata(@PathVariable bookId: Long) {
bookRepository.findByIdOrNull(bookId)?.let { book ->
Expand All @@ -363,7 +368,7 @@ class BookController(
}

@PatchMapping("api/v1/books/{bookId}/metadata")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
fun updateMetadata(
@PathVariable bookId: Long,
@Parameter(description = "Metadata fields to update. Set a field to null to unset the metadata. You can omit fields you don't want to update.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.gotson.komga.domain.model.DirectoryNotFoundException
import org.gotson.komga.domain.model.DuplicateNameException
import org.gotson.komga.domain.model.Library
import org.gotson.komga.domain.model.PathContainedInPath
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.service.LibraryLifecycle
Expand Down Expand Up @@ -59,7 +60,7 @@ class LibraryController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)

@PostMapping
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
fun addOne(
@AuthenticationPrincipal principal: KomgaPrincipal,
@Valid @RequestBody library: LibraryCreationDto
Expand All @@ -78,7 +79,7 @@ class LibraryController(
}

@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteOne(@PathVariable id: Long) {
libraryRepository.findByIdOrNull(id)?.let {
Expand All @@ -87,7 +88,7 @@ class LibraryController(
}

@PostMapping("{libraryId}/scan")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
fun scan(@PathVariable libraryId: Long) {
libraryRepository.findByIdOrNull(libraryId)?.let { library ->
Expand All @@ -96,7 +97,7 @@ class LibraryController(
}

@PostMapping("{libraryId}/analyze")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
fun analyze(@PathVariable libraryId: Long) {
bookRepository.findAllIdByLibraryId(libraryId).forEach {
Expand All @@ -105,7 +106,7 @@ class LibraryController(
}

@PostMapping("{libraryId}/metadata/refresh")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
fun refreshMetadata(@PathVariable libraryId: Long) {
bookRepository.findAllIdByLibraryId(libraryId).forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import mu.KotlinLogging
import org.gotson.komga.application.tasks.TaskReceiver
import org.gotson.komga.domain.model.BookSearchWithReadProgress
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.domain.model.ReadStatus
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
Expand Down Expand Up @@ -213,7 +214,7 @@ class SeriesController(
}

@PostMapping("{seriesId}/analyze")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
fun analyze(@PathVariable seriesId: Long) {
bookRepository.findAllIdBySeriesId(seriesId).forEach {
Expand All @@ -222,7 +223,7 @@ class SeriesController(
}

@PostMapping("{seriesId}/metadata/refresh")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
fun refreshMetadata(@PathVariable seriesId: Long) {
bookRepository.findAllIdBySeriesId(seriesId).forEach {
Expand All @@ -231,7 +232,7 @@ class SeriesController(
}

@PatchMapping("{seriesId}/metadata")
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
fun updateMetadata(
@PathVariable seriesId: Long,
@Parameter(description = "Metadata fields to update. Set a field to null to unset the metadata. You can omit fields you don't want to update.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package org.gotson.komga.interfaces.rest

import mu.KotlinLogging
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
import org.gotson.komga.domain.model.UserEmailAlreadyExistsException
import org.gotson.komga.domain.persistence.KomgaUserRepository
import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.service.KomgaUserLifecycle
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.interfaces.rest.dto.PasswordUpdateDto
import org.gotson.komga.interfaces.rest.dto.RolesUpdateDto
import org.gotson.komga.interfaces.rest.dto.SharedLibrariesUpdateDto
import org.gotson.komga.interfaces.rest.dto.UserCreationDto
import org.gotson.komga.interfaces.rest.dto.UserDto
Expand Down Expand Up @@ -58,13 +62,13 @@ class UserController(
}

@GetMapping
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
fun getAll(): List<UserWithSharedLibrariesDto> =
userRepository.findAll().map { it.toWithSharedLibrariesDto() }

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
fun addOne(@Valid @RequestBody newUser: UserCreationDto): UserDto =
try {
userLifecycle.createUser(newUser.toDomain()).toDto()
Expand All @@ -74,7 +78,7 @@ class UserController(

@DeleteMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ADMIN') and #principal.user.id != #id")
@PreAuthorize("hasRole('$ROLE_ADMIN') and #principal.user.id != #id")
fun delete(
@PathVariable id: Long,
@AuthenticationPrincipal principal: KomgaPrincipal
Expand All @@ -84,9 +88,29 @@ class UserController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}

@PatchMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('$ROLE_ADMIN') and #principal.user.id != #id")
fun updateUserRoles(
@PathVariable id: Long,
@Valid @RequestBody patch: RolesUpdateDto,
@AuthenticationPrincipal principal: KomgaPrincipal
) {
userRepository.findByIdOrNull(id)?.let { user ->
val updatedUser = user.copy(
roleAdmin = patch.roles.contains(ROLE_ADMIN),
roleFileDownload = patch.roles.contains(ROLE_FILE_DOWNLOAD),
rolePageStreaming = patch.roles.contains(ROLE_PAGE_STREAMING)
)
userRepository.save(updatedUser).also {
logger.info { "Updated user roles: $it" }
}
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}

@PatchMapping("{id}/shared-libraries")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
fun updateSharesLibraries(
@PathVariable id: Long,
@Valid @RequestBody sharedLibrariesUpdateDto: SharedLibrariesUpdateDto
Expand All @@ -99,7 +123,9 @@ class UserController(
.map { it.id }
.toSet()
)
userRepository.save(updatedUser)
userRepository.save(updatedUser).also {
logger.info { "Updated user shared libraries: $it" }
}
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.gotson.komga.interfaces.rest.dto

import org.gotson.komga.domain.model.KomgaUser
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import javax.validation.constraints.Email
import javax.validation.constraints.NotBlank
Expand Down Expand Up @@ -47,7 +50,13 @@ data class UserCreationDto(
val roles: List<String> = emptyList()
) {
fun toDomain(): KomgaUser =
KomgaUser(email, password, roleAdmin = roles.contains("ADMIN"))
KomgaUser(
email,
password,
roleAdmin = roles.contains(ROLE_ADMIN),
roleFileDownload = roles.contains(ROLE_FILE_DOWNLOAD),
rolePageStreaming = roles.contains(ROLE_PAGE_STREAMING)
)
}

data class PasswordUpdateDto(
Expand All @@ -58,3 +67,7 @@ data class SharedLibrariesUpdateDto(
val all: Boolean,
val libraryIds: Set<Long>
)

data class RolesUpdateDto(
val roles: List<String>
)

0 comments on commit 6291dab

Please sign in to comment.