Skip to content
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
4 changes: 2 additions & 2 deletions .github/workflows/backend-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: 21
java-version: 25
distribution: 'temurin'

- name: Set up Gradle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,4 @@ sealed class AccessScope {
is Global -> null
is Site -> this.siteId
}

val isGlobal: Boolean get() = this is Global
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.github.devcavin.gatelog.backend.auth

import io.github.devcavin.gatelog.backend.common.exception.ResourceNotFoundException
import io.github.devcavin.gatelog.backend.common.exception.AccessDeniedException
import io.github.devcavin.gatelog.backend.sites.Site
import io.github.devcavin.gatelog.backend.users.User
import io.github.devcavin.gatelog.backend.visitors.Visitor
import org.springframework.security.authorization.AuthorizationDeniedException
Expand All @@ -18,7 +19,8 @@ class AuthorizationService {
fun scopeFor(user: User): AccessScope {
return when (user.role.name) {
"SUPER_ADMIN" -> AccessScope.Global
else -> AccessScope.Site(user.site.id!!)
"MANAGER", "STAFF" -> AccessScope.Site(user.site.id!!)
else -> throw AccessDeniedException("Unsupported user role")
}
}

Expand All @@ -28,12 +30,8 @@ class AuthorizationService {
*/

fun assertCovers(user: User, siteId: UUID) {
val scope = scopeFor(user)

if (!scope.covers(siteId)) {
throw AuthorizationDeniedException(
"Authorization denied for user"
)
if (!scopeFor(user).covers(siteId)) {
throw AccessDeniedException("Authorization denied")
}
}

Expand All @@ -44,12 +42,9 @@ class AuthorizationService {
*/

fun assertCanAccessVisitor(user: User, visitor: Visitor) {
val scope = scopeFor(user)

if (!scope.covers(visitor.site.id!!)) throw ResourceNotFoundException(
"Visitor",
visitor.id!!
)
if (!scopeFor(user).covers(requireSiteId(visitor.site))) {
throw ResourceNotFoundException("Visitor", requireNotNull(visitor.id))
}
}

/**
Expand All @@ -60,6 +55,8 @@ class AuthorizationService {

fun siteFilterFor(user: User): UUID? = scopeFor(user).siteIdOrNull

fun canAccessSite(user: User, siteId: UUID): Boolean = scopeFor(user).covers(siteId)

/**
* Enforces who can create a user with the given role at the given site.
* SUPER_ADMIN - unrestricted.
Expand All @@ -80,7 +77,7 @@ class AuthorizationService {
throw AccessDeniedException("Insufficient privileges to create users")

if (targetRoleName != "STAFF")
throw AccessDeniedException("Managers can only create Staff accounts")
throw AccessDeniedException("Managers can only create staff accounts")

if (targetSiteId != scope.siteId)
throw AccessDeniedException("Managers can only create users at their own site")
Expand All @@ -107,10 +104,10 @@ class AuthorizationService {
throw AccessDeniedException("User does not belong to your site")

if (target.role.name != "STAFF")
throw AccessDeniedException("Managers can only update Staff accounts")
throw AccessDeniedException("Managers can only update staff accounts")

if (newRoleName != "STAFF")
throw AccessDeniedException("Managers cannot change role beyond Staff")
throw AccessDeniedException("Managers cannot change role beyond staff")
}
}
}
Expand All @@ -129,8 +126,9 @@ class AuthorizationService {
is AccessScope.Site -> {
if (target.site.id != scope.siteId)
throw AccessDeniedException("User does not belong to your site")

if (target.role.name != "STAFF")
throw AccessDeniedException("Managers can only deactivate Staff accounts")
throw AccessDeniedException("Managers can only deactivate staff accounts")
}
}
}
Expand All @@ -147,11 +145,19 @@ class AuthorizationService {

is AccessScope.Site -> {
if (target.site.id != scope.siteId)
throw ResourceNotFoundException("User", target.id!!)
throw ResourceNotFoundException("User", requireNotNull(target.id))

if (target.role.name != "STAFF")
throw AccessDeniedException("Managers can only view Staff accounts")
throw AccessDeniedException("Managers can only view staff accounts")
}
}
}

private fun requireSiteId(user: User) : UUID {
return user.site.id ?: throw AccessDeniedException("User is not associated with a site")
}

private fun requireSiteId(site: Site) : UUID {
return site.id ?: throw AccessDeniedException("Resource is not associated with a site")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ class JwtAuthenticationFilter(
val token = extractToken(request)

if (token != null && jwtTokenProvider.validateToken(token)) {

val userId = jwtTokenProvider.getUserIdFromToken(token)
val role = jwtTokenProvider.getRoleFromToken(token)

val user = userRepository.findByIdWithRoleAndSite(userId)

if (user != null && user.isActive) {
val authorities = listOf(SimpleGrantedAuthority("ROLE_$role"))
val authorities = listOf(SimpleGrantedAuthority("ROLE_${user.role.name}"))

val authentication = UsernamePasswordAuthenticationToken(user, null, authorities)

SecurityContextHolder.getContext().authentication = authentication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,9 @@ import io.github.devcavin.gatelog.backend.common.exception.InvalidCredentialsExc
import io.github.devcavin.gatelog.backend.common.exception.InvalidStateException
import io.github.devcavin.gatelog.backend.common.exception.ResourceNotFoundException
import io.github.devcavin.gatelog.backend.sites.SiteRepository
import io.github.devcavin.gatelog.backend.users.dto.*
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import io.github.devcavin.gatelog.backend.common.exception.AccessDeniedException
import io.github.devcavin.gatelog.backend.users.dto.ChangePasswordRequest
import io.github.devcavin.gatelog.backend.users.dto.CreateUserRequest
import io.github.devcavin.gatelog.backend.users.dto.UpdateUserRequest
import io.github.devcavin.gatelog.backend.users.dto.UserResponse
import io.github.devcavin.gatelog.backend.users.dto.toResponse
import org.springframework.transaction.annotation.Transactional
import java.util.*

Expand Down Expand Up @@ -54,6 +49,7 @@ class UserService(
is AccessScope.Global -> userRepository
.findAllWithRole()
.map { it.toResponse() }

is AccessScope.Site -> userRepository
.findAllBySiteIdWithRole(scope.siteId)
.filter { it.role.name == "STAFF" }
Expand All @@ -65,15 +61,8 @@ class UserService(
fun getById(requestedBy: User, userId: UUID): UserResponse {
val target = userRepository.findById(userId)
.orElseThrow { ResourceNotFoundException("User", userId) }
when (val scope = authorizationService.scopeFor(requestedBy)) {
is AccessScope.Global -> Unit // SUPER_ADMIN sees any user
is AccessScope.Site -> {
if (target.site.id != scope.siteId)
throw ResourceNotFoundException("User", userId)
if (target.role.name != "STAFF")
throw AccessDeniedException("Managers can only view Staff accounts")
}
}

authorizationService.assertCanViewUser(requestedBy, target)

return target.toResponse()
}
Expand All @@ -90,9 +79,7 @@ class UserService(
authorizationService.assertCanViewUser(requestedBy, target)
authorizationService.assertCanUpdateUser(requestedBy, target, request.roleName)

if (request.email != target.email &&
userRepository.existsByEmail(request.email)
) {
if (request.email != target.email && userRepository.existsByEmail(request.email)) {
throw ConflictException("Email already in use: ${request.email}")
}

Expand All @@ -115,6 +102,7 @@ class UserService(
.orElseThrow { ResourceNotFoundException("User", userId) }

authorizationService.assertCanViewUser(requestedBy, target)

authorizationService.assertCanDeactivateUser(requestedBy, target)

target.isActive = false
Expand All @@ -125,7 +113,9 @@ class UserService(
fun activate(requestedBy: User, userId: UUID): UserResponse {
val target = userRepository.findById(userId)
.orElseThrow { ResourceNotFoundException("User", userId) }

authorizationService.assertCanViewUser(requestedBy, target)

target.isActive = true
return userRepository.save(target).toResponse()
}
Expand All @@ -135,12 +125,10 @@ class UserService(
requestedBy: User,
request: ChangePasswordRequest
): UserResponse {
if (!passwordEncoder.matches(
request.currentPassword, requestedBy.passwordHash
)
) {
if (!passwordEncoder.matches(request.currentPassword, requestedBy.passwordHash)) {
throw InvalidCredentialsException()
}

requestedBy.passwordHash = passwordEncoder.encode(request.newPassword)
return userRepository.save(requestedBy).toResponse()
}
Expand Down
Loading