Skip to content
Permalink
Browse files

Merge pull request #255 from code-freak/feature/user-details-db

Improve user DB model
  • Loading branch information
erikhofer committed Nov 28, 2019
2 parents f1d9ff9 + dda94ca commit b7097dded9261c752f61112044389b3525fd0a75
Showing with 212 additions and 101 deletions.
  1. +2 −4 build.gradle
  2. +0 −18 src/main/kotlin/de/code_freak/codefreak/auth/AppUser.kt
  3. +0 −26 src/main/kotlin/de/code_freak/codefreak/auth/DevUserDetailsService.kt
  4. +14 −8 src/main/kotlin/de/code_freak/codefreak/auth/LdapUserDetailsContextMapper.kt
  5. +17 −0 src/main/kotlin/de/code_freak/codefreak/auth/SimpleUserDetailsService.kt
  6. +11 −11 src/main/kotlin/de/code_freak/codefreak/auth/lti/LtiAuthenticationProvider.kt
  7. +3 −3 src/main/kotlin/de/code_freak/codefreak/auth/lti/LtiAuthenticationToken.kt
  8. +1 −0 src/main/kotlin/de/code_freak/codefreak/config/AppConfiguration.kt
  9. +3 −3 src/main/kotlin/de/code_freak/codefreak/config/LtiSecurityConfiguration.kt
  10. +5 −8 src/main/kotlin/de/code_freak/codefreak/config/SecurityConfiguration.kt
  11. +38 −1 src/main/kotlin/de/code_freak/codefreak/entity/User.kt
  12. +4 −4 src/main/kotlin/de/code_freak/codefreak/frontend/AssignmentController.kt
  13. +4 −4 src/main/kotlin/de/code_freak/codefreak/frontend/BaseController.kt
  14. +2 −2 src/main/kotlin/de/code_freak/codefreak/frontend/EvaluationController.kt
  15. +2 −2 src/main/kotlin/de/code_freak/codefreak/graphql/api/AssignmentApi.kt
  16. +1 −1 src/main/kotlin/de/code_freak/codefreak/graphql/api/AuthApi.kt
  17. +1 −1 src/main/kotlin/de/code_freak/codefreak/repository/UserRepository.kt
  18. +20 −3 src/main/kotlin/de/code_freak/codefreak/service/SeedDatabaseService.kt
  19. +34 −0 src/main/kotlin/de/code_freak/codefreak/service/UserService.kt
  20. +2 −2 src/main/kotlin/de/code_freak/codefreak/util/FrontendUtil.kt
  21. +3 −0 src/main/resources/db/changelog-master.yaml
  22. +45 −0 src/main/resources/db/changelogs/20191025095704-user-properties.yaml
@@ -78,13 +78,11 @@ dependencies {

// workaround for https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/issues/1468
implementation 'org.springframework.security.oauth:spring-security-oauth2:2.1.5.RELEASE'

liquibaseRuntime 'org.liquibase:liquibase-core:3.6.3'

liquibaseRuntime 'org.liquibase.ext:liquibase-hibernate5:3.6'
liquibaseRuntime 'com.h2database:h2:1.4.199'
liquibaseRuntime 'org.yaml:snakeyaml:1.15'
liquibaseRuntime 'org.springframework.boot:spring-boot-starter-data-jpa'
liquibaseRuntime 'org.jetbrains.kotlin:kotlin-stdlib:1.3.50'
liquibaseRuntime sourceSets.main.runtimeClasspath
liquibaseRuntime sourceSets.main.output
}

This file was deleted.

This file was deleted.

@@ -1,8 +1,7 @@
package de.code_freak.codefreak.auth

import de.code_freak.codefreak.config.AppConfiguration
import de.code_freak.codefreak.entity.User
import de.code_freak.codefreak.repository.UserRepository
import de.code_freak.codefreak.service.UserService
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.ldap.core.DirContextAdapter
@@ -16,7 +15,7 @@ import org.springframework.stereotype.Component
class LdapUserDetailsContextMapper : UserDetailsContextMapper {

@Autowired
private lateinit var userRepository: UserRepository
private lateinit var userService: UserService

@Autowired
private lateinit var config: AppConfiguration
@@ -50,11 +49,18 @@ class LdapUserDetailsContextMapper : UserDetailsContextMapper {
}
}

