mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
WIP: Implement SSO
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="UI debug" type="JavascriptDebugType" engineId="37cae5b9-e8b2-4949-9172-aafa37fbc09c" uri="http://localhost:8080">
|
||||
<configuration default="false" name="UI debug" type="JavascriptDebugType" uri="http://localhost:8080">
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
+5
-1
@@ -37,7 +37,6 @@ dependencies {
|
||||
// Spring Boot & Kotlin
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("jakarta.validation:jakarta.validation-api:3.0.2")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
@@ -56,6 +55,11 @@ dependencies {
|
||||
implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.14")
|
||||
implementation("org.springframework.cloud:spring-cloud-starter")
|
||||
|
||||
// SSO
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
|
||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||
|
||||
// Development
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
|
||||
@@ -2,11 +2,20 @@ import {useAuth} from "Frontend/util/auth";
|
||||
import {GearFine, Question, SignOut, User} from "@phosphor-icons/react";
|
||||
import {Avatar, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger} from "@nextui-org/react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {ConfigController} from "Frontend/generated/endpoints";
|
||||
|
||||
export default function ProfileMenu() {
|
||||
const auth = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function logout() {
|
||||
if (auth.state.user?.managedBySso) {
|
||||
window.location.href = await ConfigController.getLogoutUrl() || "/";
|
||||
} else {
|
||||
await auth.logout();
|
||||
}
|
||||
}
|
||||
|
||||
const profileMenuItems = [
|
||||
{
|
||||
label: "My Profile",
|
||||
@@ -27,7 +36,7 @@ export default function ProfileMenu() {
|
||||
{
|
||||
label: "Sign Out",
|
||||
icon: <SignOut/>,
|
||||
onClick: () => auth.logout(),
|
||||
onClick: logout,
|
||||
color: "primary"
|
||||
},
|
||||
];
|
||||
|
||||
@@ -77,6 +77,8 @@ export default function ProfileManagement() {
|
||||
<Form>
|
||||
<div className="flex flex-row flex-grow justify-between mb-8">
|
||||
<h2 className="text-2xl font-bold">My Profile</h2>
|
||||
{auth.state.user?.managedBySso &&
|
||||
<p className="text-warning">Your account is managed externally.</p>}
|
||||
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{formik.values.newPassword.length > 0 &&
|
||||
@@ -88,7 +90,7 @@ export default function ProfileManagement() {
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting || configSaved}
|
||||
disabled={formik.isSubmitting || configSaved || auth.state.user?.managedBySso}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : configSaved ? <Check/> : "Save"}
|
||||
@@ -105,24 +107,28 @@ export default function ProfileManagement() {
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<NextUiInput type="file" accept="image/*" onChange={onFileSelected}/>
|
||||
<NextUiInput type="file" accept="image/*" onChange={onFileSelected}
|
||||
isDisabled={auth.state.user?.managedBySso}/>
|
||||
<Button onClick={() => uploadAvatar(avatar)} isDisabled={avatar == null}
|
||||
color="success">Upload</Button>
|
||||
<Tooltip content="Remove your current avatar">
|
||||
<Button onClick={removeAvatar} isIconOnly color="danger"><Trash/></Button>
|
||||
<Button onClick={removeAvatar} isIconOnly color="danger"
|
||||
isDisabled={auth.state.user?.managedBySso}><Trash/></Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-grow">
|
||||
<Section title="Personal information"/>
|
||||
<Input name="username" label="Username" type="text" autocomplete="username"/>
|
||||
<Input name="email" label="Email" type="email" autocomplete="email"/>
|
||||
<Input name="username" label="Username" type="text" autocomplete="username"
|
||||
isDisabled={auth.state.user?.managedBySso}/>
|
||||
<Input name="email" label="Email" type="email" autocomplete="email"
|
||||
isDisabled={auth.state.user?.managedBySso}/>
|
||||
<Section title="Security"/>
|
||||
<Input name="newPassword" label="New Password" type="password"
|
||||
autocomplete="new-password"/>
|
||||
autocomplete="new-password" isDisabled={auth.state.user?.managedBySso}/>
|
||||
<Input name="passwordRepeat" label="Repeat password" type="password"
|
||||
autocomplete="new-password"/>
|
||||
autocomplete="new-password" isDisabled={auth.state.user?.managedBySso}/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -1,11 +1,75 @@
|
||||
import React from "react";
|
||||
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||
import * as Yup from 'yup';
|
||||
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||
import Section from "Frontend/components/general/Section";
|
||||
import {Button} from "@nextui-org/react";
|
||||
import {MagicWand} from "@phosphor-icons/react";
|
||||
import {toast} from "sonner";
|
||||
|
||||
function SsoMangementLayout({getConfig, formik}: any) {
|
||||
|
||||
function isAutoPopulateDisabled() {
|
||||
return !formik.values.sso.oidc.enabled || !formik.values.sso.oidc["issuer-url"];
|
||||
}
|
||||
|
||||
async function autoPopulate() {
|
||||
let issuerUrl: string = formik.values.sso.oidc["issuer-url"];
|
||||
if (issuerUrl.endsWith("/")) issuerUrl = issuerUrl.slice(0, -1);
|
||||
|
||||
try {
|
||||
const response = await fetch(issuerUrl + "/.well-known/openid-configuration");
|
||||
const data = await response.json();
|
||||
|
||||
formik.setFieldValue("sso.oidc.authorize-url", data.authorization_endpoint);
|
||||
formik.setFieldValue("sso.oidc.token-url", data.token_endpoint);
|
||||
formik.setFieldValue("sso.oidc.userinfo-url", data.userinfo_endpoint);
|
||||
formik.setFieldValue("sso.oidc.logout-url", data.end_session_endpoint);
|
||||
formik.setFieldValue("sso.oidc.jwks-url", data.jwks_uri);
|
||||
} catch (e) {
|
||||
toast.error("Failed to auto-populate SSO configuration");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-col flex-1">
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.enabled")}/>
|
||||
|
||||
<Section title="SSO user handling"/>
|
||||
<div className="flex flex-row">
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.auto-register-new-users")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.match-existing-users-by")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
</div>
|
||||
|
||||
<Section title="SSO provider configuration"/>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.client-id")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.client-secret")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
<div className="flex flex-row gap-2">
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.issuer-url")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
<Button
|
||||
isDisabled={isAutoPopulateDisabled()}
|
||||
onPress={autoPopulate}
|
||||
className="h-14 mt-2"><MagicWand/> Auto-populate</Button>
|
||||
</div>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.authorize-url")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.token-url")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.userinfo-url")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.logout-url")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.jwks-url")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ function UserManagementLayout({getConfig, formik}: any) {
|
||||
{users.map((user) => <UserManagementCard user={user} key={user.username}/>)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
;
|
||||
);
|
||||
}
|
||||
|
||||
export const UserManagement = withConfigPage(UserManagementLayout, "User Management", "users");
|
||||
@@ -9,7 +9,7 @@ const Input = ({label, ...props}) => {
|
||||
const [field, meta] = useField(props);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-grow max-w-sm items-start gap-2 my-2">
|
||||
<div className="flex flex-col flex-grow items-start gap-2 my-2">
|
||||
<NextUiInput
|
||||
{...props}
|
||||
{...field}
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.grimsi.gameyfin.config
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import de.grimsi.gameyfin.config.dto.ConfigEntryDto
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import jakarta.annotation.security.PermitAll
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
|
||||
@Endpoint
|
||||
@@ -15,10 +16,20 @@ class ConfigController(
|
||||
return appConfigService.getAllConfigValues(prefix)
|
||||
}
|
||||
|
||||
fun getConfig(key: String): String {
|
||||
fun getConfig(key: String): String? {
|
||||
return appConfigService.getConfigValue(key)
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
fun isSsoEnabled(): Boolean? {
|
||||
return appConfigService.getConfigValue(ConfigProperties.SsoEnabled)
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
fun getLogoutUrl(): String? {
|
||||
return appConfigService.getConfigValue(ConfigProperties.SsoLogoutUrl)
|
||||
}
|
||||
|
||||
fun setConfig(key: String, value: String) {
|
||||
appConfigService.setConfigValue(key, value)
|
||||
}
|
||||
|
||||
@@ -104,6 +104,12 @@ sealed class ConfigProperties<T : Serializable>(
|
||||
"JWKS URL"
|
||||
)
|
||||
|
||||
data object SsoLogoutUrl : ConfigProperties<String>(
|
||||
String::class,
|
||||
"sso.oidc.logout-url",
|
||||
"Logout URL"
|
||||
)
|
||||
|
||||
data object SsoMatchExistingUsersBy : ConfigProperties<MatchUsersBy>(
|
||||
MatchUsersBy::class,
|
||||
"sso.oidc.match-existing-users-by",
|
||||
|
||||
@@ -50,10 +50,9 @@ class ConfigService(
|
||||
* Used internally.
|
||||
*
|
||||
* @param configProperty: The config property containing necessary type information
|
||||
* @return The current value if set or the default value
|
||||
* @throws IllegalArgumentException if no value is set and no default value exists
|
||||
* @return The current value if set or the default value or null if no value is set and no default value exists
|
||||
*/
|
||||
fun <T : Serializable> getConfigValue(configProperty: ConfigProperties<T>): T {
|
||||
fun <T : Serializable> getConfigValue(configProperty: ConfigProperties<T>): T? {
|
||||
|
||||
log.info { "Getting config value '${configProperty.key}'" }
|
||||
|
||||
@@ -61,7 +60,7 @@ class ConfigService(
|
||||
return if (appConfig != null) {
|
||||
getValue(appConfig.value, configProperty)
|
||||
} else {
|
||||
configProperty.default ?: throw IllegalArgumentException("No value found for key: ${configProperty.key}")
|
||||
configProperty.default ?: return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +69,9 @@ class ConfigService(
|
||||
* Used for the external API.
|
||||
*
|
||||
* @param key: The key of the config property
|
||||
* @return The current value if set or the default value
|
||||
* @throws IllegalArgumentException if no value is set and no default value exists
|
||||
* @return The current value if set or the default value or null if no value is set and no default value exists
|
||||
*/
|
||||
fun getConfigValue(key: String): String {
|
||||
fun getConfigValue(key: String): String? {
|
||||
|
||||
log.info { "Getting config value '$key'" }
|
||||
|
||||
@@ -83,11 +81,22 @@ class ConfigService(
|
||||
return if (appConfig != null) {
|
||||
getValue(appConfig.value, configProperty).toString()
|
||||
} else {
|
||||
configProperty.default?.toString()
|
||||
?: throw IllegalArgumentException("No value found for key: ${configProperty.key}")
|
||||
configProperty.default?.toString() ?: return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the value for a specified key in a type-safe way.
|
||||
*
|
||||
* @param configProperty: The target config property
|
||||
* @param value: Value to set the config property to
|
||||
* @throws IllegalArgumentException if the value can't be cast to the type defined for the config property
|
||||
*/
|
||||
fun <T : Serializable> setConfigValue(configProperty: ConfigProperties<T>, value: T) {
|
||||
return setConfigValue(configProperty.key, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for a specified key.
|
||||
* Checks if the value can be cast to the type defined for the config property.
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
package de.grimsi.gameyfin.meta
|
||||
|
||||
import com.vaadin.flow.spring.security.VaadinWebSecurity
|
||||
import de.grimsi.gameyfin.config.ConfigProperties
|
||||
import de.grimsi.gameyfin.config.ConfigService
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Conditional
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.core.session.SessionRegistry
|
||||
import org.springframework.security.core.session.SessionRegistryImpl
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType
|
||||
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig(
|
||||
private val environment: Environment
|
||||
private val environment: Environment,
|
||||
private val config: ConfigService
|
||||
) : VaadinWebSecurity() {
|
||||
|
||||
@Bean
|
||||
fun sessionRegistry(): SessionRegistry {
|
||||
return SessionRegistryImpl()
|
||||
}
|
||||
private val ssoProviderKey: String = "oidc"
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun configure(http: HttpSecurity) {
|
||||
@@ -35,12 +43,19 @@ class SecurityConfig(
|
||||
|
||||
http.sessionManagement { sessionManagement ->
|
||||
sessionManagement
|
||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||
.maximumSessions(3)
|
||||
.sessionRegistry(sessionRegistry())
|
||||
}
|
||||
|
||||
super.configure(http)
|
||||
setLoginView(http, "/login")
|
||||
|
||||
if (config.getConfigValue(ConfigProperties.SsoEnabled) == true) {
|
||||
setOAuth2LoginPage(http, "/oauth2/authorization/$ssoProviderKey")
|
||||
http.logout { logout -> logout.logoutSuccessHandler((HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))) }
|
||||
} else {
|
||||
setLoginView(http, "/login")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
@@ -52,6 +67,32 @@ class SecurityConfig(
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun sessionRegistry(): SessionRegistry {
|
||||
return SessionRegistryImpl()
|
||||
}
|
||||
|
||||
// TODO: Maybe switch to a database-backed client registration repository? Not sure if worth it.
|
||||
@Bean
|
||||
@Conditional(SsoEnabledCondition::class)
|
||||
fun clientRegistrationRepository(): ClientRegistrationRepository? {
|
||||
val clientRegistration = ClientRegistration.withRegistrationId(ssoProviderKey)
|
||||
.clientId(config.getConfigValue(ConfigProperties.SsoClientId))
|
||||
.clientSecret(config.getConfigValue(ConfigProperties.SsoClientSecret))
|
||||
.scope("openid", "profile", "email")
|
||||
.userNameAttributeName("preferred_username")
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.issuerUri(config.getConfigValue(ConfigProperties.SsoIssuerUrl))
|
||||
.authorizationUri(config.getConfigValue(ConfigProperties.SsoAuthorizeUrl))
|
||||
.tokenUri(config.getConfigValue(ConfigProperties.SsoTokenUrl))
|
||||
.userInfoUri(config.getConfigValue(ConfigProperties.SsoUserInfoUrl))
|
||||
.jwkSetUri(config.getConfigValue(ConfigProperties.SsoJwksUrl))
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
|
||||
.build()
|
||||
|
||||
return InMemoryClientRegistrationRepository(clientRegistration)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder(): PasswordEncoder {
|
||||
return BCryptPasswordEncoder()
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.grimsi.gameyfin.meta
|
||||
|
||||
import de.grimsi.gameyfin.config.ConfigProperties
|
||||
import org.springframework.context.annotation.Condition
|
||||
import org.springframework.context.annotation.ConditionContext
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata
|
||||
import java.sql.DriverManager
|
||||
|
||||
/**
|
||||
* Since we are loading this config so early the Spring context has not fully loaded yet, not even a DataSource.
|
||||
* Thankfully the environment is already available, and we can use it to connect to the database.
|
||||
* So we are rawdogging the database connection and query execution here.
|
||||
*/
|
||||
class SsoEnabledCondition : Condition {
|
||||
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
|
||||
try {
|
||||
val environment = context.beanFactory!!.getBean(Environment::class.java);
|
||||
val url = environment.getProperty("spring.datasource.url");
|
||||
val user = environment.getProperty("spring.datasource.username");
|
||||
val password = environment.getProperty("spring.datasource.password");
|
||||
val connection = DriverManager.getConnection(url, user, password);
|
||||
|
||||
connection.use { connection ->
|
||||
val statement = connection.prepareStatement("SELECT \"value\" FROM app_config WHERE \"key\" = ?")
|
||||
statement.setString(1, ConfigProperties.SsoEnabled.key)
|
||||
val resultSet = statement.executeQuery()
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getBoolean("value")
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class DynamicAccessInterceptor(
|
||||
// Check if method is annotated with @DynamicPublicAccess
|
||||
if (method.isAnnotationPresent(DynamicPublicAccess::class.java)) {
|
||||
// Check if user is authenticated or public access is enabled
|
||||
if (request.userPrincipal != null || configService.getConfigValue(ConfigProperties.LibraryAllowPublicAccess)) {
|
||||
if (request.userPrincipal != null || configService.getConfigValue(ConfigProperties.LibraryAllowPublicAccess) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class UserEndpoint(
|
||||
@PermitAll
|
||||
fun getUserInfo(): UserInfoDto {
|
||||
val auth: Authentication = SecurityContextHolder.getContext().authentication
|
||||
return userService.getUserInfo(auth.name)
|
||||
return userService.getUserInfo(auth)
|
||||
}
|
||||
|
||||
@RolesAllowed(Roles.Names.SUPERADMIN, Roles.Names.ADMIN)
|
||||
|
||||
@@ -9,12 +9,14 @@ import de.grimsi.gameyfin.users.entities.User
|
||||
import de.grimsi.gameyfin.users.persistence.AvatarContentStore
|
||||
import de.grimsi.gameyfin.users.persistence.UserRepository
|
||||
import jakarta.transaction.Transactional
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.InputStream
|
||||
@@ -50,8 +52,14 @@ class UserService(
|
||||
return userRepository.findAll().map { u -> toUserInfo(u) }
|
||||
}
|
||||
|
||||
fun getUserInfo(username: String): UserInfoDto {
|
||||
val user = userByUsername(username)
|
||||
fun getUserInfo(auth: Authentication): UserInfoDto {
|
||||
val principal = auth.principal
|
||||
|
||||
if (principal is OidcUser) {
|
||||
return toUserInfo(User(principal))
|
||||
}
|
||||
|
||||
val user = userByUsername(auth.name)
|
||||
return toUserInfo(user)
|
||||
}
|
||||
|
||||
@@ -120,6 +128,7 @@ class UserService(
|
||||
return UserInfoDto(
|
||||
username = user.username,
|
||||
email = user.email,
|
||||
managedBySso = user.oidcProviderId != null,
|
||||
roles = user.roles.map { r -> r.rolename }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin.users.dto
|
||||
|
||||
data class UserInfoDto(
|
||||
val username: String,
|
||||
val managedBySso: Boolean,
|
||||
val email: String,
|
||||
val roles: List<String>
|
||||
)
|
||||
@@ -1,8 +1,10 @@
|
||||
package de.grimsi.gameyfin.users.entities
|
||||
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import jakarta.annotation.Nullable
|
||||
import jakarta.persistence.*
|
||||
import jakarta.validation.constraints.NotNull
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser
|
||||
|
||||
|
||||
@Entity
|
||||
@@ -39,4 +41,14 @@ class User(
|
||||
inverseJoinColumns = [JoinColumn(name = "role_id", referencedColumnName = "id")]
|
||||
)
|
||||
var roles: Collection<Role> = emptyList()
|
||||
)
|
||||
) {
|
||||
|
||||
constructor(oidcUser: OidcUser) : this(
|
||||
username = oidcUser.preferredUsername,
|
||||
email = oidcUser.email,
|
||||
oidcProviderId = oidcUser.subject
|
||||
) {
|
||||
// FIXME: Implement role mapping from OIDC provider
|
||||
this.roles = listOf(Role(Roles.ADMIN.roleName))
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface UserRepository : JpaRepository<User, Long> {
|
||||
fun findByUsername(userName: String): User?
|
||||
fun findByOidcProviderId(oidcProviderId: String): User?
|
||||
}
|
||||
Reference in New Issue
Block a user