Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create annotation to validate role and serialize roles #147

Merged
merged 21 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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("")
rubuy-74 marked this conversation as resolved.
Show resolved Hide resolved
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
rubuy-74 marked this conversation as resolved.
Show resolved Hide resolved
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 {
rubuy-74 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading