Skip to content

Commit

Permalink
refactor: move user roles to separate database table
Browse files Browse the repository at this point in the history
  • Loading branch information
gotson committed Jan 8, 2025
1 parent 6dcebb4 commit cbb0d61
Show file tree
Hide file tree
Showing 52 changed files with 352 additions and 339 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
CREATE TABLE USER_ROLE
(
USER_ID varchar NOT NULL,
ROLE varchar NOT NULL,
PRIMARY KEY (USER_ID, ROLE),
FOREIGN KEY (USER_ID) REFERENCES USER (ID)
);

insert into USER_ROLE
select id, "ADMIN"
from user
where ROLE_ADMIN = 1;

insert into USER_ROLE
select id, "KOREADER_SYNC"
from user
where ROLE_ADMIN = 1;

insert into USER_ROLE
select id, "FILE_DOWNLOAD"
from user
where ROLE_FILE_DOWNLOAD = 1;

insert into USER_ROLE
select id, "PAGE_STREAMING"
from user
where ROLE_PAGE_STREAMING = 1;

insert into USER_ROLE
select id, "KOBO_SYNC"
from user
where ROLE_KOBO_SYNC = 1;

-- Remove columns ROLE_ADMIN, ROLE_FILE_DOWNLOAD, ROLE_PAGE_STREAMING, ROLE_KOBO_SYNC from USER
PRAGMA foreign_keys= OFF;

create table USER_dg_tmp
(
ID varchar not null
primary key,
CREATED_DATE datetime default CURRENT_TIMESTAMP not null,
LAST_MODIFIED_DATE datetime default CURRENT_TIMESTAMP not null,
EMAIL varchar not null
unique,
PASSWORD varchar not null,
SHARED_ALL_LIBRARIES boolean default 1 not null,
AGE_RESTRICTION integer,
AGE_RESTRICTION_ALLOW_ONLY boolean
);

insert into USER_dg_tmp(ID, CREATED_DATE, LAST_MODIFIED_DATE, EMAIL, PASSWORD, SHARED_ALL_LIBRARIES, AGE_RESTRICTION,
AGE_RESTRICTION_ALLOW_ONLY)
select ID,
CREATED_DATE,
LAST_MODIFIED_DATE,
EMAIL,
PASSWORD,
SHARED_ALL_LIBRARIES,
AGE_RESTRICTION,
AGE_RESTRICTION_ALLOW_ONLY
from USER;

drop table USER;

alter table USER_dg_tmp
rename to USER;

PRAGMA foreign_keys= ON;
25 changes: 5 additions & 20 deletions komga/src/main/kotlin/org/gotson/komga/domain/model/KomgaUser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,13 @@ import jakarta.validation.constraints.NotBlank
import org.gotson.komga.language.lowerNotBlank
import java.time.LocalDateTime

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

