From 1bebaff86c72b4439b8e4fdeedfcd2bcd1eb5b68 Mon Sep 17 00:00:00 2001 From: Cavin Date: Tue, 18 Aug 2026 18:01:28 +0300 Subject: [PATCH] refactor: [authorization and access scope], modify backend ci --- .github/workflows/backend-ci.yaml | 4 +- .../gatelog/backend/auth/AccessScope.kt | 2 - .../backend/auth/AuthorizationService.kt | 44 +++++++++++-------- .../backend/auth/JwtAuthenticationFilter.kt | 5 ++- .../gatelog/backend/users/UserService.kt | 32 +++++--------- 5 files changed, 40 insertions(+), 47 deletions(-) diff --git a/.github/workflows/backend-ci.yaml b/.github/workflows/backend-ci.yaml index 15ec513..f01c73a 100644 --- a/.github/workflows/backend-ci.yaml +++ b/.github/workflows/backend-ci.yaml @@ -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 diff --git a/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/AccessScope.kt b/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/AccessScope.kt index 23a2bdd..a715293 100644 --- a/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/AccessScope.kt +++ b/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/AccessScope.kt @@ -32,6 +32,4 @@ sealed class AccessScope { is Global -> null is Site -> this.siteId } - - val isGlobal: Boolean get() = this is Global } \ No newline at end of file diff --git a/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/AuthorizationService.kt b/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/AuthorizationService.kt index 34bca19..a35374d 100644 --- a/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/AuthorizationService.kt +++ b/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/AuthorizationService.kt @@ -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 @@ -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") } } @@ -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") } } @@ -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)) + } } /** @@ -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. @@ -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") @@ -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") } } } @@ -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") } } } @@ -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") + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/JwtAuthenticationFilter.kt b/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/JwtAuthenticationFilter.kt index e345478..2a64467 100644 --- a/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/JwtAuthenticationFilter.kt +++ b/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/auth/JwtAuthenticationFilter.kt @@ -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 diff --git a/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/users/UserService.kt b/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/users/UserService.kt index 305b327..ae3cecb 100644 --- a/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/users/UserService.kt +++ b/backend/src/main/kotlin/io/github/devcavin/gatelog/backend/users/UserService.kt @@ -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.* @@ -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" } @@ -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() } @@ -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}") } @@ -115,6 +102,7 @@ class UserService( .orElseThrow { ResourceNotFoundException("User", userId) } authorizationService.assertCanViewUser(requestedBy, target) + authorizationService.assertCanDeactivateUser(requestedBy, target) target.isActive = false @@ -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() } @@ -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() }