Skip to content

Commit eea4af5

Browse files
authored
Merge pull request #15 from nenadjakic/feature/security-and-permissions
Added @PreAuthorize and @PostFilter annotations to controllers and se…
2 parents 09286ad + 7011176 commit eea4af5

12 files changed

+89
-20
lines changed

src/main/kotlin/com/github/nenadjakic/eav/configuration/MainConfiguration.kt

-3
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,10 @@ import io.swagger.v3.oas.models.security.SecurityRequirement
88
import io.swagger.v3.oas.models.security.SecurityScheme
99
import org.modelmapper.ModelMapper
1010
import org.springframework.beans.factory.annotation.Value
11-
import org.springframework.beans.factory.support.BeanDefinitionRegistry
12-
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
1311
import org.springframework.cache.CacheManager
1412
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
1513
import org.springframework.context.annotation.Bean
1614
import org.springframework.context.annotation.Configuration
17-
import org.springframework.context.annotation.DependsOn
1815
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
1916
import org.springframework.scheduling.annotation.EnableAsync
2017
import org.springframework.security.authentication.AuthenticationManager

src/main/kotlin/com/github/nenadjakic/eav/configuration/SecurityConfiguration.kt

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package com.github.nenadjakic.eav.configuration
22

33
import com.github.nenadjakic.eav.security.filter.JwtAuthenticationFilter
4-
import com.github.nenadjakic.eav.service.security.AttributePermissionService
54
import org.springframework.beans.factory.config.BeanDefinition
65
import org.springframework.context.annotation.Bean
76
import org.springframework.context.annotation.Configuration
8-
import org.springframework.context.annotation.DependsOn
97
import org.springframework.context.annotation.Role
108
import org.springframework.security.authentication.AuthenticationProvider
119
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
@@ -28,7 +26,7 @@ open class SecurityConfiguration(
2826
@Bean
2927
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
3028
open fun grantedAuthorityDefaults(): GrantedAuthorityDefaults {
31-
return GrantedAuthorityDefaults("");
29+
return GrantedAuthorityDefaults("")
3230
}
3331
}
3432