val user = userRepository.findByUsernameIgnoreCase(username!!).orElseGet { userRepository.save(User(username)) }
val user = userService.getOrCreateUser(username!!) {
firstName = config.ldap.firstNameAttribute?.let { ctx?.getStringAttribute(it) }
lastName = config.ldap.lastNameAttribute?.let { ctx?.getStringAttribute(it) }
if (config.ldap.forceLdapRoles) {
// force synchronisation with LDAP roles by removing all current roles
this.roles.clear()
}
// merge roles from LDAP with current ones in database
// this allows promotion but not demotion
this.roles.addAll(roles)
}
log.debug("Logging in ${user.username} with roles $roles")
return AppUser(user, roles,
firstName = config.ldap.firstNameAttribute?.let { ctx?.getStringAttribute(it) },
lastName = config.ldap.lastNameAttribute?.let { ctx?.getStringAttribute(it) }
)
return user
}
}
@@ -0,0 +1,17 @@
package de.code_freak.codefreak.auth

import de.code_freak.codefreak.service.EntityNotFoundException
import de.code_freak.codefreak.service.UserService
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException

class SimpleUserDetailsService(private val userService: UserService) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
return try {
userService.getUser(username)
} catch (e: EntityNotFoundException) {
throw UsernameNotFoundException("User $username cannot be found")
}
}
}
@@ -1,16 +1,15 @@
package de.code_freak.codefreak.auth.lti

import com.nimbusds.jwt.JWTClaimsSet
import de.code_freak.codefreak.auth.AppUser
import de.code_freak.codefreak.auth.Role
import de.code_freak.codefreak.entity.User
import de.code_freak.codefreak.repository.UserRepository
import de.code_freak.codefreak.service.UserService
import org.mitre.openid.connect.client.OIDCAuthenticationProvider
import org.mitre.openid.connect.model.PendingOIDCAuthenticationToken
import org.slf4j.LoggerFactory
import org.springframework.security.core.Authentication

