Skip to content

Commit

Permalink
Merge pull request #147 from NIAEFEUP/feature/serialize-role
Browse files Browse the repository at this point in the history
Create annotation to validate role and serialize roles
  • Loading branch information
MRita443 authored Aug 30, 2024
2 parents cf4c310 + f25ebb6 commit 279d0e6
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class AuthConfig(

fun rolesConverter(): JwtAuthenticationConverter? {
val authoritiesConverter = JwtGrantedAuthoritiesConverter()
authoritiesConverter.setAuthorityPrefix("ROLE_")
authoritiesConverter.setAuthorityPrefix("")
val converter = JwtAuthenticationConverter()
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter)
return converter
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package pt.up.fe.ni.website.backend.controller

import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import pt.up.fe.ni.website.backend.dto.auth.LoginDto
import pt.up.fe.ni.website.backend.dto.auth.TokenDto
import pt.up.fe.ni.website.backend.model.Account
import pt.up.fe.ni.website.backend.service.AuthService

@RestController
Expand All @@ -28,10 +29,29 @@ class AuthController(val authService: AuthService) {
return mapOf("access_token" to accessToken)
}

@PreAuthorize("isAuthenticated()")
@GetMapping
@PreAuthorize("hasRole('MEMBER')")
fun checkAuthentication(): Map<String, Account> {
fun checkAuthentication(): Map<String, Any> {
val authentication = SecurityContextHolder.getContext().authentication
val account = authService.getAuthenticatedAccount()
return mapOf("authenticated_user" to account)
return mapOf(
"authenticated_user" to account,
"jwt_permissions" to authentication.authorities.map { it.toString() }.toList()
)
}

@PreAuthorize("@authService.hasPermission(#permission)")
@GetMapping("/hasPermission/{permission}")
fun protectedPermission(@PathVariable permission: String): Map<String, String> {
return mapOf("message" to "You have permission to access this endpoint!")
}

@PreAuthorize("@authService.hasActivityPermission(#activityId, #permission)")
@GetMapping("/hasPermission/{activityId}/{permission}")
fun protectedPerActivityPermission(
@PathVariable activityId: Long,
@PathVariable permission: String
): Map<String, String> {
return mapOf("message" to "You have permission to access this endpoint!")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class Account(
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER)
val websites: List<@Valid CustomWebsite> = emptyList(),

@ManyToMany
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable
@OrderColumn
@JsonIgnore // TODO: Decide if we want to return roles (or IDs) by default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@ class PerActivityRole(
@ManyToOne
@JsonBackReference
lateinit var role: Role

override fun toString(): String {
val permissionNames = permissions.joinToString(separator = "-") { it.name }
return "$id:$permissionNames"
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/Role.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,12 @@ class Role(
@ManyToOne(fetch = FetchType.LAZY)
@JsonBackReference
lateinit var generation: Generation

override fun toString(): String {
val permissionsPayload = permissions.joinToString(separator = " ") { it.name }
if (associatedActivities.isEmpty()) {
return permissionsPayload
}
return "$name $permissionsPayload ${associatedActivities.joinToString(" ").trimEnd()}"
}
}
61 changes: 46 additions & 15 deletions src/main/kotlin/pt/up/fe/ni/website/backend/service/AuthService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package pt.up.fe.ni.website.backend.service

import java.time.Duration
import java.time.Instant
import java.util.Locale
import java.util.stream.Collectors
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.GrantedAuthority
Expand All @@ -16,21 +17,48 @@ import org.springframework.security.oauth2.server.resource.InvalidBearerTokenExc
import org.springframework.stereotype.Service
import pt.up.fe.ni.website.backend.config.auth.AuthConfigProperties
import pt.up.fe.ni.website.backend.model.Account
import pt.up.fe.ni.website.backend.model.Project
import pt.up.fe.ni.website.backend.repository.ActivityRepository
import pt.up.fe.ni.website.backend.service.activity.ActivityService

@Service
class AuthService(
val accountService: AccountService,
val activityService: ActivityService,
val authConfigProperties: AuthConfigProperties,
val jwtEncoder: JwtEncoder,
val jwtDecoder: JwtDecoder,
private val passwordEncoder: PasswordEncoder
private val passwordEncoder: PasswordEncoder,
val repository: ActivityRepository<Project>
) {
fun hasPermission(permission: String): Boolean {
val authentication = SecurityContextHolder.getContext().authentication
return authentication.authorities.any {
it.toString() == permission.trim().uppercase(Locale.getDefault())
}
}

fun hasActivityPermission(activityId: Long, permission: String): Boolean {
val authentication = SecurityContextHolder.getContext().authentication

return authentication.authorities.any {
val payload = it.toString().split(":")
val checkSize = payload.size == 2
val checkActivityID = checkSize && payload[0].toLong() == activityId
val checkPermission = checkSize && payload[1].split("-").any { p ->
p == permission.trim().uppercase(Locale.getDefault())
}

checkActivityID && checkPermission
}
}

fun authenticate(email: String, password: String): Account {
val account = accountService.getAccountByEmail(email)
if (!passwordEncoder.matches(password, account.password)) {
throw InvalidBearerTokenException(ErrorMessages.invalidCredentials)
}
val authentication = UsernamePasswordAuthenticationToken(email, password, getAuthorities())
val authentication = UsernamePasswordAuthenticationToken(email, password, generateAuthorities(account))
SecurityContextHolder.getContext().authentication = authentication
return account
}
Expand All @@ -44,12 +72,11 @@ class AuthService(
}

fun refreshAccessToken(refreshToken: String): String {
val jwt =
try {
jwtDecoder.decode(refreshToken)
} catch (e: Exception) {
throw InvalidBearerTokenException(ErrorMessages.invalidRefreshToken)
}
val jwt = try {
jwtDecoder.decode(refreshToken)
} catch (e: Exception) {
throw InvalidBearerTokenException(ErrorMessages.invalidRefreshToken)
}
if (jwt.expiresAt?.isBefore(Instant.now()) != false) {
throw InvalidBearerTokenException(ErrorMessages.expiredRefreshToken)
}
Expand All @@ -63,7 +90,7 @@ class AuthService(
}

private fun generateToken(account: Account, expiration: Duration, isRefresh: Boolean = false): String {
val roles = if (isRefresh) emptyList() else getAuthorities() // TODO: Pass account to getAuthorities()
val roles = if (isRefresh) emptyList() else generateAuthorities(account)
val scope = roles
.stream()
.map(GrantedAuthority::getAuthority)
Expand All @@ -75,14 +102,18 @@ class AuthService(
.issuedAt(currentInstant)
.expiresAt(currentInstant.plus(expiration))
.subject(account.email)
.claim("scope", scope)
.build()
.claim("scope", scope).build()
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).tokenValue
}

private fun getAuthorities(): List<GrantedAuthority> {
return listOf("BOARD", "MEMBER").stream() // TODO: get roles from account
.map { role -> SimpleGrantedAuthority(role) }
.collect(Collectors.toList())
private fun generateAuthorities(account: Account): List<GrantedAuthority> {
return account.roles.map {
it.toString().split(" ")
}
.flatten()
.distinct()
.map {
SimpleGrantedAuthority(it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ object ErrorMessages {

fun activityNotFound(id: Long): String = "activity not found with id $id"

fun activityNotFound(title: String): String = "activity not found with title $title"

fun accountNotFound(id: Long): String = "account not found with id $id"

fun generationNotFound(id: Long): String = "generation not found with id $id"
Expand Down
10 changes: 10 additions & 0 deletions src/main/kotlin/pt/up/fe/ni/website/backend/service/RoleService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ class RoleService(
if (validator.validateProperty(generation, "roles").isNotEmpty()) {
throw IllegalArgumentException(ErrorMessages.roleAlreadyExists(role.name, generation.schoolYear))
}

for (perActivityRoleDto in dto.associatedActivities) {
val activity = activityService.getActivityById(perActivityRoleDto.activityId!!)

for (perActivityRole in role.associatedActivities) {
perActivityRole.role = role
perActivityRole.activity = activity
}
}

role.generation = generation
roleRepository.save(role)
return role
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ abstract class AbstractActivityService<T : Activity>(
protected val accountService: AccountService,
protected val fileUploader: FileUploader
) {
fun getAll() = repository.findAll().toList()

fun getActivityById(id: Long): T = repository.findByIdOrNull(id)
?: throw NoSuchElementException(ErrorMessages.activityNotFound(id))

Expand Down
Loading

0 comments on commit 279d0e6

Please sign in to comment.