src/main/kotlin/com/github/nenadjakic/eav/controller/AttributeController.kt

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.swagger.v3.oas.annotations.tags.Tag
1414
import org.modelmapper.ModelMapper
1515
import org.springframework.data.domain.Page
1616
import org.springframework.http.ResponseEntity
17+
import org.springframework.security.access.prepost.PreAuthorize
1718
import org.springframework.web.bind.annotation.RequestMapping
1819
import org.springframework.web.bind.annotation.RestController
1920
import org.springframework.web.servlet.support.ServletUriComponentsBuilder
@@ -83,6 +84,7 @@ open class AttributeController(
8384
ApiResponse(responseCode = "204", description = "Entity deleted successfully")
8485
]
8586
)
87+
@PreAuthorize("hasRole('ADMINISTRATOR')")
8688
open override fun deleteById(id: Long): ResponseEntity<Void> {
8789
attributeService.deleteById(id)
8890
return ResponseEntity.noContent().build()
@@ -99,6 +101,7 @@ open class AttributeController(
99101
ApiResponse(responseCode = "400", description = "Invalid request data.")
100102
]
101103
)
104+
@PreAuthorize("hasRole('ADMINISTRATOR')")
102105
open override fun update(model: AttributeUpdateRequest): ResponseEntity<Void> {
103106
val entity = modelMapper.map(model, Attribute::class.java)
104107
attributeService.update(entity)
@@ -116,6 +119,7 @@ open class AttributeController(
116119
ApiResponse(responseCode = "400", description = "Invalid request data.")
117120
]
118121
)
122+
@PreAuthorize("hasRole('ADMINISTRATOR')")
119123
open override fun create(model: AttributeAddRequest): ResponseEntity<Void> {
120124
val entity = modelMapper.map(model, Attribute::class.java)
121125
val createdEntity = attributeService.create(entity)

src/main/kotlin/com/github/nenadjakic/eav/controller/AttributeValueController.kt

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag
1212
import jakarta.validation.Valid
1313
import org.modelmapper.ModelMapper
1414
import org.springframework.http.ResponseEntity
15+
import org.springframework.security.access.prepost.PreAuthorize
1516
import org.springframework.validation.annotation.Validated
1617
import org.springframework.web.bind.annotation.DeleteMapping
1718
import org.springframework.web.bind.annotation.GetMapping
@@ -81,6 +82,7 @@ open class AttributeValueController(
8182
]
8283
)
8384
@PostMapping
85+
@PreAuthorize("@attributeSecurityService.canCreate(#attributeValueAddRequest.attributeId)")
8486
open fun create(@RequestBody @Valid attributeValueAddRequest: AttributeValueAddRequest): ResponseEntity<Void> {
8587
val attributeValue = modelMapper.map(attributeValueAddRequest, AttributeValue::class.java)
8688
val createdAttributeValue = attributeValueService.create(attributeValue)
@@ -106,6 +108,7 @@ open class AttributeValueController(
106108
]
107109
)
108110
@PutMapping
111+
@PreAuthorize("@attributeSecurityService.canUpdate(#attributeValueAddRequest.attributeId)")
109112
open fun update(@RequestBody @Valid attributeValueAddRequest: AttributeValueAddRequest): ResponseEntity<Void> {
110113
val attributeValue = modelMapper.map(attributeValueAddRequest, AttributeValue::class.java)
111114
attributeValueService.update(attributeValue)
@@ -123,6 +126,7 @@ open class AttributeValueController(
123126
]
124127
)
125128
@DeleteMapping
129+
@PreAuthorize("@attributeSecurityService.canDelete(#attributeId)")
126130
open fun deleteById(@PathVariable entityId: Long, @PathVariable attributeId: Long): ResponseEntity<Void> {
127131
attributeValueService.deleteById(entityId, attributeId)
128132
return ResponseEntity.noContent().build()

src/main/kotlin/com/github/nenadjakic/eav/controller/EavReadController.kt

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.github.nenadjakic.eav.controller
33
import org.springframework.data.domain.Page
44
import org.springframework.http.MediaType
55
import org.springframework.http.ResponseEntity
6-
import org.springframework.security.access.prepost.PostAuthorize
76
import org.springframework.security.access.prepost.PreAuthorize
87
import org.springframework.web.bind.annotation.GetMapping
98
import org.springframework.web.bind.annotation.PathVariable

src/main/kotlin/com/github/nenadjakic/eav/controller/EavWriteController.kt

+4-9
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,8 @@ package com.github.nenadjakic.eav.controller
33
import jakarta.validation.Valid
44
import org.springframework.http.MediaType
55
import org.springframework.http.ResponseEntity
6-
import org.springframework.security.access.prepost.PreAuthorize
76
import org.springframework.validation.annotation.Validated
8-
import org.springframework.web.bind.annotation.DeleteMapping
9-
import org.springframework.web.bind.annotation.PathVariable
10-
import org.springframework.web.bind.annotation.PostMapping
11-
import org.springframework.web.bind.annotation.PutMapping
12-
import org.springframework.web.bind.annotation.RequestBody
7+
import org.springframework.web.bind.annotation.*
138

149
/**
1510
* Interface for writing operations in the EAV (Entity-Attribute-Value) system.
@@ -21,16 +16,16 @@ import org.springframework.web.bind.annotation.RequestBody
2116
@Validated
2217
interface EavWriteController<CR, UR> {
2318

24-
@PreAuthorize("hasRole('WRITER')")
19+
//@PreAuthorize("hasRole('ADMINISTRATOR')")
2520
@PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE],
2621
produces = [MediaType.APPLICATION_JSON_VALUE])
2722
fun create(@Valid @RequestBody model:CR): ResponseEntity<Void>
2823

29-
@PreAuthorize("hasRole('WRITER')")
24+
//@PreAuthorize("hasRole('ADMINISTRATOR')")
3025
@PutMapping(consumes = [MediaType.APPLICATION_JSON_VALUE])
3126
fun update(@Valid @RequestBody model:UR): ResponseEntity<Void>
3227

33-
@PreAuthorize("hasRole('WRITER')")
28+
//@PreAuthorize("hasRole('ADMINISTRATOR')")
3429
@DeleteMapping("/{id}")
3530
fun deleteById(@PathVariable id: Long): ResponseEntity<Void>
3631
}

src/main/kotlin/com/github/nenadjakic/eav/controller/EntityController.kt

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.swagger.v3.oas.annotations.tags.Tag
1414
import org.modelmapper.ModelMapper
1515
import org.springframework.data.domain.Page
1616
import org.springframework.http.ResponseEntity
17+
import org.springframework.security.access.prepost.PreAuthorize
1718
import org.springframework.web.bind.annotation.RequestMapping
1819
import org.springframework.web.bind.annotation.RestController
1920
import org.springframework.web.servlet.support.ServletUriComponentsBuilder
@@ -91,6 +92,7 @@ class EntityController(
9192
ApiResponse(responseCode = "400", description = "Invalid request data.")
9293
]
9394
)
95+
@PreAuthorize("hasRole('ADMINISTRATOR')")
9496
open override fun create(model: EntityAddRequest): ResponseEntity<Void> {
9597
val entity = modelMapper.map(model, Entity::class.java)
9698
val createdEntity = entityService.create(entity)
@@ -114,6 +116,7 @@ class EntityController(
114116
ApiResponse(responseCode = "400", description = "Invalid request data.")
115117
]
116118
)
119+
@PreAuthorize("hasRole('ADMINISTRATOR')")
117120
open override fun update(model: EntityUpdateRequest): ResponseEntity<Void> {
118121
val entity = modelMapper.map(model, Entity::class.java)
119122
entityService.update(entity)
@@ -130,6 +133,7 @@ class EntityController(
130133
ApiResponse(responseCode = "204", description = "Entity deleted successfully")
131134
]
132135
)
136+
@PreAuthorize("hasRole('ADMINISTRATOR')")
133137
open override fun deleteById(id: Long): ResponseEntity<Void> {
134138
entityService.deleteById(id)
135139
return ResponseEntity.noContent().build()

src/main/kotlin/com/github/nenadjakic/eav/controller/EntityTypeController.kt

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.swagger.v3.oas.annotations.tags.Tag
1414
import org.modelmapper.ModelMapper
1515
import org.springframework.data.domain.Page
1616
import org.springframework.http.ResponseEntity
17+
import org.springframework.security.access.prepost.PreAuthorize
1718
import org.springframework.web.bind.annotation.RequestMapping
1819
import org.springframework.web.bind.annotation.RestController
1920
import org.springframework.web.servlet.support.ServletUriComponentsBuilder
@@ -84,6 +85,7 @@ open class EntityTypeController(
8485
ApiResponse(responseCode = "400", description = "Invalid request data.")
8586
]
8687
)
88+
@PreAuthorize("hasRole('ADMINISTRATOR')")
8789
open override fun create(model: EntityTypeAddRequest): ResponseEntity<Void> {
8890
val entity = modelMapper.map(model, EntityType::class.java)
8991
val createdEntity = entityTypeService.create(entity)
@@ -107,6 +109,7 @@ open class EntityTypeController(
107109
ApiResponse(responseCode = "400", description = "Invalid request data.")
108110
]
109111
)
112+
@PreAuthorize("hasRole('ADMINISTRATOR')")
110113
open override fun update(model: EntityTypeUpdateRequest): ResponseEntity<Void> {
111114
val entityType = modelMapper.map(model, EntityType::class.java)
112115
entityTypeService.update(entityType)
@@ -123,6 +126,7 @@ open class EntityTypeController(
123126
ApiResponse(responseCode = "204", description = "Entity type deleted successfully")
124127
]
125128
)
129+
@PreAuthorize("hasRole('ADMINISTRATOR')")
126130
open override fun deleteById(id: Long): ResponseEntity<Void> {
127131
entityTypeService.deleteById(id)
128132
return ResponseEntity.noContent().build()

src/main/kotlin/com/github/nenadjakic/eav/dto/AttributeAddRequest.kt

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.github.nenadjakic.eav.dto
22

33
import jakarta.validation.Valid
44
import jakarta.validation.constraints.NotNull
5-
import jakarta.xml.bind.annotation.XmlElement
65

76
class AttributeAddRequest {
87
@NotNull

src/main/kotlin/com/github/nenadjakic/eav/security/AttributeSecurityService.kt

+66-1
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@ import org.springframework.security.core.context.SecurityContextHolder
99
import org.springframework.security.core.userdetails.UserDetails
1010
import org.springframework.stereotype.Service
1111

12+
/**
13+
* Service class for attribute-based security operations.
14+
*
15+
* @param attributePermissionService The service for managing attribute permissions
16+
*/
1217
@Service
1318
class AttributeSecurityService(
1419
private val attributePermissionService: AttributePermissionService
1520
) {
21+
@Deprecated(message = "Test purpose only. Use canRead(attributeId: Long).")
1622
fun canRead(root: MethodSecurityExpressionOperations, attributeId: Long): Boolean {
1723
val user = (root.authentication.principal as SecurityUser?) ?: throw Exception("User does not exists.")
1824
val roles = getRoles(user)
@@ -27,16 +33,48 @@ class AttributeSecurityService(
2733
return false
2834
}
2935

36+
/**
37+
* Checks if the user has permission to read an attribute-value for given attribute.
38+
*
39+
* @param attributeId The ID of the attribute to check for read permission
40+
* @return true if the user has permission to read the attribute-value, otherwise false
41+
*/
3042
fun canRead(attributeId: Long): Boolean = hasPermission(attributeId, Action.READ)
3143

44+
/**
45+
* Checks if the user has permission to create an attribute-value for the given attribute.
46+
*
47+
* @param attributeId The ID of the attribute to check for creation permission
48+
* @return true if the user has permission to create the attribute-value, otherwise false
49+
*/
3250
fun canCreate(attributeId: Long): Boolean = hasPermission(attributeId, Action.CREATE)
3351

52+
/**
53+
* Checks if the user has permission to update an attribute-value for the given attribute.
54+
*
55+
* @param attributeId The ID of the attribute to check for update permission
56+
* @return true if the user has permission to update the attribute-value, otherwise false
57+
*/
3458
fun canUpdate(attributeId: Long): Boolean = hasPermission(attributeId, Action.UPDATE)
3559

60+
61+
/**
62+
* Checks if the user has permission to delete an attribute-value for the given attribute.
63+
*
64+
* @param attributeId The ID of the attribute to check for delete permission
65+
* @return true if the user has permission to delete the attribute-value, otherwise false
66+
*/
3667
fun canDelete(attributeId: Long): Boolean = hasPermission(attributeId, Action.DELETE)
3768

69+
/**
70+
* Checks if the user has permission to perform a specific action on an attribute-value for the given attribute.
71+
*
72+
* @param attributeId The ID of the attribute to check for permission
73+
* @param action The action to check permission for (e.g., READ, CREATE, UPDATE, DELETE)
74+
* @return true if the user has permission to perform the action, otherwise false
75+
*/
3876
private fun hasPermission(attributeId: Long, action: Action): Boolean {
39-
var hasPermission = false;
77+
var hasPermission = false
4078
for (role in getRoles(getUser() ?: throw Exception("User does not exists."))) {
4179
hasPermission = hasPermission(role, attributeId, action)
4280
if (hasPermission) {
@@ -46,12 +84,33 @@ class AttributeSecurityService(
4684
return hasPermission
4785
}
4886

87+
/**
88+
* Checks if a user with the specified role has permission to perform a specific action
89+
* on an attribute-value for the given attribute.
90+
*
91+
* @param role The role of the user to check for permission
92+
* @param attributeId The ID of the attribute to check for permission
93+
* @param action The action to check permission for (e.g., READ, CREATE, UPDATE, DELETE)
94+
* @return true if the user with the specified role has permission to perform the action, otherwise false
95+
*/
4996
private fun hasPermission(role: String, attributeId: Long, action: Action): Boolean = hasPermission(attributePermissionService.findByRoleNameAndAttributeId(role, attributeId), action)
5097

98+
/**
99+
* Checks if the attribute permission allows a specific action to be performed.
100+
*
101+
* @param attributePermission The attribute permission object to check
102+
* @param action The action to check permission for (e.g., READ, CREATE, UPDATE, DELETE)
103+
* @return true if the attribute permission allows the action, otherwise false
104+
*/
51105
private fun hasPermission(attributePermission: AttributePermission?, action: Action): Boolean {
52106
return attributePermission != null && attributePermission.actions.contains(action)
53107
}
54108

109+
/**
110+
* Retrieves the current authenticated user from the security context.
111+
*
112+
* @return The authenticated user as a SecurityUser object, or null if not authenticated
113+
*/
55114
private fun getUser(): SecurityUser? {
56115
return if (SecurityContextHolder.getContext().authentication == null) {
57116
null
@@ -60,5 +119,11 @@ class AttributeSecurityService(
60119
}
61120
}
62121

122+
/**
123+
* Retrieves the roles associated with the provided UserDetails.
124+
*
125+
* @param user The UserDetails object for which roles are to be retrieved
126+
* @return A list of roles (as strings) associated with the user
127+
*/
63128
private fun getRoles(user: UserDetails): List<out String> = user.authorities.map { it.authority }
64129
}

src/test/kotlin/com/github/nenadjakic/eav/controller/EntityControllerTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class EntityControllerTest {
3838
val securityUser = SecurityUser()
3939
securityUser.username = "test"
4040
securityUser.addAuthority(SimpleGrantedAuthority("READER"))
41-
securityUser.addAuthority(SimpleGrantedAuthority("WRITER"))
41+
securityUser.addAuthority(SimpleGrantedAuthority("ADMINISTRATOR"))
4242
val token = jwtService.createToken(securityUser)
4343

4444
headers = HttpHeaders()

src/test/kotlin/com/github/nenadjakic/eav/controller/EntityTypeControllerTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class EntityTypeControllerTest {
3939
val securityUser = SecurityUser()
4040
securityUser.username = "test"
4141
securityUser.addAuthority(SimpleGrantedAuthority("READER"))
42-
securityUser.addAuthority(SimpleGrantedAuthority("WRITER"))
42+
securityUser.addAuthority(SimpleGrantedAuthority("ADMINISTRATOR"))
4343
val token = jwtService.createToken(securityUser)
4444

4545
headers = HttpHeaders()

0 commit comments

Comments
 (0)