data class KomgaUser(
@Email(regexp = ".+@.+\\..+")
@NotBlank
val email: String,
@NotBlank
val password: String,
val roleAdmin: Boolean,
val roleFileDownload: Boolean = true,
val rolePageStreaming: Boolean = true,
val roleKoboSync: Boolean = false,
val roles: Set<UserRoles> = setOf(UserRoles.FILE_DOWNLOAD, UserRoles.PAGE_STREAMING),
val sharedLibrariesIds: Set<String> = emptySet(),
val sharedAllLibraries: Boolean = true,
val restrictions: ContentRestrictions = ContentRestrictions(),
Expand All @@ -30,14 +21,8 @@ data class KomgaUser(
override val lastModifiedDate: LocalDateTime = createdDate,
) : Auditable {
@delegate:Transient
val roles: Set<String> by lazy {
buildSet {
add(ROLE_USER)
if (roleAdmin) add(ROLE_ADMIN)
if (roleFileDownload) add(ROLE_FILE_DOWNLOAD)
if (rolePageStreaming) add(ROLE_PAGE_STREAMING)
if (roleKoboSync) add(ROLE_KOBO_SYNC)
}
val isAdmin: Boolean by lazy {
roles.contains(UserRoles.ADMIN)
}

/**
Expand All @@ -60,7 +45,7 @@ data class KomgaUser(
else -> null
}

fun canAccessAllLibraries(): Boolean = sharedAllLibraries || roleAdmin
fun canAccessAllLibraries(): Boolean = sharedAllLibraries || isAdmin

fun canAccessLibrary(libraryId: String): Boolean = canAccessAllLibraries() || sharedLibrariesIds.any { it == libraryId }

Expand Down Expand Up @@ -107,5 +92,5 @@ data class KomgaUser(
return !ageDenied && !labelDenied
}

override fun toString(): String = "KomgaUser(email='$email', roleAdmin=$roleAdmin, roleFileDownload=$roleFileDownload, rolePageStreaming=$rolePageStreaming, roleKoboSync=$roleKoboSync, sharedLibrariesIds=$sharedLibrariesIds, sharedAllLibraries=$sharedAllLibraries, restrictions=$restrictions, id='$id', createdDate=$createdDate, lastModifiedDate=$lastModifiedDate)"
override fun toString(): String = "KomgaUser(createdDate=$createdDate, email='$email', roles=$roles, sharedLibrariesIds=$sharedLibrariesIds, sharedAllLibraries=$sharedAllLibraries, restrictions=$restrictions, id='$id', lastModifiedDate=$lastModifiedDate)"
}
26 changes: 26 additions & 0 deletions komga/src/main/kotlin/org/gotson/komga/domain/model/UserRoles.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.gotson.komga.domain.model

enum class UserRoles {
ADMIN,
FILE_DOWNLOAD,
PAGE_STREAMING,
KOBO_SYNC,
;

companion object {
/**
* Returns a Set composed of the enum constant of this type with the specified name.
* The string must match exactly an identifier used to declare an enum constant in this type.
* (Extraneous whitespace characters are not permitted.)
*/
fun valuesOf(roles: Iterable<String>): Set<UserRoles> =
roles
.mapNotNull {
try {
valueOf(it)
} catch (_: Exception) {
null
}
}.toSet()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.gotson.komga.domain.model.AllowExclude
import org.gotson.komga.domain.model.ApiKey
import org.gotson.komga.domain.model.ContentRestrictions
import org.gotson.komga.domain.model.KomgaUser
import org.gotson.komga.domain.model.UserRoles
import org.gotson.komga.domain.persistence.KomgaUserRepository
import org.gotson.komga.jooq.main.Tables
import org.gotson.komga.jooq.main.tables.records.AnnouncementsReadRecord
Expand All @@ -23,6 +24,7 @@ class KomgaUserDao(
private val dsl: DSLContext,
) : KomgaUserRepository {
private val u = Tables.USER
private val ur = Tables.USER_ROLE
private val ul = Tables.USER_LIBRARY_SHARING
private val us = Tables.USER_SHARING
private val ar = Tables.ANNOUNCEMENTS_READ
Expand Down Expand Up @@ -60,34 +62,37 @@ class KomgaUserDao(
private fun ResultQuery<Record>.fetchAndMap() =
this
.fetchGroups({ it.into(u) }, { it.into(ul) })
.map { (ur, ulr) ->
.map { (userRecord, ulr) ->
val usr =
dsl
.selectFrom(us)
.where(us.USER_ID.eq(ur.id))
.where(us.USER_ID.eq(userRecord.id))
.toList()
val roles =
dsl
.select(ur.ROLE)
.from(ur)
.where(ur.USER_ID.eq(userRecord.id))
.fetch(ur.ROLE)
KomgaUser(
email = ur.email,
password = ur.password,
roleAdmin = ur.roleAdmin,
roleFileDownload = ur.roleFileDownload,
rolePageStreaming = ur.rolePageStreaming,
roleKoboSync = ur.roleKoboSync,
email = userRecord.email,
password = userRecord.password,
roles = UserRoles.valuesOf(roles),
sharedLibrariesIds = ulr.mapNotNull { it.libraryId }.toSet(),
sharedAllLibraries = ur.sharedAllLibraries,
sharedAllLibraries = userRecord.sharedAllLibraries,
restrictions =
ContentRestrictions(
ageRestriction =
if (ur.ageRestriction != null && ur.ageRestrictionAllowOnly != null)
AgeRestriction(ur.ageRestriction, if (ur.ageRestrictionAllowOnly) AllowExclude.ALLOW_ONLY else AllowExclude.EXCLUDE)
if (userRecord.ageRestriction != null && userRecord.ageRestrictionAllowOnly != null)
AgeRestriction(userRecord.ageRestriction, if (userRecord.ageRestrictionAllowOnly) AllowExclude.ALLOW_ONLY else AllowExclude.EXCLUDE)
else
null,
labelsAllow = usr.filter { it.allow }.map { it.label }.toSet(),
labelsExclude = usr.filterNot { it.allow }.map { it.label }.toSet(),
),
id = ur.id,
createdDate = ur.createdDate.toCurrentTimeZone(),
lastModifiedDate = ur.lastModifiedDate.toCurrentTimeZone(),
id = userRecord.id,
createdDate = userRecord.createdDate.toCurrentTimeZone(),
lastModifiedDate = userRecord.lastModifiedDate.toCurrentTimeZone(),
)
}

Expand All @@ -98,10 +103,6 @@ class KomgaUserDao(
.set(u.ID, user.id)
.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.ROLE_KOBO_SYNC, user.roleKoboSync)
.set(u.SHARED_ALL_LIBRARIES, user.sharedAllLibraries)
.set(u.AGE_RESTRICTION, user.restrictions.ageRestriction?.age)
.set(
Expand All @@ -113,6 +114,7 @@ class KomgaUserDao(
},
).execute()

insertRoles(user)
insertSharedLibraries(user)
insertSharingRestrictions(user)
}
Expand All @@ -133,10 +135,6 @@ class KomgaUserDao(
.update(u)
.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.ROLE_KOBO_SYNC, user.roleKoboSync)
.set(u.SHARED_ALL_LIBRARIES, user.sharedAllLibraries)
.set(u.AGE_RESTRICTION, user.restrictions.ageRestriction?.age)
.set(
Expand All @@ -150,6 +148,11 @@ class KomgaUserDao(
.where(u.ID.eq(user.id))
.execute()

dsl
.deleteFrom(ur)
.where(ur.USER_ID.eq(user.id))
.execute()

dsl
.deleteFrom(ul)
.where(ul.USER_ID.eq(user.id))
Expand All @@ -160,6 +163,7 @@ class KomgaUserDao(
.where(us.USER_ID.eq(user.id))
.execute()

insertRoles(user)
insertSharedLibraries(user)
insertSharingRestrictions(user)
}
Expand All @@ -171,6 +175,16 @@ class KomgaUserDao(
dsl.batchStore(announcementIds.map { AnnouncementsReadRecord(user.id, it) }).execute()
}

private fun insertRoles(user: KomgaUser) {
user.roles.forEach {
dsl
.insertInto(ur)
.columns(ur.USER_ID, ur.ROLE)
.values(user.id, it.name)
.execute()
}
}

private fun insertSharedLibraries(user: KomgaUser) {
user.sharedLibrariesIds.forEach {
dsl
Expand Down Expand Up @@ -205,6 +219,7 @@ class KomgaUserDao(
dsl.deleteFrom(ar).where(ar.USER_ID.equal(userId)).execute()
dsl.deleteFrom(us).where(us.USER_ID.equal(userId)).execute()
dsl.deleteFrom(ul).where(ul.USER_ID.equal(userId)).execute()
dsl.deleteFrom(ur).where(ur.USER_ID.equal(userId)).execute()
dsl.deleteFrom(u).where(u.ID.equal(userId)).execute()
}

Expand All @@ -214,6 +229,7 @@ class KomgaUserDao(
dsl.deleteFrom(ar).execute()
dsl.deleteFrom(us).execute()
dsl.deleteFrom(ul).execute()
dsl.deleteFrom(ur).execute()
dsl.deleteFrom(u).execute()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package org.gotson.komga.infrastructure.security

import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.Filter
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.domain.model.ROLE_KOBO_SYNC
import org.gotson.komga.domain.model.ROLE_USER
import org.gotson.komga.domain.model.UserRoles
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.gotson.komga.infrastructure.security.apikey.ApiKeyAuthenticationFilter
import org.gotson.komga.infrastructure.security.apikey.ApiKeyAuthenticationProvider
import org.gotson.komga.infrastructure.security.apikey.HeaderApiKeyAuthenticationConverter
import org.gotson.komga.infrastructure.security.apikey.UriRegexApiKeyAuthenticationConverter
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest
import org.springframework.boot.actuate.health.HealthEndpoint
Expand Down Expand Up @@ -81,7 +80,7 @@ class SecurityConfiguration(
// this will only show limited details as `management.endpoint.health.show-details` is set to `when-authorized`
it.requestMatchers(EndpointRequest.to(HealthEndpoint::class.java)).permitAll()
// restrict all other actuator endpoints to ADMIN only
it.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole(ROLE_ADMIN)
it.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole(UserRoles.ADMIN.name)

it
.requestMatchers(
Expand All @@ -101,7 +100,7 @@ class SecurityConfiguration(
"/api/**",
"/opds/**",
"/sse/**",
).hasRole(ROLE_USER)
).authenticated()
}.headers { headersConfigurer ->
headersConfigurer.cacheControl { it.disable() } // headers are set in WebMvcConfiguration
headersConfigurer.frameOptions { it.sameOrigin() } // for epubreader iframes
Expand Down Expand Up @@ -174,7 +173,7 @@ class SecurityConfiguration(

securityMatcher("/kobo/**")
authorizeHttpRequests {
authorize(anyRequest, hasRole(ROLE_KOBO_SYNC))
authorize(anyRequest, hasRole(UserRoles.KOBO_SYNC.name))
}

headers {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class KomgaOAuth2UserServiceConfiguration(
private fun tryCreateNewUser(email: String) =
if (komgaProperties.oauth2AccountCreation) {
logger.info { "Creating new user from OAuth2 login: $email" }
userLifecycle.createUser(KomgaUser(email, RandomStringUtils.secure().nextAlphanumeric(12), roleAdmin = false))
userLifecycle.createUser(KomgaUser(email, RandomStringUtils.secure().nextAlphanumeric(12)))
} else {
throw OAuth2AuthenticationException("ERR_1025")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import org.gotson.komga.domain.model.MediaNotReadyException
import org.gotson.komga.domain.model.MediaProfile
import org.gotson.komga.domain.model.MediaUnsupportedException
import org.gotson.komga.domain.model.R2Progression
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
import org.gotson.komga.domain.model.toR2Progression
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.MediaRepository
Expand Down Expand Up @@ -199,7 +197,7 @@ class CommonBookController(
],
produces = [MediaType.ALL_VALUE],
)
@PreAuthorize("hasRole('$ROLE_PAGE_STREAMING')")
@PreAuthorize("hasRole('PAGE_STREAMING')")
fun getBookPageRaw(
@AuthenticationPrincipal principal: KomgaPrincipal,
request: ServletWebRequest,
Expand Down Expand Up @@ -319,7 +317,7 @@ class CommonBookController(
],
produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE],
)
@PreAuthorize("hasRole('$ROLE_FILE_DOWNLOAD')")
@PreAuthorize("hasRole('FILE_DOWNLOAD')")
fun getBookFile(
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable bookId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import org.gotson.komga.domain.model.MediaType.EPUB
import org.gotson.komga.domain.model.R2Device
import org.gotson.komga.domain.model.R2Locator
import org.gotson.komga.domain.model.R2Progression
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
import org.gotson.komga.domain.model.SyncPoint
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.MediaRepository
Expand Down Expand Up @@ -633,7 +632,7 @@ class KoboController(
value = ["v1/books/{bookId}/file/epub"],
produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE],
)
@PreAuthorize("hasRole('$ROLE_FILE_DOWNLOAD')")
@PreAuthorize("hasRole('FILE_DOWNLOAD')")
fun getBookFile(
@AuthenticationPrincipal principal: KomgaPrincipal,
@PathVariable bookId: String,
Expand Down
Loading

0 comments on commit cbb0d61

Please sign in to comment.