class LtiAuthenticationProvider(private val userRepository: UserRepository) : OIDCAuthenticationProvider() {
class LtiAuthenticationProvider(private val userService: UserService) : OIDCAuthenticationProvider() {

private val log = LoggerFactory.getLogger(this::class.java)

@@ -32,22 +31,23 @@ class LtiAuthenticationProvider(private val userRepository: UserRepository) : OI

val roles = buildAuthorities(claims)
return LtiAuthenticationToken(
buildAppUser(claims, roles),
buildUser(claims, roles),
authentication.accessTokenValue,
roles,
claims
)
}

private fun buildAppUser(claims: JWTClaimsSet, roles: List<Role>): AppUser {
private fun buildUser(claims: JWTClaimsSet, roles: List<Role>): User {
val username = claims.getStringClaim("email")
val user = userRepository.findByUsernameIgnoreCase(username!!).orElseGet { userRepository.save(User(username)) }
val user = userService.getOrCreateUser(username) {
firstName = claims.getStringClaim("given_name")
lastName = claims.getStringClaim("family_name")
}
// roles from LTI should not be persisted
user.roles = roles.toMutableSet()
log.debug("Logging in ${user.username} with roles $roles")
return AppUser(
user, roles,
firstName = claims.getStringClaim("given_name"),
lastName = claims.getStringClaim("family_name")
)
return user
}

private fun buildAuthorities(claims: JWTClaimsSet): List<Role> {
@@ -1,12 +1,12 @@
package de.code_freak.codefreak.auth.lti

import com.nimbusds.jwt.JWTClaimsSet
import de.code_freak.codefreak.auth.AppUser
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails

class LtiAuthenticationToken(
val appUser: AppUser,
val user: UserDetails,
val accessToken: String,
authorities: List<GrantedAuthority>,
val claims: JWTClaimsSet
@@ -15,6 +15,6 @@ class LtiAuthenticationToken(
super.setAuthenticated(true)
}

override fun getPrincipal() = appUser
override fun getPrincipal() = user.username
override fun getCredentials() = accessToken
}
@@ -107,6 +107,7 @@ class AppConfiguration {
var groupSearchFilter = "member={0}"
/** Manually set the roles for a specific username */
var overrideRoles: Map<String, Role> = mapOf()
var forceLdapRoles = true
}

class Files {
@@ -6,7 +6,7 @@ import de.code_freak.codefreak.auth.lti.IdCodeAuthRequestBuilder
import de.code_freak.codefreak.auth.lti.LtiAuthenticationFilter
import de.code_freak.codefreak.auth.lti.LtiAuthenticationProvider
import de.code_freak.codefreak.auth.lti.LtiAuthenticationSuccessHandler
import de.code_freak.codefreak.repository.UserRepository
import de.code_freak.codefreak.service.UserService
import org.mitre.jwt.signer.service.JWTSigningAndValidationService
import org.mitre.jwt.signer.service.impl.DefaultJWTSigningAndValidationService
import org.mitre.oauth2.model.ClientDetailsEntity
@@ -37,7 +37,7 @@ import java.security.KeyStore
@Order(1)
class LtiSecurityConfiguration(
@Autowired appConfiguration: AppConfiguration,
@Autowired val userRepository: UserRepository
@Autowired val userService: UserService
) : WebSecurityConfigurerAdapter() {
val config = appConfiguration.lti
private val ltiLoginPath = "/lti/login"
@@ -68,7 +68,7 @@ class LtiSecurityConfiguration(
}

override fun configure(auth: AuthenticationManagerBuilder?) {
auth?.authenticationProvider(LtiAuthenticationProvider(userRepository))
auth?.authenticationProvider(LtiAuthenticationProvider(userService))
}

@Bean
@@ -1,15 +1,14 @@
package de.code_freak.codefreak.config

import de.code_freak.codefreak.auth.AuthenticationMethod
import de.code_freak.codefreak.auth.DevUserDetailsService
import de.code_freak.codefreak.auth.SimpleUserDetailsService
import de.code_freak.codefreak.auth.LdapUserDetailsContextMapper
import de.code_freak.codefreak.service.UserService
import de.code_freak.codefreak.util.withTrailingSlash
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.env.Environment
import org.springframework.core.env.Profiles
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.BeanIds
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
@@ -25,7 +24,7 @@ class SecurityConfiguration : WebSecurityConfigurerAdapter() {
lateinit var config: AppConfiguration

@Autowired
lateinit var env: Environment
lateinit var userService: UserService

@Autowired(required = false)
var ldapUserDetailsContextMapper: LdapUserDetailsContextMapper? = null
@@ -50,13 +49,11 @@ class SecurityConfiguration : WebSecurityConfigurerAdapter() {
?.and()
?.csrf()?.ignoringAntMatchers("/graphql")
}

@Bean
override fun userDetailsService(): UserDetailsService {
return when (config.authenticationMethod) {
AuthenticationMethod.SIMPLE -> when (env.acceptsProfiles(Profiles.of("dev", "test"))) {
true -> DevUserDetailsService()
false -> throw NotImplementedError("Simple authentication is currently only supported in dev mode.")
}
AuthenticationMethod.SIMPLE -> SimpleUserDetailsService(userService)
else -> super.userDetailsService()
}
}
@@ -1,6 +1,43 @@
package de.code_freak.codefreak.entity

import de.code_freak.codefreak.auth.Role
import org.springframework.security.core.CredentialsContainer
import org.springframework.security.core.userdetails.UserDetails
import javax.persistence.CollectionTable
import javax.persistence.Column
import javax.persistence.ElementCollection
import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated
import javax.persistence.FetchType

@Entity
class User(val username: String) : BaseEntity()
class User(private val username: String) : BaseEntity(), UserDetails, CredentialsContainer {
@Column(unique = true)
val usernameCanonical = username.toLowerCase()

@ElementCollection(targetClass = Role::class, fetch = FetchType.EAGER)
@CollectionTable
@Enumerated(EnumType.STRING)
@Column(name = "role")
var roles: MutableSet<Role> = mutableSetOf()

var firstName: String? = null

var lastName: String? = null

var password: String? = null
@JvmName("_getPassword") get

fun getDisplayName() = listOfNotNull(firstName, lastName).ifEmpty { listOf(username) }.joinToString(" ")
override fun getUsername() = username
override fun getPassword() = password
override fun getAuthorities() = roles.flatMap { it.allGrantedAuthorities }.toMutableList()
override fun isEnabled() = true
override fun isCredentialsNonExpired() = true
override fun isAccountNonExpired() = true
override fun isAccountNonLocked() = true
override fun eraseCredentials() {
password = null
}
}
@@ -46,10 +46,10 @@ class AssignmentController : BaseController() {

@GetMapping("/assignments")
fun getAssignment(model: Model): String {
val assignments = if (user.authorities.contains(Role.TEACHER)) {
val assignments = if (user.roles.contains(Role.TEACHER)) {
assignmentService.findAllAssignments()
} else {
assignmentService.findAllAssignmentsForUser(user.entity.id)
assignmentService.findAllAssignmentsForUser(user.id)
}
model.addAttribute("assignments", assignments)
return "assignments"
@@ -61,7 +61,7 @@ class AssignmentController : BaseController() {
model: Model
): String {
val assignment = assignmentService.findAssignment(assignmentId)
val answerIds = answerService.getAnswerIdsForTaskIds(assignment.tasks.map { it.id }, user.entity.id)
val answerIds = answerService.getAnswerIdsForTaskIds(assignment.tasks.map { it.id }, user.id)
val latestEvaluations = evaluationService.getLatestEvaluations(answerIds.values)
val taskInfos = assignment.tasks.map {
val answerId = answerIds[it.id]
@@ -93,7 +93,7 @@ class AssignmentController : BaseController() {

ByteArrayOutputStream().use { out ->
TarUtil.writeUploadAsTar(file, out)
val result = assignmentService.createFromTar(out.toByteArray(), user.entity, deadline)
val result = assignmentService.createFromTar(out.toByteArray(), user, deadline)
model.successMessage("Assignment has been created.")
if (result.taskErrors.isNotEmpty()) {
model.errorMessage("Not all tasks could be imported successfully:\n" + result.taskErrors.map { "${it.key}: ${it.value.message}" }.joinToString("\n"))
@@ -1,8 +1,8 @@
package de.code_freak.codefreak.frontend

import de.code_freak.codefreak.auth.AppUser
import de.code_freak.codefreak.config.AppConfiguration
import de.code_freak.codefreak.entity.Submission
import de.code_freak.codefreak.entity.User
import de.code_freak.codefreak.service.AnswerService
import de.code_freak.codefreak.service.AssignmentService
import de.code_freak.codefreak.service.SubmissionService
@@ -39,15 +39,15 @@ abstract class BaseController {
@Autowired
protected lateinit var config: AppConfiguration

protected val user: AppUser
protected val user: User
get() = FrontendUtil.getCurrentUser()

/**
* Returns the submission for the given assignment or creates one if there is none already.
*/
protected fun getOrCreateSubmission(assignmentId: UUID): Submission {
return submissionService.findSubmission(assignmentId, user.entity.id).orElseGet {
submissionService.createSubmission(assignmentService.findAssignment(assignmentId), user.entity)
return submissionService.findSubmission(assignmentId, user.id).orElseGet {
submissionService.createSubmission(assignmentService.findAssignment(assignmentId), user)
}
}

@@ -55,7 +55,7 @@ class EvaluationController : BaseController() {

@PostMapping("/evaluations")
fun startEvaluation(@RequestParam("taskId") taskId: UUID, model: RedirectAttributes): String {
val answer = answerService.getAnswerForTaskId(taskId, user.entity.id)
val answer = answerService.getAnswerForTaskId(taskId, user.id)
val assignmentPage = urls.get(answer.task.assignment)
return withErrorPage(assignmentPage) {
answer.task.assignment.requireNotClosed()
@@ -69,7 +69,7 @@ class EvaluationController : BaseController() {
@GetMapping("/evaluations/{evaluationId}")
fun getEvaluation(@PathVariable("evaluationId") evaluationId: UUID, model: Model): String {
val evaluation = evaluationService.getEvaluation(evaluationId)
if (!user.authorities.contains(Role.TEACHER) && evaluation.answer.submission.user != user.entity) {
if (!user.roles.contains(Role.TEACHER) && evaluation.answer.submission.user != user) {
throw AccessDeniedException("Cannot access evaluation")
}
val latestEvaluation = evaluationService.getLatestEvaluation(evaluation.answer.id).orElse(null)

0 comments on commit b7097dd

Please sign in to comment.
You can’t perform that action at this time.