Skip to content

Commit

Permalink
feat: ldap support
Browse files Browse the repository at this point in the history
see Documentation for setup : https://bayang.github.io/jelu-web/
  • Loading branch information
bayang committed Jun 2, 2022
1 parent 035ca42 commit 9def6a1
Show file tree
Hide file tree
Showing 18 changed files with 303 additions and 16 deletions.
4 changes: 3 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
id("org.springframework.boot") version "2.6.6"
id("org.springframework.boot") version "2.7.0"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.10"
kotlin("plugin.spring") version "1.6.10"
Expand Down Expand Up @@ -40,6 +40,8 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.session:spring-session-core")
implementation("com.github.gotson:spring-session-caffeine:1.0.3")
implementation("org.springframework.security:spring-security-ldap")
// implementation("com.unboundid:unboundid-ldapsdk:6.0.5")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

implementation("com.github.ben-manes.caffeine:caffeine")
Expand Down
2 changes: 2 additions & 0 deletions src/jelu-ui/src/components/ProfilePage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useI18n } from 'vue-i18n'
import { useStore } from 'vuex'
import { key } from '../store'
import UserModalVue from './UserModal.vue'
import { Provider } from "../model/User"
const {oruga} = useProgrammatic()
Expand Down Expand Up @@ -71,6 +72,7 @@ function modalClosed() {
</p>
<div class="card-actions justify-end">
<button
v-if="user.provider !== Provider.LDAP"
v-tooltip="t('profile.edit_user')"
class="btn btn-circle btn-ghost"
@click="toggleUserModal"
Expand Down
3 changes: 2 additions & 1 deletion src/jelu-ui/src/model/ServerSettings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface ServerSettings {
metadataFetchEnabled: boolean,
metadataFetchCalibreEnabled: boolean,
appVersion: string
appVersion: string,
ldapEnabled: boolean
}
13 changes: 10 additions & 3 deletions src/jelu-ui/src/model/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ export interface User {
creationDate?: string,
login: string,
isAdmin: boolean,
modificationDate?: string
modificationDate?: string,
provider?: Provider
}
export interface UserAuthentication {
user: User,
Expand All @@ -12,9 +13,15 @@ export interface UserAuthentication {
export interface CreateUser {
login: string,
password: string,
isAdmin: boolean
isAdmin: boolean,
provider?: Provider
}
export interface UpdateUser {
password: string,
isAdmin?: boolean
isAdmin?: boolean,
provider?: Provider
}
export enum Provider {
LDAP = 'LDAP',
JELU_DB = 'JELU_DB'
}
4 changes: 4 additions & 0 deletions src/jelu-ui/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const store = createStore<State>({
serverSettings: {
metadataFetchEnabled: false,
metadataFetchCalibreEnabled: false,
ldapEnabled: false,
appVersion: ""
} as ServerSettings,
}
Expand Down Expand Up @@ -124,6 +125,9 @@ const store = createStore<State>({
getMetadataFetchEnabled(state): boolean {
return state.serverSettings.metadataFetchEnabled
},
getLdapEnabled(state): boolean {
return state.serverSettings.ldapEnabled
},
getLogged(state): boolean {
return state.isLogged
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.github.bayang.jelu.config

import io.github.bayang.jelu.dao.Provider
import io.github.bayang.jelu.dao.UserRepository
import io.github.bayang.jelu.dto.CreateUserDto
import io.github.bayang.jelu.dto.JeluUser
import io.github.bayang.jelu.dto.UpdateUserDto
import mu.KotlinLogging
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.ldap.core.DirContextAdapter
import org.springframework.ldap.core.DirContextOperations
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import javax.naming.directory.Attributes

private val logger = KotlinLogging.logger {}

@Component
@ConditionalOnProperty(name = ["jelu.auth.ldap.enabled"], havingValue = "true", matchIfMissing = false)
class JeluLdapUserDetailsContextMapper(
private val userRepository: UserRepository,
) : UserDetailsContextMapper {

@Transactional
override fun mapUserFromContext(
ctx: DirContextOperations?,
username: String?,
authorities: MutableCollection<out GrantedAuthority>?
): UserDetails {
dumpAttributesForDebug(ctx?.attributes)
val isAdmin = findAdminMembership(ctx?.attributes)
val res = userRepository.findByLoginAndProvider(username!!, Provider.LDAP)
if (res.empty()) {
val saved = userRepository.save(CreateUserDto(login = username, password = "ldap", isAdmin = isAdmin, Provider.LDAP))
return JeluUser(saved)
}
var user = res.first()
if (user.isAdmin != isAdmin) {
user = userRepository.updateUser(user.id.value, UpdateUserDto(password = "ldap", isAdmin = isAdmin, provider = null))
}
return JeluUser(user)
}

private fun findAdminMembership(attributes: Attributes?): Boolean {
var isAdmin = false
if (attributes != null) {
val all = attributes.all
while (all.hasMoreElements()) {
val att = all.next()
if (att != null) {
if (att.id.equals("memberOf")) {
var values = ""
if (att.all != null) {
values = att.all.toList().joinToString()
}
if (! values.isNullOrBlank() && values.contains("jelu-admin")) {
isAdmin = true
}
}
}
}
}
return isAdmin
}

override fun mapUserToContext(user: UserDetails?, ctx: DirContextAdapter?) {
TODO("Not yet implemented")
}

fun dumpAttributesForDebug(attributes: Attributes?) {
logger.trace { "ldap attributes : " }
if (attributes != null) {
val all = attributes.all
while (all.hasMoreElements()) {
val att = all.next()
if (att != null) {
var values = ""
if (att.all != null) {
values = att.all.toList().joinToString()
}
logger.trace { "Attribute: ${att.id} -> $values" }
}
}
}
}
}
15 changes: 14 additions & 1 deletion src/main/kotlin/io/github/bayang/jelu/config/JeluProperties.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ data class JeluProperties(
val files: Files,
val session: Session,
val cors: Cors = Cors(),
val metadata: Metadata = Metadata(Calibre(null))
val metadata: Metadata = Metadata(Calibre(null)),
val auth: Auth = Auth(Ldap())
) {

data class Database(
Expand Down Expand Up @@ -42,4 +43,16 @@ data class JeluProperties(
data class Metadata(
var calibre: Calibre
)

data class Auth(
var ldap: Ldap
)

data class Ldap(
var enabled: Boolean = false,
val url: String = "",
val userDnPatterns: List<String> = emptyList(),
val userSearchFilter: String = "",
val userSearchBase: String = ""
)
}
44 changes: 44 additions & 0 deletions src/main/kotlin/io/github/bayang/jelu/config/LdapConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.github.bayang.jelu.config

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.ldap.core.support.BaseLdapPathContextSource
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.ldap.DefaultSpringSecurityContextSource
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticator
import org.springframework.security.ldap.authentication.BindAuthenticator
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper

@Configuration
@ConditionalOnProperty(name = ["jelu.auth.ldap.enabled"], havingValue = "true", matchIfMissing = false)
class LdapConfig(
private val userDetailsContextMapper: UserDetailsContextMapper,
private val properties: JeluProperties
) {

@Bean
fun contextSource(): BaseLdapPathContextSource {
return DefaultSpringSecurityContextSource(properties.auth.ldap.url)
}

@Bean
fun authenticationProvider(contextSource: BaseLdapPathContextSource): AuthenticationProvider {
val authenticator: AbstractLdapAuthenticator = BindAuthenticator(contextSource)
if (properties.auth.ldap.userDnPatterns.isNotEmpty()) {
authenticator.setUserDnPatterns(properties.auth.ldap.userDnPatterns.toTypedArray())
}
if (! properties.auth.ldap.userSearchFilter.isNullOrBlank()) {
val userSearchBase = if (properties.auth.ldap.userSearchBase.isNullOrBlank()) "" else properties.auth.ldap.userSearchBase
authenticator.setUserSearch(
FilterBasedLdapUserSearch(userSearchBase, properties.auth.ldap.userSearchFilter, contextSource)
)
}
authenticator.afterPropertiesSet()
val provider: LdapAuthenticationProvider = LdapAuthenticationProvider(authenticator)
provider.setUserDetailsContextMapper(userDetailsContextMapper)
return provider
}
}
26 changes: 23 additions & 3 deletions src/main/kotlin/io/github/bayang/jelu/config/SecurityConfig.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
package io.github.bayang.jelu.config

import org.springframework.context.annotation.Bean
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.ProviderManager
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain

@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {
class SecurityConfig(
private val authenticationProvider: AuthenticationProvider?,
private val properties: JeluProperties,
private val userDetailsService: UserDetailsService,
private val passwordEncoder: PasswordEncoder,
) {

override fun configure(http: HttpSecurity) {
@Bean
@Throws(Exception::class)
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
http
.cors { }
.csrf { it.disable() }
Expand Down Expand Up @@ -45,5 +58,12 @@ class SecurityConfig : WebSecurityConfigurerAdapter() {
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
if (properties.auth.ldap.enabled) {
val dao = DaoAuthenticationProvider()
dao.setUserDetailsService(userDetailsService)
dao.setPasswordEncoder(passwordEncoder)
http.authenticationManager(ProviderManager(authenticationProvider, dao))
}
return http.build()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.bayang.jelu.controllers

import io.github.bayang.jelu.dao.Provider
import io.github.bayang.jelu.dto.AuthenticationDto
import io.github.bayang.jelu.dto.CreateUserDto
import io.github.bayang.jelu.dto.DummyUser
Expand Down Expand Up @@ -53,7 +54,8 @@ class UsersController(
id = null,
password = "****",
modificationDate = null,
creationDate = null
creationDate = null,
provider = Provider.JELU_DB
),
token = session.id
)
Expand All @@ -68,7 +70,8 @@ class UsersController(
id = (principal.principal as JeluUser).user.id.value,
password = "****",
modificationDate = null,
creationDate = null
creationDate = null,
provider = (principal.principal as JeluUser).user.provider
),
token = session.id
)
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/io/github/bayang/jelu/dao/UserRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.bayang.jelu.dto.UpdateUserDto
import io.github.bayang.jelu.utils.nowInstant
import mu.KotlinLogging
import org.jetbrains.exposed.sql.SizedIterable
import org.jetbrains.exposed.sql.and
import org.springframework.stereotype.Repository
import java.time.Instant
import java.util.UUID
Expand All @@ -31,6 +32,9 @@ class UserRepository {
fun findByLogin(login: String): SizedIterable<User> =
User.find { UserTable.login eq login }

fun findByLoginAndProvider(login: String, provider: Provider): SizedIterable<User> =
User.find { UserTable.login eq login and(UserTable.provider eq provider) }

fun findUserById(id: UUID): User = User[id]

fun save(user: CreateUserDto): User {
Expand All @@ -41,6 +45,7 @@ class UserRepository {
modificationDate = instant
password = user.password
isAdmin = user.isAdmin
provider = user.provider
}
return created
}
Expand All @@ -52,6 +57,9 @@ class UserRepository {
if (updateUserDto.isAdmin != null) {
this.isAdmin = updateUserDto.isAdmin
}
if (updateUserDto.provider != null) {
this.provider = updateUserDto.provider
}
}
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/io/github/bayang/jelu/dao/UserTable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ object UserTable : UUIDTable("user") {
val login: Column<String> = varchar("login", 50)
val password: Column<String> = varchar("password", 1000)
val isAdmin: Column<Boolean> = bool("is_admin")
val provider = enumerationByName("provider", 200, Provider::class)
}

class User(id: EntityID<UUID>) : UUIDEntity(id) {
Expand All @@ -25,6 +26,7 @@ class User(id: EntityID<UUID>) : UUIDEntity(id) {
login = this.login,
password = "****",
isAdmin = this.isAdmin,
provider = this.provider
)

companion object : UUIDEntityClass<User>(UserTable)
Expand All @@ -33,5 +35,11 @@ class User(id: EntityID<UUID>) : UUIDEntity(id) {
var login by UserTable.login
var password by UserTable.password
var isAdmin by UserTable.isAdmin
var provider by UserTable.provider
val userBooks by UserBook referrersOn UserBookTable.book
}

enum class Provider {
LDAP,
JELU_DB
}
Loading

0 comments on commit 9def6a1

Please sign in to comment.