From 77415c146dd17d34b3e3a9d3c165d7da7d148355 Mon Sep 17 00:00:00 2001 From: Michael Clausen Date: Sat, 17 Sep 2022 00:57:46 +0200 Subject: [PATCH 1/9] Added support for JWT token authentication --- build.gradle.kts | 2 + src/main/kotlin/ch/hevs/cloudio/cloud/main.kt | 20 ++-- .../restapi/account/AccountController.kt | 92 +++++++++++-------- .../restapi/token/AccessTokenController.kt | 47 ++++++++++ .../restapi/token/AccessTokenRequestEntity.kt | 6 ++ .../cloud/security/AccessTokenFilter.kt | 34 +++++++ .../cloud/security/AccessTokenManager.kt | 56 +++++++++++ .../security/CloudioUserDetailsService.kt | 10 +- 8 files changed, 222 insertions(+), 45 deletions(-) create mode 100644 src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenController.kt create mode 100644 src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenRequestEntity.kt create mode 100644 src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt create mode 100644 src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt diff --git a/build.gradle.kts b/build.gradle.kts index dfea082..091255f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -63,6 +63,8 @@ dependencies { implementation("org.influxdb:influxdb-java") implementation("org.bouncycastle:bcpkix-jdk15on:1.70") + implementation("io.jsonwebtoken:jjwt:0.9.1") + testImplementation("org.jetbrains.kotlin:kotlin-test:1.6.21") testImplementation("org.springframework.boot:spring-boot-starter-test") diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/main.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/main.kt index e09654a..0817d62 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/main.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/main.kt @@ -2,10 +2,7 @@ package ch.hevs.cloudio.cloud import ch.hevs.cloudio.cloud.cors.CorsRepository import ch.hevs.cloudio.cloud.internalservice.certificatemanager.CertificateManagerService -import ch.hevs.cloudio.cloud.security.Authority -import ch.hevs.cloudio.cloud.security.CloudioCorsConfigurationSource -import ch.hevs.cloudio.cloud.security.CloudioUserDetails -import ch.hevs.cloudio.cloud.security.CloudioUserDetailsService +import ch.hevs.cloudio.cloud.security.* import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.rabbitmq.client.DefaultSaslConfig @@ -34,14 +31,17 @@ import org.springframework.security.config.annotation.authentication.builders.Au import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.web.cors.CorsConfigurationSource import java.net.InetAddress import java.util.* import javax.net.ssl.KeyManagerFactory import javax.net.ssl.TrustManagerFactory + @SpringBootApplication @ConfigurationPropertiesScan @EnableGlobalMethodSecurity(prePostEnabled = true) @@ -59,6 +59,11 @@ import javax.net.ssl.TrustManagerFactory type = SecuritySchemeType.HTTP, scheme = "basic" ) +@SecurityScheme( + name = "tokenAuth", + type = SecuritySchemeType.HTTP, + scheme = "bearer" +) class CloudioApplication { @Bean fun passwordEncoder() = BCryptPasswordEncoder() @@ -157,7 +162,7 @@ class CloudioApplication { fun corsConfigurationSource(repo: CorsRepository): CorsConfigurationSource = CloudioCorsConfigurationSource(repo) @Bean - fun webSecurityConfigurerAdapter(cloudioUserDetailsService: CloudioUserDetailsService) = object : WebSecurityConfigurerAdapter() { + fun webSecurityConfigurerAdapter(cloudioUserDetailsService: CloudioUserDetailsService, accessTokenFilter: AccessTokenFilter) = object : WebSecurityConfigurerAdapter() { override fun configure(auth: AuthenticationManagerBuilder) { auth.userDetailsService(cloudioUserDetailsService) } @@ -167,11 +172,12 @@ class CloudioApplication { .csrf().disable() .authorizeRequests().antMatchers( "/v3/api-docs", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", - "/api/v1/provision/*", "/messageformat/**" + "/api/v1/provision/*", "/messageformat/**", "/api/v1/auth/login" ).permitAll() .anyRequest().hasAuthority(Authority.HTTP_ACCESS.name) .and().httpBasic() - .and().sessionManagement().disable() + .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + http.addFilterBefore(accessTokenFilter, UsernamePasswordAuthenticationFilter::class.java) } } diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/account/AccountController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/account/AccountController.kt index d88a60a..e25a86d 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/account/AccountController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/account/AccountController.kt @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus @@ -27,7 +28,10 @@ import org.springframework.web.bind.annotation.* @Profile("rest-api") @Tag(name = "Account Management", description = "Allows users to access and modify their account information.") @RequestMapping("/api/v1/account") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class AccountController( private val userRepository: UserRepository, private val permissionManager: CloudioPermissionManager, @@ -37,11 +41,13 @@ class AccountController( @ResponseStatus(HttpStatus.OK) @Transactional(readOnly = true) @Operation(summary = "Get information about the currently authenticated user.") - @ApiResponses(value = [ - ApiResponse(description = "Currently authenticated user's account.", responseCode = "200", content = [Content(schema = Schema(implementation = AccountEntity::class))]), - ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), - ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) - ]) + @ApiResponses( + value = [ + ApiResponse(description = "Currently authenticated user's account.", responseCode = "200", content = [Content(schema = Schema(implementation = AccountEntity::class))]), + ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) + ] + ) fun getMyAccount( @Parameter(hidden = true) authentication: Authentication ) = userRepository.findById(authentication.userDetails().id).orElseThrow { @@ -59,12 +65,14 @@ class AccountController( @PutMapping("/password") @ResponseStatus(HttpStatus.NO_CONTENT) @Operation(summary = "Change the currently authenticated user's password.") - @ApiResponses(value = [ - ApiResponse(description = "Password was changed.", responseCode = "204", content = [Content()]), - ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), - ApiResponse(description = "Existing password is incorrect.", responseCode = "400", content = [Content()]), - ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) - ]) + @ApiResponses( + value = [ + ApiResponse(description = "Password was changed.", responseCode = "204", content = [Content()]), + ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), + ApiResponse(description = "Existing password is incorrect.", responseCode = "400", content = [Content()]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) + ] + ) fun putMyPassword( @RequestParam @Parameter(description = "Existing password.") existingPassword: String, @RequestParam @Parameter(description = "New password.") newPassword: String, @@ -84,11 +92,13 @@ class AccountController( @GetMapping("/email", produces = ["text/plain"]) @ResponseStatus(HttpStatus.OK) @Operation(summary = "Returns the currently authenticated user's email address.") - @ApiResponses(value = [ - ApiResponse(description = "Users email address.", responseCode = "200", content = [Content(schema = Schema(type = "string", example = "john.doe@theinternet.org"))]), - ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), - ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) - ]) + @ApiResponses( + value = [ + ApiResponse(description = "Users email address.", responseCode = "200", content = [Content(schema = Schema(type = "string", example = "john.doe@theinternet.org"))]), + ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) + ] + ) fun getMyEmailAddress( @Parameter(hidden = true) authentication: Authentication ) = userRepository.findById(authentication.userDetails().id).orElseThrow { @@ -98,12 +108,14 @@ class AccountController( @PutMapping("/email") @ResponseStatus(HttpStatus.NO_CONTENT) @Operation(summary = "Changes the currently authenticated user's email address.") - @ApiResponses(value = [ - ApiResponse(description = "Email address was changed.", responseCode = "204", content = [Content()]), - ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), - ApiResponse(description = "Invalid Email address.", responseCode = "400", content = [Content()]), - ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) - ]) + @ApiResponses( + value = [ + ApiResponse(description = "Email address was changed.", responseCode = "204", content = [Content()]), + ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), + ApiResponse(description = "Invalid Email address.", responseCode = "400", content = [Content()]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) + ] + ) fun putMyEmailAddress( @Parameter(hidden = true) authentication: Authentication, @RequestParam @Parameter(description = "Email address to assign to user.", example = "john.doe@theinternet.org") email: String @@ -123,11 +135,13 @@ class AccountController( @GetMapping("/metaData", produces = [MediaType.APPLICATION_JSON_VALUE]) @ResponseStatus(HttpStatus.OK) @Operation(summary = "Returns the currently authenticated user's meta data.") - @ApiResponses(value = [ - ApiResponse(description = "Users meta data.", responseCode = "200", content = [Content(schema = Schema(type = "object", example = "{\"location\": \"Sion\", \"position\": \"Manager\"}"))]), - ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), - ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) - ]) + @ApiResponses( + value = [ + ApiResponse(description = "Users meta data.", responseCode = "200", content = [Content(schema = Schema(type = "object", example = "{\"location\": \"Sion\", \"position\": \"Manager\"}"))]), + ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) + ] + ) fun getMyMetaData( @Parameter(hidden = true) authentication: Authentication ) = userRepository.findById(authentication.userDetails().id).orElseThrow { @@ -137,11 +151,13 @@ class AccountController( @PutMapping("/metaData", consumes = [MediaType.APPLICATION_JSON_VALUE]) @ResponseStatus(HttpStatus.NO_CONTENT) @Operation(summary = "Changes the currently authenticated user's meta data.") - @ApiResponses(value = [ - ApiResponse(description = "Meta data was modified.", responseCode = "204", content = [Content()]), - ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), - ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) - ]) + @ApiResponses( + value = [ + ApiResponse(description = "Meta data was modified.", responseCode = "204", content = [Content()]), + ApiResponse(description = "User not found.", responseCode = "404", content = [Content()]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) + ] + ) fun putMyMetaData( @Parameter(hidden = true) authentication: Authentication, @RequestBody @Parameter(description = "User's metadata.", schema = Schema(type = "object", example = "{\"location\": \"Sion\", \"position\": \"Manager\"}")) @@ -159,10 +175,12 @@ class AccountController( @ResponseStatus(HttpStatus.OK) @Transactional(readOnly = true) @Operation(summary = "Get the all endpoint permissions.") - @ApiResponses(value = [ - ApiResponse(description = "Users endpoint permissions.", responseCode = "200", content = [Content(array = ArraySchema(schema = Schema(implementation = EndpointPermissionEntity::class)))]), - ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) - ]) + @ApiResponses( + value = [ + ApiResponse(description = "Users endpoint permissions.", responseCode = "200", content = [Content(array = ArraySchema(schema = Schema(implementation = EndpointPermissionEntity::class)))]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) + ] + ) fun getMyEndpointPermissions( @Parameter(hidden = true) authentication: Authentication ) = permissionManager.resolvePermissions(authentication.userDetails()).map { diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenController.kt new file mode 100644 index 0000000..b190e78 --- /dev/null +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenController.kt @@ -0,0 +1,47 @@ +package ch.hevs.cloudio.cloud.restapi.token + +import ch.hevs.cloudio.cloud.restapi.CloudioHttpExceptions +import ch.hevs.cloudio.cloud.security.AccessTokenManager +import ch.hevs.cloudio.cloud.security.Authority +import ch.hevs.cloudio.cloud.security.CloudioUserDetails +import ch.hevs.cloudio.cloud.security.CloudioUserDetailsService +import io.swagger.v3.oas.annotations.Parameter +import org.apache.juli.logging.LogFactory +import org.springframework.http.MediaType +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.crypto.password.PasswordEncoder +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 + +@RestController +@RequestMapping("/api/v1/auth") +class AccessTokenController( + private val accessTokenManager: AccessTokenManager, + private val userDetailsService: CloudioUserDetailsService, + private val passwordEncoder: PasswordEncoder +) { + private val log = LogFactory.getLog(AccessTokenController::class.java) + + @PostMapping("/login", produces = [MediaType.TEXT_PLAIN_VALUE]) + fun login( + @RequestBody @Parameter() request: AccessTokenRequestEntity + ): String = userDetailsService.loadUserByUsername(request.username).let { + when { + !it.isAccountNonExpired -> { + log.info("Token refused for user \"${request.username}\" using password authentication - User is banned.") + throw CloudioHttpExceptions.Forbidden("User is banned.") + } + !it.authorities.contains(SimpleGrantedAuthority(Authority.HTTP_ACCESS.toString())) -> { + log.info("Token refused for user \"${request.username}\" using password authentication - User is missing HTTP_ACCESS role.") + throw CloudioHttpExceptions.Forbidden("No HTTP access.") + } + !passwordEncoder.matches(request.password, it.password) -> { + log.info("Token refused for user \"${request.username}\" using password authentication - Password is incorrect.") + throw CloudioHttpExceptions.Forbidden("Wrong password.") + } + else -> accessTokenManager.generate(it as CloudioUserDetails) + } + } +} diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenRequestEntity.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenRequestEntity.kt new file mode 100644 index 0000000..1ebc1a3 --- /dev/null +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenRequestEntity.kt @@ -0,0 +1,6 @@ +package ch.hevs.cloudio.cloud.restapi.token + +data class AccessTokenRequestEntity( + val username: String, + val password: String +) diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt new file mode 100644 index 0000000..ac68aa3 --- /dev/null +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt @@ -0,0 +1,34 @@ +package ch.hevs.cloudio.cloud.security + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import javax.servlet.FilterChain +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +@Component +class AccessTokenFilter( + private val accessTokenManager: AccessTokenManager, + private val userDetailsService: CloudioUserDetailsService +): OncePerRequestFilter() { + override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { + request.getHeader("Authorization").let { header -> + if (header != null && header.isNotEmpty() && header.startsWith("Bearer")) { + header.split(" ").getOrNull(1)?.trim()?.let { token -> + accessTokenManager.validate(token)?.let { username -> + userDetailsService.loadUserByID(username).let { userDetails -> + SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities).apply { + details = WebAuthenticationDetailsSource().buildDetails(request) + } + } + } + } + } + } + + filterChain.doFilter(request, response) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt new file mode 100644 index 0000000..b68ffef --- /dev/null +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt @@ -0,0 +1,56 @@ +package ch.hevs.cloudio.cloud.security + +import io.jsonwebtoken.* +import org.apache.juli.logging.LogFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import java.util.* +import javax.annotation.PostConstruct + +@Service +class AccessTokenManager { + private val log = LogFactory.getLog(AccessTokenManager::class.java) + + @Value("\${cloudio.jwt.secret:#{null}}") + private var secretKey: String? = null + + @Value("\${cloudio.jwt.expirationDuration:#{24 * 60 * 60 * 1000}}") + private var tokenExpirationDuration: Long = 24 * 60 * 60 * 1000 + + @PostConstruct + private fun generateSecretKeyIfMissing() { + if (secretKey == null) { + log.error("JWT token secret key (cloudio.jwt.secret) is missing, a key will be generated. Tokens will only be valid for this node and not across restarts.") + secretKey = ByteArray(64).let { + Random(System.currentTimeMillis()).nextBytes(it) + Base64.getEncoder().encodeToString(it) + } + } + } + + fun generate(user: CloudioUserDetails): String = Jwts.builder() + .setSubject(user.id.toString()) + .setIssuer("cloud.iO") + .setIssuedAt(Date()) + .setExpiration(Date(System.currentTimeMillis() + tokenExpirationDuration)) + .signWith(SignatureAlgorithm.HS512, secretKey) + .compact() + + fun validate(token: String): Long? { + try { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).body.subject.toLong() + } catch (exception: ExpiredJwtException) { + log.warn("JWT expired", exception) + } catch (exception: IllegalArgumentException) { + log.error("Token is null, empty or only whitespace", exception) + } catch (exception: MalformedJwtException) { + log.error("JWT is invalid", exception) + } catch (exception: UnsupportedJwtException) { + log.error("JWT is not supported", exception) + } catch (exception: SignatureException) { + log.error("Signature validation failed", exception) + } + + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioUserDetailsService.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioUserDetailsService.kt index 1345e36..d69ef86 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioUserDetailsService.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioUserDetailsService.kt @@ -23,12 +23,20 @@ class CloudioUserDetailsService( @Transactional override fun loadUserByUsername(userName: String): UserDetails = userRepository.findByUserName(userName).orElseThrow { - UsernameNotFoundException("User \"$userName\"not found.") + UsernameNotFoundException("User \"$userName\" not found.") }.let { user -> CloudioUserDetails(user.id, user.userName, user.password, user.banned, user.authorities.map(Authority::name).map { authority -> SimpleGrantedAuthority(authority) }, user.groupMemberships.map { it.id }) } + @Transactional + fun loadUserByID(id: Long): CloudioUserDetails = userRepository.findById(id).orElseThrow { + UsernameNotFoundException("User with ID $id not found.") + }.let { user -> + CloudioUserDetails(user.id, user.userName, user.password, user.banned, user.authorities.map(Authority::name).map { authority -> + SimpleGrantedAuthority(authority) + }, user.groupMemberships.map { it.id }) } + @Value(value = "\${cloudio.initialAdminPassword:#{null}}") private var initialAdminPassword: String? = null From 0a53e74053762def87aa8d7ad0dbb6ee81481742 Mon Sep 17 00:00:00 2001 From: Michael Clausen Date: Sat, 17 Sep 2022 01:04:28 +0200 Subject: [PATCH 2/9] Updated SecurityRequirements annotations --- .../cloud/restapi/admin/cors/CorsManagementController.kt | 6 +++++- .../cloud/restapi/admin/user/UserManagementController.kt | 6 +++++- .../admin/usergroup/UserGroupManagementController.kt | 6 +++++- .../restapi/endpoint/data/EndpointDataAccessController.kt | 6 +++++- .../restapi/endpoint/data/EndpointWOTAccessController.kt | 6 +++++- .../endpoint/group/EndpointGroupManagementController.kt | 6 +++++- .../endpoint/history/EndpointHistoryAccessController.kt | 6 +++++- .../cloud/restapi/endpoint/jobs/EndpointJobsController.kt | 6 +++++- .../cloud/restapi/endpoint/log/EndpointLogController.kt | 6 +++++- .../endpoint/management/EndpointManagementController.kt | 6 +++++- .../permission/EndpointGroupPermissionController.kt | 6 +++++- .../endpoint/permission/EndpointPermissionController.kt | 6 +++++- .../endpoint/provisioning/EndpointProvisioningController.kt | 6 +++++- .../ch/hevs/cloudio/cloud/restapi/node/NodeController.kt | 6 +++++- 14 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/cors/CorsManagementController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/cors/CorsManagementController.kt index 85de243..1291a55 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/cors/CorsManagementController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/cors/CorsManagementController.kt @@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus @@ -22,7 +23,10 @@ import org.springframework.web.bind.annotation.* @Profile("rest-api") @Tag(name = "Cors Management", description = "Allows an admin user to manage cors allowed origins.") @RequestMapping("/api/v1/admin") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) @Authority.HttpAdmin class CorsManagementController ( private val corsRepository: CorsRepository diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/user/UserManagementController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/user/UserManagementController.kt index fcb0b61..944bbc6 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/user/UserManagementController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/user/UserManagementController.kt @@ -14,6 +14,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus @@ -26,7 +27,10 @@ import org.springframework.web.bind.annotation.* @Profile("rest-api") @Tag(name = "User Management", description = "Allows a user with the authority HTTP_ADMIN to manage users and user groups.") @RequestMapping("/api/v1/admin") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) @Authority.HttpAdmin class UserManagementController( private var userRepository: UserRepository, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/usergroup/UserGroupManagementController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/usergroup/UserGroupManagementController.kt index 716a6b4..6df103a 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/usergroup/UserGroupManagementController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/admin/usergroup/UserGroupManagementController.kt @@ -14,6 +14,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus @@ -25,7 +26,10 @@ import javax.transaction.Transactional @Profile("rest-api") @Tag(name = "User Management") @RequestMapping("/api/v1/admin") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) @Authority.HttpAdmin class UserGroupManagementController( private var userGroupRepository: UserGroupRepository, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt index 13f7869..638975b 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.influxdb.InfluxDB import org.springframework.amqp.rabbit.core.RabbitTemplate @@ -34,7 +35,10 @@ import javax.servlet.http.HttpServletRequest @Profile("rest-api") @Tag(name = "Endpoint Data Access", description = "Allows an user to access data of endpoints.") @RequestMapping("/api/v1/data") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class EndpointDataAccessController( private val endpointRepository: EndpointRepository, private val permissionManager: CloudioPermissionManager, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointWOTAccessController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointWOTAccessController.kt index 80321a9..5c9863a 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointWOTAccessController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointWOTAccessController.kt @@ -18,6 +18,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.influxdb.InfluxDB import org.springframework.context.annotation.Profile @@ -34,7 +35,10 @@ import javax.servlet.http.HttpServletRequest @Profile("rest-api") @Tag(name = "Endpoint Data Access") @RequestMapping("/api/v1/wot") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class EndpointWOTAccessController(private val endpointRepository: EndpointRepository, private val permissionManager: CloudioPermissionManager, private val influxDB: InfluxDB, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/group/EndpointGroupManagementController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/group/EndpointGroupManagementController.kt index 2079ffb..6d8f862 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/group/EndpointGroupManagementController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/group/EndpointGroupManagementController.kt @@ -10,6 +10,7 @@ import ch.hevs.cloudio.cloud.security.EndpointPermission import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus @@ -22,7 +23,10 @@ import javax.transaction.Transactional @Profile("rest-api") @Tag(name = "Endpoint Group Management", description = "Allows a user to manage endpoint groups.") @RequestMapping("/api/v1/endpoints") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class EndpointGroupManagementController( private var endpointGroupRepository: EndpointGroupRepository, private var endpointRepository: EndpointRepository, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt index 91d47d3..335077e 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt @@ -16,6 +16,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.influxdb.InfluxDB import org.influxdb.dto.Query @@ -34,7 +35,10 @@ import javax.servlet.http.HttpServletRequest @Profile("rest-api") @Tag(name = "Endpoint History Access", description = "Allows an user to access time series data of endpoints.") @RequestMapping("/api/v1/history") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class EndpointHistoryAccessController( private val endpointRepository: EndpointRepository, private val permissionManager: CloudioPermissionManager, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/jobs/EndpointJobsController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/jobs/EndpointJobsController.kt index efb341f..c53c028 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/jobs/EndpointJobsController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/jobs/EndpointJobsController.kt @@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.apache.juli.logging.LogFactory import org.springframework.amqp.rabbit.core.RabbitTemplate @@ -25,7 +26,10 @@ import java.util.* @Profile("rest-api") @Tag(name = "Endpoint remote job execution", description = "Run job (command or script) remotely on endpoints.") @RequestMapping("/api/v1/endpoints") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class EndpointJobsController( private val endpointRepository: EndpointRepository, private val serializationFormats: Collection, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/log/EndpointLogController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/log/EndpointLogController.kt index 3d5caa6..e5b148b 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/log/EndpointLogController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/log/EndpointLogController.kt @@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.influxdb.InfluxDB import org.influxdb.dto.Query @@ -26,7 +27,10 @@ import java.util.* @Profile("rest-api") @Tag(name = "Endpoint Log Access", description = "Access log output of an endpoint.") @RequestMapping("/api/v1/endpoints") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class EndpointLogController( private val influx: InfluxDB, private val influxProperties: CloudioInfluxProperties diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/management/EndpointManagementController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/management/EndpointManagementController.kt index 3da18cf..f317dc8 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/management/EndpointManagementController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/management/EndpointManagementController.kt @@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.amqp.rabbit.core.RabbitTemplate import org.springframework.context.annotation.Profile @@ -32,7 +33,10 @@ import java.util.* @Profile("rest-api") @Tag(name = "Endpoint Management", description = "Allows users to list and manage their endpoints.") @RequestMapping("/api/v1/endpoints") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class EndpointManagementController( private val endpointRepository: EndpointRepository, private val permissionManager: CloudioPermissionManager, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt index b3fc6fb..cdd3f39 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt @@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus @@ -24,7 +25,10 @@ import javax.servlet.http.HttpServletRequest @Profile("rest-api") @Tag(name = "Endpoint Permissions") @RequestMapping("/api/v1/endpoints/groups") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class EndpointGroupPermissionController( private val userRepository: UserRepository, private val userEndpointGroupPermissionRepository: UserEndpointGroupPermissionRepository, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt index 92dd12d..02cc4b1 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt @@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus @@ -24,7 +25,10 @@ import javax.servlet.http.HttpServletRequest @Profile("rest-api") @Tag(name = "Endpoint Permissions", description = "Allows users to manage permissions for their owned endpoints or endpoints for which they have the GRANT permission.") @RequestMapping("/api/v1/endpoints") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class EndpointPermissionController( private val userRepository: UserRepository, private val userGroupRepository: UserGroupRepository, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/provisioning/EndpointProvisioningController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/provisioning/EndpointProvisioningController.kt index 8f3407e..19283f2 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/provisioning/EndpointProvisioningController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/provisioning/EndpointProvisioningController.kt @@ -14,6 +14,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.context.annotation.Profile import org.springframework.http.HttpHeaders @@ -33,7 +34,10 @@ import javax.servlet.http.HttpServletRequest @Profile("rest-api") @Tag(name = "Endpoint Provisioning", description = "Allows users or developers to provision new endpoints into the system.") @RequestMapping("/api/v1") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class EndpointProvisioningController( private val endpointRepository: EndpointRepository, private val provisionTokenRepository: ProvisionTokenRepository, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/node/NodeController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/node/NodeController.kt index 9ed82dd..d444143 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/node/NodeController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/node/NodeController.kt @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.boot.info.BuildProperties import org.springframework.context.annotation.Profile @@ -26,7 +27,10 @@ import java.net.InetAddress @Tag(name = "Node info", description = "Information about the cloud.iO service node.") @RestController @RequestMapping("api/v1/node") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements(value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") +]) class NodeController( private val buildProperties: BuildProperties ) { From e1b7f6287f4ae88c618c911f82cbd08c49d5e027 Mon Sep 17 00:00:00 2001 From: Michael Clausen Date: Sat, 17 Sep 2022 12:42:48 +0200 Subject: [PATCH 3/9] Fixed test compilation errors --- .../restapi/admin/UserGroupManagementControllerTests.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/kotlin/ch/hevs/cloudio/cloud/restapi/admin/UserGroupManagementControllerTests.kt b/src/test/kotlin/ch/hevs/cloudio/cloud/restapi/admin/UserGroupManagementControllerTests.kt index ab9128f..a5b4233 100644 --- a/src/test/kotlin/ch/hevs/cloudio/cloud/restapi/admin/UserGroupManagementControllerTests.kt +++ b/src/test/kotlin/ch/hevs/cloudio/cloud/restapi/admin/UserGroupManagementControllerTests.kt @@ -126,7 +126,7 @@ class UserGroupManagementControllerTests { @WithMockUser("Admin", authorities = ["HTTP_ACCESS", "HTTP_ADMIN"]) fun modifyNonExistentGroup() { assertThrows { - userGroupManagementController.updateGroupByGroupName("TestGroup2", UserGroupEntity("TestGroup2", mapOf("test" to true))) + userGroupManagementController.updateGroupByGroupName("TestGroup2", UserGroupEntity("TestGroup2", mapOf("test" to true), emptyList())) } } @@ -134,7 +134,7 @@ class UserGroupManagementControllerTests { @WithMockUser("Admin", authorities = ["HTTP_ACCESS", "HTTP_ADMIN"]) fun modifyWhereNamesDoNotMatch() { assertThrows { - userGroupManagementController.updateGroupByGroupName("TestGroup", UserGroupEntity("TestGroup2", emptyMap())) + userGroupManagementController.updateGroupByGroupName("TestGroup", UserGroupEntity("TestGroup2", emptyMap(), emptyList())) } } @@ -142,7 +142,7 @@ class UserGroupManagementControllerTests { @WithMockUser("sepp.blatter", authorities = ["HTTP_ACCESS"]) fun modifyByNonAdmin() { assertThrows { - userGroupManagementController.updateGroupByGroupName("TestGroup", UserGroupEntity("TestGroup", mapOf("test" to true))) + userGroupManagementController.updateGroupByGroupName("TestGroup", UserGroupEntity("TestGroup", mapOf("test" to true), emptyList())) } } @@ -228,7 +228,7 @@ class UserGroupManagementControllerTests { } assertThrows { - userGroupManagementController.updateGroupByGroupName(randomCharacters, UserGroupEntity(randomCharacters, emptyMap())) + userGroupManagementController.updateGroupByGroupName(randomCharacters, UserGroupEntity(randomCharacters, emptyMap(), emptyList())) } assertThrows { From 126b3f8a9a5bdda3ebc28d13e9457fcf08b3398d Mon Sep 17 00:00:00 2001 From: Michael Clausen Date: Sat, 17 Sep 2022 12:46:22 +0200 Subject: [PATCH 4/9] Added support for single endpoint access using just a JWT token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some heavy security testsing will be required for this one… --- build.gradle.kts | 1 + .../restapi/account/AccountController.kt | 32 ++++++---- .../AccessTokenLoginController.kt} | 36 +++++++---- .../restapi/auth/UserLoginCredentials.kt | 12 ++++ .../data/EndpointDataAccessController.kt | 20 +++++-- .../data/EndpointWOTAccessController.kt | 2 +- .../EndpointGroupManagementController.kt | 25 +++++--- .../EndpointHistoryAccessController.kt | 29 ++++++--- .../EndpointManagementController.kt | 10 ++-- .../EndpointGroupPermissionController.kt | 8 ++- .../EndpointPermissionController.kt | 27 ++++++++- .../restapi/token/AccessTokenRequestEntity.kt | 6 -- .../cloud/security/AccessTokenFilter.kt | 19 ++++-- .../cloud/security/AccessTokenManager.kt | 59 +++++++++++++++---- .../security/CloudioPermissionManager.kt | 15 ++++- 15 files changed, 229 insertions(+), 72 deletions(-) rename src/main/kotlin/ch/hevs/cloudio/cloud/restapi/{token/AccessTokenController.kt => auth/AccessTokenLoginController.kt} (52%) create mode 100644 src/main/kotlin/ch/hevs/cloudio/cloud/restapi/auth/UserLoginCredentials.kt delete mode 100644 src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenRequestEntity.kt diff --git a/build.gradle.kts b/build.gradle.kts index 091255f..4859b59 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -88,6 +88,7 @@ tasks.bootRun { val adminPassword: String? by project environment("cloudio.initialAdminPassword", adminPassword ?: "admin") + environment("cloudio.jwt.secret", "56mY2Gqw1BVRErXp71HLLyLfGqSjwxMQ1d9F89yPU1GCOPD_p3VrMoOrikxhaiD16vp-iavbXkXeo2WbXio4Qw") // Certificate manager. environment("cloudio.cert-manager.caCertificate", file("cloudio-dev-environment/certificates/ca.cer").readText()) diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/account/AccountController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/account/AccountController.kt index e25a86d..eed6325 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/account/AccountController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/account/AccountController.kt @@ -49,8 +49,9 @@ class AccountController( ] ) fun getMyAccount( - @Parameter(hidden = true) authentication: Authentication - ) = userRepository.findById(authentication.userDetails().id).orElseThrow { + @Parameter(hidden = true) authentication: Authentication? + ) = if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + else userRepository.findById(authentication.userDetails().id).orElseThrow { CloudioHttpExceptions.NotFound("User not found.") }.run { AccountEntity( @@ -76,8 +77,10 @@ class AccountController( fun putMyPassword( @RequestParam @Parameter(description = "Existing password.") existingPassword: String, @RequestParam @Parameter(description = "New password.") newPassword: String, - @Parameter(hidden = true) authentication: Authentication + @Parameter(hidden = true) authentication: Authentication? ) { + if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + userRepository.findById(authentication.userDetails().id).orElseThrow { CloudioHttpExceptions.NotFound("User not found.") }.let { @@ -100,8 +103,9 @@ class AccountController( ] ) fun getMyEmailAddress( - @Parameter(hidden = true) authentication: Authentication - ) = userRepository.findById(authentication.userDetails().id).orElseThrow { + @Parameter(hidden = true) authentication: Authentication? + ) = if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + else userRepository.findById(authentication.userDetails().id).orElseThrow { CloudioHttpExceptions.NotFound("User not found.") }.emailAddress.toString() @@ -117,9 +121,11 @@ class AccountController( ] ) fun putMyEmailAddress( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) authentication: Authentication?, @RequestParam @Parameter(description = "Email address to assign to user.", example = "john.doe@theinternet.org") email: String ) { + if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + userRepository.findById(authentication.userDetails().id).orElseThrow { CloudioHttpExceptions.NotFound("User not found.") }.let { @@ -143,8 +149,9 @@ class AccountController( ] ) fun getMyMetaData( - @Parameter(hidden = true) authentication: Authentication - ) = userRepository.findById(authentication.userDetails().id).orElseThrow { + @Parameter(hidden = true) authentication: Authentication? + ) = if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + else userRepository.findById(authentication.userDetails().id).orElseThrow { CloudioHttpExceptions.NotFound("User not found.") }.metaData @@ -159,10 +166,12 @@ class AccountController( ] ) fun putMyMetaData( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) authentication: Authentication?, @RequestBody @Parameter(description = "User's metadata.", schema = Schema(type = "object", example = "{\"location\": \"Sion\", \"position\": \"Manager\"}")) body: Map ) { + if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + userRepository.findById(authentication.userDetails().id).orElseThrow { CloudioHttpExceptions.NotFound("User not found.") }.let { @@ -182,8 +191,9 @@ class AccountController( ] ) fun getMyEndpointPermissions( - @Parameter(hidden = true) authentication: Authentication - ) = permissionManager.resolvePermissions(authentication.userDetails()).map { + @Parameter(hidden = true) authentication: Authentication? + ) = if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + else permissionManager.resolvePermissions(authentication.userDetails()).map { EndpointPermissionEntity( endpoint = it.endpointUUID, permission = it.permission, diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/auth/AccessTokenLoginController.kt similarity index 52% rename from src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenController.kt rename to src/main/kotlin/ch/hevs/cloudio/cloud/restapi/auth/AccessTokenLoginController.kt index b190e78..bdbfdac 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/auth/AccessTokenLoginController.kt @@ -1,32 +1,48 @@ -package ch.hevs.cloudio.cloud.restapi.token +package ch.hevs.cloudio.cloud.restapi.auth import ch.hevs.cloudio.cloud.restapi.CloudioHttpExceptions import ch.hevs.cloudio.cloud.security.AccessTokenManager import ch.hevs.cloudio.cloud.security.Authority import ch.hevs.cloudio.cloud.security.CloudioUserDetails import ch.hevs.cloudio.cloud.security.CloudioUserDetailsService +import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag import org.apache.juli.logging.LogFactory +import org.springframework.context.annotation.Profile +import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.crypto.password.PasswordEncoder -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 org.springframework.web.bind.annotation.* @RestController +@Profile("rest-api") +@Tag(name = "Login", description = "Generates access token for the given user.") @RequestMapping("/api/v1/auth") -class AccessTokenController( +class AccessTokenLoginController( private val accessTokenManager: AccessTokenManager, private val userDetailsService: CloudioUserDetailsService, private val passwordEncoder: PasswordEncoder ) { - private val log = LogFactory.getLog(AccessTokenController::class.java) + private val log = LogFactory.getLog(AccessTokenLoginController::class.java) - @PostMapping("/login", produces = [MediaType.TEXT_PLAIN_VALUE]) + @PostMapping("/login", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.TEXT_PLAIN_VALUE]) + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Authenticates user and generates an access token for the user.") + @ApiResponses( + value = [ + ApiResponse(description = "Access token generated.", responseCode = "200", content = [Content(schema = Schema(type = "string", + example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIiwidWlkIjoyMzQyLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUyNjIzOTAyMn0.nt_g2tRx2sAUYn1p94S2nsbHVpX8CUU6oNSQ19TApC8"))]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) + ] + ) fun login( - @RequestBody @Parameter() request: AccessTokenRequestEntity + @RequestBody @Parameter(description = "User's credentials.") request: UserLoginCredentials ): String = userDetailsService.loadUserByUsername(request.username).let { when { !it.isAccountNonExpired -> { @@ -41,7 +57,7 @@ class AccessTokenController( log.info("Token refused for user \"${request.username}\" using password authentication - Password is incorrect.") throw CloudioHttpExceptions.Forbidden("Wrong password.") } - else -> accessTokenManager.generate(it as CloudioUserDetails) + else -> accessTokenManager.generateUserAccessToken(it as CloudioUserDetails) } } } diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/auth/UserLoginCredentials.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/auth/UserLoginCredentials.kt new file mode 100644 index 0000000..1cd7ed6 --- /dev/null +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/auth/UserLoginCredentials.kt @@ -0,0 +1,12 @@ +package ch.hevs.cloudio.cloud.restapi.auth + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(name = "UserLoginCredentials", description = "User credentials for access token generation.") +data class UserLoginCredentials( + @Schema(description = "Username.", example = "john.doe") + val username: String, + + @Schema(description = "User's password.", example = "--> SECRET <--") + val password: String +) diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt index 638975b..5ee8e92 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt @@ -7,9 +7,7 @@ import ch.hevs.cloudio.cloud.extension.fillFromInfluxDB import ch.hevs.cloudio.cloud.extension.userDetails import ch.hevs.cloudio.cloud.model.* import ch.hevs.cloudio.cloud.restapi.CloudioHttpExceptions -import ch.hevs.cloudio.cloud.security.CloudioPermissionManager -import ch.hevs.cloudio.cloud.security.EndpointModelElementPermission -import ch.hevs.cloudio.cloud.security.EndpointPermission +import ch.hevs.cloudio.cloud.security.* import ch.hevs.cloudio.cloud.serialization.SerializationFormat import ch.hevs.cloudio.cloud.serialization.fromIdentifiers import io.swagger.v3.oas.annotations.Operation @@ -27,6 +25,8 @@ import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.security.core.Authentication +import org.springframework.security.core.annotation.CurrentSecurityContext +import org.springframework.security.core.context.SecurityContext import org.springframework.util.AntPathMatcher import org.springframework.web.bind.annotation.* import javax.servlet.http.HttpServletRequest @@ -61,7 +61,7 @@ class EndpointDataAccessController( ] ) fun getModelElement( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) @CurrentSecurityContext context : SecurityContext, @Parameter(hidden = true) request: HttpServletRequest ): Any { @@ -77,7 +77,15 @@ class EndpointDataAccessController( } // Resolve the access level the user has to the endpoint and fail if the user has no access to the endpoint. - val endpointPermission = permissionManager.resolveEndpointPermission(authentication.userDetails(), modelIdentifier.endpoint) + val authentication = context.authentication + val endpointPermission = when(authentication.principal) { + is CloudioUserDetails -> permissionManager.resolveEndpointPermission(authentication.userDetails(), modelIdentifier.endpoint) + is AccessTokenManager.ValidEndpointPermissionToken -> (authentication.principal as AccessTokenManager.ValidEndpointPermissionToken).let { + if (it.endpointUUID != modelIdentifier.endpoint) throw CloudioHttpExceptions.Forbidden("Forbidden.") + it.permission + } + else -> EndpointPermission.DENY + } if (!endpointPermission.fulfills(EndpointPermission.ACCESS)) { throw CloudioHttpExceptions.Forbidden("Forbidden.") } @@ -157,7 +165,7 @@ class EndpointDataAccessController( ]) fun putAttribute( @RequestParam @Parameter(description = "Value to set.") value: String, - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) authentication: Authentication, // TODO: Handle token based auth @Parameter(hidden = true) request: HttpServletRequest ) { diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointWOTAccessController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointWOTAccessController.kt index 5c9863a..9853ea0 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointWOTAccessController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointWOTAccessController.kt @@ -57,7 +57,7 @@ class EndpointWOTAccessController(private val endpointRepository: EndpointReposi ] ) fun getModelElement( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) authentication: Authentication, // TODO: Handle token based auth @Parameter(hidden = true) request: HttpServletRequest ): Any { diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/group/EndpointGroupManagementController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/group/EndpointGroupManagementController.kt index 6d8f862..4e57eaa 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/group/EndpointGroupManagementController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/group/EndpointGroupManagementController.kt @@ -38,8 +38,9 @@ class EndpointGroupManagementController( @GetMapping("/groups") @ResponseStatus(HttpStatus.OK) fun getAllGroups( - @Parameter(hidden = true) authentication: Authentication - ) = permissionManager.resolveEndpointGroupsPermissions(authentication.userDetails()).mapNotNull { perm -> + @Parameter(hidden = true) authentication: Authentication? + ) = if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + else permissionManager.resolveEndpointGroupsPermissions(authentication.userDetails()).mapNotNull { perm -> endpointGroupRepository.findById(perm.endpointGroupID).orElse(null)?.let { EndpointGroupListEntity( name = it.groupName, @@ -53,7 +54,9 @@ class EndpointGroupManagementController( @ResponseStatus(HttpStatus.NO_CONTENT) @Authority.HttpEndpointCreation @Transactional - fun createGroup(@RequestBody body: EndpointGroupEntity, @Parameter(hidden = true) authentication: Authentication) { + fun createGroup(@RequestBody body: EndpointGroupEntity, @Parameter(hidden = true) authentication: Authentication?) { + if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + if (endpointGroupRepository.existsByGroupName(body.name)) { throw CloudioHttpExceptions.Conflict("Group '${body.name}' exists.") } @@ -81,8 +84,9 @@ class EndpointGroupManagementController( @PreAuthorize("hasPermission(#endpointGroupName, \"EndpointGroup\",T(ch.hevs.cloudio.cloud.security.EndpointPermission).ACCESS)") fun getGroupByGroupName( @PathVariable endpointGroupName: String, - @Parameter(hidden = true) authentication: Authentication - ) = endpointGroupRepository.findByGroupName(endpointGroupName).orElseThrow { + @Parameter(hidden = true) authentication: Authentication? + ) = if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + else endpointGroupRepository.findByGroupName(endpointGroupName).orElseThrow { CloudioHttpExceptions.NotFound("Group '$endpointGroupName' not found.") }.run { EndpointGroupEntity( @@ -107,9 +111,11 @@ class EndpointGroupManagementController( fun updateGroupByGroupName( @PathVariable endpointGroupName: String, @RequestBody body: EndpointGroupEntity, - @Parameter(hidden = true) authentication: Authentication + @Parameter(hidden = true) authentication: Authentication? ) { + if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + val endpointGroup = endpointGroupRepository.findByGroupName(endpointGroupName).orElseThrow { throw CloudioHttpExceptions.NotFound("Endpoint group not found.") } @@ -139,8 +145,13 @@ class EndpointGroupManagementController( @DeleteMapping("/groups/{endpointGroupName}") @ResponseStatus(HttpStatus.NO_CONTENT) @Transactional - fun deleteGroupByGroupName(@PathVariable endpointGroupName: String, @Parameter(hidden = true) authentication: Authentication) + fun deleteGroupByGroupName( + @PathVariable endpointGroupName: String, + @Parameter(hidden = true) authentication: Authentication? + ) { + if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + val endpointGroup = endpointGroupRepository.findByGroupName(endpointGroupName).orElseThrow { throw CloudioHttpExceptions.NotFound("Endpoint group not found.") } diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt index 335077e..8ab2639 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt @@ -6,7 +6,9 @@ import ch.hevs.cloudio.cloud.extension.userDetails import ch.hevs.cloudio.cloud.model.ActionIdentifier import ch.hevs.cloudio.cloud.model.ModelIdentifier import ch.hevs.cloudio.cloud.restapi.CloudioHttpExceptions +import ch.hevs.cloudio.cloud.security.AccessTokenManager import ch.hevs.cloudio.cloud.security.CloudioPermissionManager +import ch.hevs.cloudio.cloud.security.CloudioUserDetails import ch.hevs.cloudio.cloud.security.EndpointModelElementPermission import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter @@ -24,7 +26,8 @@ import org.influxdb.dto.QueryResult import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus import org.springframework.http.MediaType -import org.springframework.security.core.Authentication +import org.springframework.security.core.annotation.CurrentSecurityContext +import org.springframework.security.core.context.SecurityContext import org.springframework.util.AntPathMatcher import org.springframework.web.bind.annotation.* import java.text.SimpleDateFormat @@ -64,7 +67,7 @@ class EndpointHistoryAccessController( ] ) fun getModelElement( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) @CurrentSecurityContext context : SecurityContext, @RequestParam @Parameter(description = "Optional start date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no start date (all data).", required = false) from: String?, @RequestParam @Parameter(description = "Optional end date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no end date (all data).", required = false) to: String?, @RequestParam @Parameter( @@ -95,8 +98,14 @@ class EndpointHistoryAccessController( } // Check if user has access to the attribute. - if (!permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.READ)) { - throw CloudioHttpExceptions.Forbidden("Forbidden.") + val authentication = context.authentication + when { + authentication.principal is CloudioUserDetails && + !permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.READ) -> + throw CloudioHttpExceptions.Forbidden("Forbidden.") + authentication.principal is AccessTokenManager.ValidEndpointPermissionToken && + (authentication.principal as AccessTokenManager.ValidEndpointPermissionToken).endpointUUID != modelIdentifier.endpoint -> + throw CloudioHttpExceptions.Forbidden("Forbidden.") } return queryInflux(modelIdentifier, from, to, resampleInterval, resampleFunction, fillValue, max)?.values?.map { @@ -124,7 +133,7 @@ class EndpointHistoryAccessController( ] ) fun getModelElementAsCSV( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) @CurrentSecurityContext context : SecurityContext, @RequestParam @Parameter(description = "Optional start date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no start date (all data).", required = false) from: String?, @RequestParam @Parameter(description = "Optional end date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no end date (all data).", required = false) to: String?, @RequestParam @Parameter( @@ -156,8 +165,14 @@ class EndpointHistoryAccessController( } // Check if user has access to the attribute. - if (!permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.READ)) { - throw CloudioHttpExceptions.Forbidden("Forbidden.") + val authentication = context.authentication + when { + authentication.principal is CloudioUserDetails && + !permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.READ) -> + throw CloudioHttpExceptions.Forbidden("Forbidden.") + authentication.principal is AccessTokenManager.ValidEndpointPermissionToken && + (authentication.principal as AccessTokenManager.ValidEndpointPermissionToken).endpointUUID != modelIdentifier.endpoint -> + throw CloudioHttpExceptions.Forbidden("Forbidden.") } return queryInflux(modelIdentifier, from, to, resampleInterval, resampleFunction, fillValue, max)?.values?. diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/management/EndpointManagementController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/management/EndpointManagementController.kt index f317dc8..985f53b 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/management/EndpointManagementController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/management/EndpointManagementController.kt @@ -61,11 +61,12 @@ class EndpointManagementController( ] ) fun getAllAccessibleEndpoints( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) authentication: Authentication?, @RequestParam(required = false) @Parameter(description = "If given the list is filtered by the given friendly name.") friendlyName: String?, @RequestParam(required = false) @Parameter(description = "If given the list is filtered by the given banned status.") banned: Boolean?, @RequestParam(required = false) @Parameter(description = "If given the list is filtered by the given online status.") online: Boolean? - ) = permissionManager.resolvePermissions(authentication.userDetails()).mapNotNull { perm -> // TODO: Maybe resolveEndpointPermission() would be better in this case. + ) = if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + else permissionManager.resolvePermissions(authentication.userDetails()).mapNotNull { perm -> // TODO: Maybe resolveEndpointPermission() would be better in this case. endpointRepository.findById(perm.endpointUUID).orElse(null)?.let { when { !friendlyName.isNullOrEmpty() && it.friendlyName != friendlyName -> null @@ -291,8 +292,9 @@ class EndpointManagementController( ) fun postEndpointByFriendlyName( @RequestParam @Parameter(description = "Name of the endpoint.", schema = Schema(type = "string", defaultValue = "New endpoint", example = "My endpoint")) friendlyName: String = "New Endpoint", - @Parameter(hidden = true) authentication: Authentication - ) = authentication.userDetails().let { user -> + @Parameter(hidden = true) authentication: Authentication? + ) = if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + else authentication.userDetails().let { user -> val endpoint = endpointRepository.save( Endpoint( friendlyName = friendlyName diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt index cdd3f39..339593a 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt @@ -52,7 +52,7 @@ class EndpointGroupPermissionController( ] ) fun grantPermissionByEndpointGroup( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) authentication: Authentication?, @PathVariable @Parameter(description = "The endpoint group name.", required = true) endpointGroupName: String, @RequestParam @Parameter(description = "User name to grant the permission to.", required = false) userName: String?, @RequestParam @Parameter(description = "User group name to grant the permission to.", required = false) userGroupName: String?, @@ -61,6 +61,8 @@ class EndpointGroupPermissionController( schema = Schema(allowableValues = ["DENY", "ACCESS", "BROWSE", "READ", "WRITE", "CONFIGURE", "GRANT"]) ) permission: EndpointPermission ) { + if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + if (permission == EndpointPermission.OWN) { throw CloudioHttpExceptions.Forbidden("OWN permission can not be granted.") } @@ -129,13 +131,15 @@ class EndpointGroupPermissionController( ] ) fun grantModelPermissionByUUID( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) authentication: Authentication?, @PathVariable @Parameter(description = "The endpoint group name.", required = true) endpointGroupName: String, @RequestParam @Parameter(description = "User name to grant the permission to.", required = false) userName: String?, @RequestParam @Parameter(description = "Group name to grant the permission to.", required = false) userGroupName: String?, @RequestParam @Parameter(description = "Permission to grant.") permission: EndpointModelElementPermission, @Parameter(hidden = true) request: HttpServletRequest ) { + if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.") + val endpointGroup = endpointGroupRepository.findByGroupName(endpointGroupName).orElseThrow { throw CloudioHttpExceptions.NotFound("Endpoint group not found.") } diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt index 02cc4b1..9174634 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt @@ -2,6 +2,7 @@ package ch.hevs.cloudio.cloud.restapi.endpoint.permission import ch.hevs.cloudio.cloud.dao.* import ch.hevs.cloudio.cloud.restapi.CloudioHttpExceptions +import ch.hevs.cloudio.cloud.security.AccessTokenManager import ch.hevs.cloudio.cloud.security.EndpointModelElementPermission import ch.hevs.cloudio.cloud.security.EndpointPermission import io.swagger.v3.oas.annotations.Operation @@ -15,6 +16,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize import org.springframework.util.AntPathMatcher import org.springframework.web.bind.annotation.* @@ -33,7 +35,8 @@ class EndpointPermissionController( private val userRepository: UserRepository, private val userGroupRepository: UserGroupRepository, private val userEndpointPermissionRepository: UserEndpointPermissionRepository, - private val userGroupEndpointPermissionRepository: UserGroupEndpointPermissionRepository + private val userGroupEndpointPermissionRepository: UserGroupEndpointPermissionRepository, + private val accessTokenManager: AccessTokenManager ) { private val antMatcher = AntPathMatcher() @@ -102,6 +105,28 @@ class EndpointPermissionController( else -> throw CloudioHttpExceptions.BadRequest("Either userName or groupName has to be provided.") } + @GetMapping("/{uuid}/token", produces = [MediaType.TEXT_PLAIN_VALUE]) + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasPermission(#uuid,T(ch.hevs.cloudio.cloud.security.EndpointPermission).GRANT)") + @Operation(summary = "Generate an access token for the given endpoint.") + @ApiResponses( + value = [ + ApiResponse(description = "Access token generated.", responseCode = "200", content = [Content(schema = Schema(type = "string", + example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlbmRwb2ludCIsInV1aWQiOiI4ZGQwZjgzNS1jZjQwLTRiM2YtYjRmYi0xMDhiMDBjYmI2ZWMiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUyNjIzOTAyMn0.dPzU5suQ_UKpbeQcXbtIbPahZK04tEa4DOxdE1zc3ew"))]), + ApiResponse(description = "Token can only be generated for READ, WRITE and CONFIGURE permission.", responseCode = "400", content = [Content()]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) + ] + ) + fun getAccessTokenByUUID( + @PathVariable @Parameter(description = "UUID of endpoint.", required = true) uuid: UUID, + @RequestParam @Parameter( + description = "Permission to grant.", + schema = Schema(allowableValues = ["READ", "WRITE", "CONFIGURE"]) + ) permission: EndpointPermission) = when(permission) { + EndpointPermission.READ, EndpointPermission.WRITE, EndpointPermission.CONFIGURE -> accessTokenManager.generateEndpointPermissionAccessToken(uuid, permission) + else -> throw CloudioHttpExceptions.BadRequest("Token can only be generated for READ, WRITE and CONFIGURE permission.") + } + @PutMapping("/{uuid}/grant/**") @ResponseStatus(HttpStatus.NO_CONTENT) @PreAuthorize("hasPermission(#uuid,T(ch.hevs.cloudio.cloud.security.EndpointPermission).GRANT)") diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenRequestEntity.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenRequestEntity.kt deleted file mode 100644 index 1ebc1a3..0000000 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/token/AccessTokenRequestEntity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ch.hevs.cloudio.cloud.restapi.token - -data class AccessTokenRequestEntity( - val username: String, - val password: String -) diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt index ac68aa3..9d00c78 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt @@ -1,6 +1,8 @@ package ch.hevs.cloudio.cloud.security +import org.springframework.security.authentication.AnonymousAuthenticationToken import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.web.authentication.WebAuthenticationDetailsSource import org.springframework.stereotype.Component @@ -18,11 +20,18 @@ class AccessTokenFilter( request.getHeader("Authorization").let { header -> if (header != null && header.isNotEmpty() && header.startsWith("Bearer")) { header.split(" ").getOrNull(1)?.trim()?.let { token -> - accessTokenManager.validate(token)?.let { username -> - userDetailsService.loadUserByID(username).let { userDetails -> - SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities).apply { - details = WebAuthenticationDetailsSource().buildDetails(request) + accessTokenManager.validate(token).let { result -> + when(result) { + is AccessTokenManager.ValidUserToken -> userDetailsService.loadUserByID(result.userId).let { userDetails -> + SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities).apply { + details = WebAuthenticationDetailsSource().buildDetails(request) + } } + + is AccessTokenManager.ValidEndpointPermissionToken -> + SecurityContextHolder.getContext().authentication = AnonymousAuthenticationToken(result.id, result, listOf(Authority.HTTP_ACCESS).map { SimpleGrantedAuthority(it.name) }) + + is AccessTokenManager.InvalidToken -> {} } } } @@ -31,4 +40,4 @@ class AccessTokenFilter( filterChain.doFilter(request, response) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt index b68ffef..453abd9 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt @@ -9,6 +9,10 @@ import javax.annotation.PostConstruct @Service class AccessTokenManager { + sealed interface ValidationResult + object InvalidToken: ValidationResult + class ValidUserToken(val userId: Long): ValidationResult + class ValidEndpointPermissionToken(val endpointUUID: UUID, val permission: EndpointPermission, val id: String): ValidationResult private val log = LogFactory.getLog(AccessTokenManager::class.java) @Value("\${cloudio.jwt.secret:#{null}}") @@ -20,7 +24,7 @@ class AccessTokenManager { @PostConstruct private fun generateSecretKeyIfMissing() { if (secretKey == null) { - log.error("JWT token secret key (cloudio.jwt.secret) is missing, a key will be generated. Tokens will only be valid for this node and not across restarts.") + log.error("JWT token secret key (cloudio.jwt.secret) is missing, a key will be generated. Tokens will only be valid for this node and not across restarts!") secretKey = ByteArray(64).let { Random(System.currentTimeMillis()).nextBytes(it) Base64.getEncoder().encodeToString(it) @@ -28,29 +32,64 @@ class AccessTokenManager { } } - fun generate(user: CloudioUserDetails): String = Jwts.builder() - .setSubject(user.id.toString()) + fun generateUserAccessToken(user: CloudioUserDetails): String = Jwts.builder() + .setSubject("user") + .setId(UUID.randomUUID().toString()) + .claim("uid", user.id.toString()) .setIssuer("cloud.iO") .setIssuedAt(Date()) .setExpiration(Date(System.currentTimeMillis() + tokenExpirationDuration)) .signWith(SignatureAlgorithm.HS512, secretKey) .compact() - fun validate(token: String): Long? { + fun generateEndpointPermissionAccessToken(endpointUUID: UUID, permission: EndpointPermission): String = Jwts.builder() + .setSubject("endpoint") + .setId(UUID.randomUUID().toString()) + .claim("uuid", endpointUUID.toString()) + .claim("perm", permission) + .setIssuer("cloud.iO") + .setIssuedAt(Date()) + .setExpiration(Date(System.currentTimeMillis() + tokenExpirationDuration)) + .signWith(SignatureAlgorithm.HS512, secretKey) + .compact() + + fun validate(token: String): ValidationResult { try { - return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).body.subject.toLong() + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).let { claims -> + when (claims.body.subject) { + "user" -> ValidUserToken((claims.body["uid"] as String).toLong()) + "endpoint" -> { + EndpointPermission.valueOf(claims.body["perm"] as String).let { permission -> + when(permission) { + EndpointPermission.READ, EndpointPermission.WRITE, EndpointPermission.CONFIGURE -> + ValidEndpointPermissionToken(UUID.fromString(claims.body["uuid"] as String), permission, claims.body.id) + else -> { + log.warn("Token for endpoint access contains invalid permission") + InvalidToken + } + } + } + } + else -> { + log.warn("Token contains invalid subject: ${claims.body.subject}") + InvalidToken + } + } + } } catch (exception: ExpiredJwtException) { log.warn("JWT expired", exception) } catch (exception: IllegalArgumentException) { - log.error("Token is null, empty or only whitespace", exception) + log.warn("Token is null, empty or only whitespace or endpoint token contains invalid UUID", exception) } catch (exception: MalformedJwtException) { - log.error("JWT is invalid", exception) + log.warn("JWT is invalid", exception) } catch (exception: UnsupportedJwtException) { - log.error("JWT is not supported", exception) + log.warn("JWT is not supported", exception) } catch (exception: SignatureException) { - log.error("Signature validation failed", exception) + log.warn("Signature validation failed", exception) + } catch (exception: NumberFormatException) { + log.warn("Invalid user ID in token.", exception) } - return null + return InvalidToken } } \ No newline at end of file diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt index fb1dc14..40b0fd0 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt @@ -23,20 +23,31 @@ class CloudioPermissionManager( private val log = LogFactory.getLog(CloudioPermissionManager::class.java) override fun hasPermission(authentication: Authentication, subject: Any?, permission: Any?) = when { - subject is UUID && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> hasEndpointPermission(authentication.principal as CloudioUserDetails, subject, permission) + subject is UUID && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> + hasEndpointPermission(authentication.principal as CloudioUserDetails, subject, permission) + + subject is UUID && permission is EndpointPermission && authentication.principal is AccessTokenManager.ValidEndpointPermissionToken -> + (authentication.principal as AccessTokenManager.ValidEndpointPermissionToken).let { token -> + subject == token.endpointUUID && token.permission.fulfills(permission) + } + subject is String && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> try { hasEndpointPermission(authentication.principal as CloudioUserDetails, UUID.fromString(subject), permission) } catch (e: Exception) { log.error("Invalid endpoint UUID: Authentication = $authentication, subject = $subject, permission = $permission") false } - subject is ModelIdentifier && permission is EndpointModelElementPermission && authentication.principal is CloudioUserDetails -> hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, subject, permission) + + subject is ModelIdentifier && permission is EndpointModelElementPermission && authentication.principal is CloudioUserDetails -> + hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, subject, permission) + subject is String && permission is EndpointModelElementPermission && authentication.principal is CloudioUserDetails -> ModelIdentifier(subject).let { if (it.valid) hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, it, permission) else { log.error("Invalid model identifier: Authentication = $authentication, subject = $subject, permission = $permission") false } } + else -> { log.error("Unknown permission request: Authentication = $authentication, subject = $subject, permission = $permission") false From 7376735cbc1c0b140a17ff432bca61b51b3249d7 Mon Sep 17 00:00:00 2001 From: Michael Clausen Date: Sat, 17 Sep 2022 14:46:28 +0200 Subject: [PATCH 5/9] Using JWT tokens for endpoint provisioning --- .../hevs/cloudio/cloud/dao/ProvisionToken.kt | 28 ------- .../cloud/dao/ProvisionTokenRepository.kt | 12 --- .../EndpointProvisioningController.kt | 75 ++++++++----------- .../cloud/security/AccessTokenFilter.kt | 4 +- .../cloud/security/AccessTokenManager.kt | 29 ++++--- 5 files changed, 53 insertions(+), 95 deletions(-) delete mode 100644 src/main/kotlin/ch/hevs/cloudio/cloud/dao/ProvisionToken.kt delete mode 100644 src/main/kotlin/ch/hevs/cloudio/cloud/dao/ProvisionTokenRepository.kt diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/dao/ProvisionToken.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/dao/ProvisionToken.kt deleted file mode 100644 index 23867bd..0000000 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/dao/ProvisionToken.kt +++ /dev/null @@ -1,28 +0,0 @@ -package ch.hevs.cloudio.cloud.dao - -import java.util.* -import javax.persistence.* - -@Entity -@Table(name = "cloudio_provision_token") -data class ProvisionToken( - @Column(unique = true, nullable = false, updatable = false, length = 255) - val token: String = generateToken(), - - @Column(nullable = false, updatable = false) - val endpointUUID: UUID = UUID(0, 0), - - @Column(nullable = false, updatable = false) - val expires: Date = Date() -) { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long = 0 - - companion object { - private fun generateToken(): String { - val alphabet: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') - return List(64) { alphabet.random() }.joinToString("") - } - } -} diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/dao/ProvisionTokenRepository.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/dao/ProvisionTokenRepository.kt deleted file mode 100644 index 72a5228..0000000 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/dao/ProvisionTokenRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ch.hevs.cloudio.cloud.dao - -import org.springframework.data.repository.CrudRepository -import org.springframework.stereotype.Repository -import java.util.* - -@Repository -interface ProvisionTokenRepository : CrudRepository { - fun findByToken(token: String): Optional - fun deleteByToken(token: String) - fun deleteByEndpointUUID(uuid: UUID) -} diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/provisioning/EndpointProvisioningController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/provisioning/EndpointProvisioningController.kt index 19283f2..7034977 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/provisioning/EndpointProvisioningController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/provisioning/EndpointProvisioningController.kt @@ -2,11 +2,10 @@ package ch.hevs.cloudio.cloud.restapi.endpoint.provisioning import ch.hevs.cloudio.cloud.dao.Endpoint import ch.hevs.cloudio.cloud.dao.EndpointRepository -import ch.hevs.cloudio.cloud.dao.ProvisionToken -import ch.hevs.cloudio.cloud.dao.ProvisionTokenRepository import ch.hevs.cloudio.cloud.extension.* import ch.hevs.cloudio.cloud.internalservice.certificatemanager.CertificateManagerService import ch.hevs.cloudio.cloud.restapi.CloudioHttpExceptions +import ch.hevs.cloudio.cloud.security.AccessTokenManager import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.media.Content @@ -34,14 +33,16 @@ import javax.servlet.http.HttpServletRequest @Profile("rest-api") @Tag(name = "Endpoint Provisioning", description = "Allows users or developers to provision new endpoints into the system.") @RequestMapping("/api/v1") -@SecurityRequirements(value = [ - SecurityRequirement(name = "basicAuth"), - SecurityRequirement(name = "tokenAuth") -]) +@SecurityRequirements( + value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") + ] +) class EndpointProvisioningController( private val endpointRepository: EndpointRepository, - private val provisionTokenRepository: ProvisionTokenRepository, - private val certificateManager: CertificateManagerService + private val certificateManager: CertificateManagerService, + private val accessTokenManager: AccessTokenManager ) { private val caCertificate by lazy { certificateManager.getCACertificate() @@ -97,18 +98,7 @@ class EndpointProvisioningController( endpoint.configuration.properties.putAll(this) } - // Remove pending tokens for this endpoint. - provisionTokenRepository.deleteByEndpointUUID(uuid) - - // Add a token to the provision token database. - val expires = Date() - expires.time = System.currentTimeMillis() + 24 * 3600 * 1000 - return provisionTokenRepository.save( - ProvisionToken( - endpointUUID = uuid, - expires = expires - ) - ).token + return accessTokenManager.generateProvisionToken(uuid, 24 * 2600 * 1000) } @GetMapping("/endpoints/{uuid}/provision", produces = [MediaType.APPLICATION_JSON_VALUE, "application/java-archive", "application/zip"]) @@ -140,7 +130,7 @@ class EndpointProvisioningController( }, publicKey ) - @GetMapping("/provision/{token}", produces = [MediaType.APPLICATION_JSON_VALUE, "application/java-archive"]) + @GetMapping("/provision/{token}", produces = [MediaType.APPLICATION_JSON_VALUE, "application/java-archive", "application/zip"]) @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get endpoint configuration information for a pending provision token.") @ApiResponses( @@ -160,25 +150,26 @@ class EndpointProvisioningController( @RequestParam @Parameter(description = "Public key to use for certificate generation in PEM format.") publicKey: String?, @RequestParam @Parameter(description = "Name of the properties file. If not specified, the file will named according to the UUID of the endpoint.") propertiesFileName: String?, @Parameter(hidden = true) request: HttpServletRequest - ): ResponseEntity = provisionTokenRepository.findByToken(token).orElseThrow { - CloudioHttpExceptions.Forbidden("Forbidden") - }.let { - endpointRepository.findById(it.endpointUUID).orElseThrow { - CloudioHttpExceptions.NotFound("Endpoint not found") - }.getProvisionEntity( - when (request.getHeader("Accept")) { - "application/java-archive" -> EndpointProvisioningDataFormat.JAR_ARCHIVE - "application/zip" -> EndpointProvisioningDataFormat.ZIP_ARCHIVE - else -> EndpointProvisioningDataFormat.JSON - }, publicKey, propertiesFileName, it - ) + ): ResponseEntity = accessTokenManager.validate(token).let { result -> + when (result) { + is AccessTokenManager.ValidProvisioningToken -> endpointRepository.findById(result.endpointUUID).orElseThrow { + CloudioHttpExceptions.NotFound("Endpoint not found") + }.getProvisionEntity( + when (request.getHeader("Accept")) { + "application/java-archive" -> EndpointProvisioningDataFormat.JAR_ARCHIVE + "application/zip" -> EndpointProvisioningDataFormat.ZIP_ARCHIVE + else -> EndpointProvisioningDataFormat.JSON + }, publicKey, propertiesFileName + ) + + else -> throw CloudioHttpExceptions.Forbidden("Forbidden") + } } private fun Endpoint.getProvisionEntity( endpointProvisionDataFormat: EndpointProvisioningDataFormat?, publicKey: String?, - propertiesFileName: String? = null, - token: ProvisionToken? = null, + propertiesFileName: String? = null ): ResponseEntity = this.run { if (configuration.clientCertificate.isNotEmpty() && publicKey != null) { configuration.clientCertificate = "" @@ -201,9 +192,6 @@ class EndpointProvisioningController( } endpointRepository.save(this) - if (token != null) { - provisionTokenRepository.delete(token) - } when (endpointProvisionDataFormat ?: EndpointProvisioningDataFormat.JSON) { EndpointProvisioningDataFormat.JSON -> ResponseEntity.ok() @@ -264,6 +252,7 @@ class EndpointProvisioningController( .header("Endpoint", this.uuid.toString()) .body(output.toByteArray()) } + EndpointProvisioningDataFormat.ZIP_ARCHIVE -> { if (configuration.privateKey.isEmpty()) { CloudioHttpExceptions.BadRequest("Endpoint has no private key.") @@ -296,7 +285,7 @@ class EndpointProvisioningController( zip.write(certificate) zip.closeEntry() - if (configuration.privateKey.isNotEmpty()){ + if (configuration.privateKey.isNotEmpty()) { zip.putNextEntry(ZipEntry("${uuid}-private-key.pem")) zip.write(configuration.privateKey.toByteArray()) zip.closeEntry() @@ -306,10 +295,10 @@ class EndpointProvisioningController( zip.close() ResponseEntity.ok() - .contentType(MediaType.parseMediaType("application/zip")) - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"${this.uuid}.zip\"") - .header("Endpoint", this.uuid.toString()) - .body(output.toByteArray()) + .contentType(MediaType.parseMediaType("application/zip")) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"${this.uuid}.zip\"") + .header("Endpoint", this.uuid.toString()) + .body(output.toByteArray()) } } } diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt index 9d00c78..1babc53 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt @@ -29,8 +29,8 @@ class AccessTokenFilter( } is AccessTokenManager.ValidEndpointPermissionToken -> - SecurityContextHolder.getContext().authentication = AnonymousAuthenticationToken(result.id, result, listOf(Authority.HTTP_ACCESS).map { SimpleGrantedAuthority(it.name) }) - + SecurityContextHolder.getContext().authentication = AnonymousAuthenticationToken(result.hashCode().toString(), result, listOf(Authority.HTTP_ACCESS).map { SimpleGrantedAuthority(it.name) }) + is AccessTokenManager.InvalidToken -> {} } } diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt index 453abd9..df96c22 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt @@ -11,8 +11,9 @@ import javax.annotation.PostConstruct class AccessTokenManager { sealed interface ValidationResult object InvalidToken: ValidationResult + class ValidProvisioningToken(val endpointUUID: UUID): ValidationResult class ValidUserToken(val userId: Long): ValidationResult - class ValidEndpointPermissionToken(val endpointUUID: UUID, val permission: EndpointPermission, val id: String): ValidationResult + data class ValidEndpointPermissionToken(val endpointUUID: UUID, val permission: EndpointPermission): ValidationResult private val log = LogFactory.getLog(AccessTokenManager::class.java) @Value("\${cloudio.jwt.secret:#{null}}") @@ -32,9 +33,17 @@ class AccessTokenManager { } } + fun generateProvisionToken(endpointUUID: UUID, expirationDuration: Long = tokenExpirationDuration): String = Jwts.builder() + .setSubject("provision") + .claim("endpoint", endpointUUID.toString()) + .setIssuer("cloud.iO") + .setIssuedAt(Date()) + .setExpiration(Date(System.currentTimeMillis() + expirationDuration)) + .signWith(SignatureAlgorithm.HS512, secretKey) + .compact() + fun generateUserAccessToken(user: CloudioUserDetails): String = Jwts.builder() .setSubject("user") - .setId(UUID.randomUUID().toString()) .claim("uid", user.id.toString()) .setIssuer("cloud.iO") .setIssuedAt(Date()) @@ -44,7 +53,6 @@ class AccessTokenManager { fun generateEndpointPermissionAccessToken(endpointUUID: UUID, permission: EndpointPermission): String = Jwts.builder() .setSubject("endpoint") - .setId(UUID.randomUUID().toString()) .claim("uuid", endpointUUID.toString()) .claim("perm", permission) .setIssuer("cloud.iO") @@ -57,12 +65,13 @@ class AccessTokenManager { try { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).let { claims -> when (claims.body.subject) { + "provision" -> ValidProvisioningToken(UUID.fromString(claims.body["endpoint"] as String)) "user" -> ValidUserToken((claims.body["uid"] as String).toLong()) "endpoint" -> { EndpointPermission.valueOf(claims.body["perm"] as String).let { permission -> when(permission) { EndpointPermission.READ, EndpointPermission.WRITE, EndpointPermission.CONFIGURE -> - ValidEndpointPermissionToken(UUID.fromString(claims.body["uuid"] as String), permission, claims.body.id) + ValidEndpointPermissionToken(UUID.fromString(claims.body["uuid"] as String), permission) else -> { log.warn("Token for endpoint access contains invalid permission") InvalidToken @@ -77,17 +86,17 @@ class AccessTokenManager { } } } catch (exception: ExpiredJwtException) { - log.warn("JWT expired", exception) + log.warn("JWT expired") } catch (exception: IllegalArgumentException) { - log.warn("Token is null, empty or only whitespace or endpoint token contains invalid UUID", exception) + log.warn("Token is null, empty or only whitespace or endpoint token contains invalid UUID") } catch (exception: MalformedJwtException) { - log.warn("JWT is invalid", exception) + log.warn("JWT is invalid") } catch (exception: UnsupportedJwtException) { - log.warn("JWT is not supported", exception) + log.warn("JWT is not supported") } catch (exception: SignatureException) { - log.warn("Signature validation failed", exception) + log.warn("Signature validation failed") } catch (exception: NumberFormatException) { - log.warn("Invalid user ID in token.", exception) + log.warn("Invalid user ID in token") } return InvalidToken From 4f38d47a3212fc5aa5fedd8bdf9b165f0a91afa1 Mon Sep 17 00:00:00 2001 From: Michael Clausen Date: Sat, 17 Sep 2022 18:52:59 +0200 Subject: [PATCH 6/9] Added support for endpoint group access tokens --- .../data/EndpointDataAccessController.kt | 63 ++++-- .../EndpointHistoryAccessController.kt | 45 ++-- .../EndpointGroupPermissionController.kt | 46 +++- .../EndpointPermissionController.kt | 14 +- .../cloud/security/AccessTokenFilter.kt | 32 ++- .../cloud/security/AccessTokenManager.kt | 69 ++++-- .../security/CloudioPermissionManager.kt | 210 ++++++++++-------- src/main/resources/application-rest-api.yml | 5 +- 8 files changed, 322 insertions(+), 162 deletions(-) diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt index 5ee8e92..a0776c3 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/data/EndpointDataAccessController.kt @@ -24,7 +24,6 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus import org.springframework.http.MediaType -import org.springframework.security.core.Authentication import org.springframework.security.core.annotation.CurrentSecurityContext import org.springframework.security.core.context.SecurityContext import org.springframework.util.AntPathMatcher @@ -35,10 +34,12 @@ import javax.servlet.http.HttpServletRequest @Profile("rest-api") @Tag(name = "Endpoint Data Access", description = "Allows an user to access data of endpoints.") @RequestMapping("/api/v1/data") -@SecurityRequirements(value = [ - SecurityRequirement(name = "basicAuth"), - SecurityRequirement(name = "tokenAuth") -]) +@SecurityRequirements( + value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") + ] +) class EndpointDataAccessController( private val endpointRepository: EndpointRepository, private val permissionManager: CloudioPermissionManager, @@ -61,7 +62,7 @@ class EndpointDataAccessController( ] ) fun getModelElement( - @Parameter(hidden = true) @CurrentSecurityContext context : SecurityContext, + @Parameter(hidden = true) @CurrentSecurityContext context: SecurityContext, @Parameter(hidden = true) request: HttpServletRequest ): Any { @@ -78,12 +79,14 @@ class EndpointDataAccessController( // Resolve the access level the user has to the endpoint and fail if the user has no access to the endpoint. val authentication = context.authentication - val endpointPermission = when(authentication.principal) { + val endpointPermission = when (authentication.principal) { is CloudioUserDetails -> permissionManager.resolveEndpointPermission(authentication.userDetails(), modelIdentifier.endpoint) - is AccessTokenManager.ValidEndpointPermissionToken -> (authentication.principal as AccessTokenManager.ValidEndpointPermissionToken).let { - if (it.endpointUUID != modelIdentifier.endpoint) throw CloudioHttpExceptions.Forbidden("Forbidden.") - it.permission - } + is AccessTokenManager.ValidEndpointPermissionToken, is AccessTokenManager.ValidEndpointGroupPermissionToken -> + if (permissionManager.hasPermission(authentication, modelIdentifier.endpoint, EndpointPermission.READ)) { + EndpointPermission.READ + } else { + EndpointPermission.DENY + } else -> EndpointPermission.DENY } if (!endpointPermission.fulfills(EndpointPermission.ACCESS)) { @@ -155,17 +158,21 @@ class EndpointDataAccessController( @PutMapping("/**", consumes = [MediaType.APPLICATION_JSON_VALUE]) @ResponseStatus(HttpStatus.NO_CONTENT) @Operation(summary = "Write access to all endpoint's data model.") - @ApiResponses(value = [ - ApiResponse(description = "Endpoint data was written to the given path.", responseCode = "204", content = [Content()]), - ApiResponse(description = "Invalid model identifier, attribute is not a SetPoint, nor a Parameter or element is not an attribute.", - responseCode = "400", content = [Content()]), - ApiResponse(description = "Endpoint not found or model element not found.", responseCode = "404", content = [Content()]), - ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]), - ApiResponse(description = "Invalid datatype or endpoint does not support any serialization format.", responseCode = "500", content = [Content()]), - ]) + @ApiResponses( + value = [ + ApiResponse(description = "Endpoint data was written to the given path.", responseCode = "204", content = [Content()]), + ApiResponse( + description = "Invalid model identifier, attribute is not a SetPoint, nor a Parameter or element is not an attribute.", + responseCode = "400", content = [Content()] + ), + ApiResponse(description = "Endpoint not found or model element not found.", responseCode = "404", content = [Content()]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]), + ApiResponse(description = "Invalid datatype or endpoint does not support any serialization format.", responseCode = "500", content = [Content()]), + ] + ) fun putAttribute( @RequestParam @Parameter(description = "Value to set.") value: String, - @Parameter(hidden = true) authentication: Authentication, // TODO: Handle token based auth + @Parameter(hidden = true) @CurrentSecurityContext context: SecurityContext, @Parameter(hidden = true) request: HttpServletRequest ) { @@ -178,7 +185,21 @@ class EndpointDataAccessController( modelIdentifier.action = ActionIdentifier.ATTRIBUTE_SET // Resolve the access level the user has to the element. - if (!permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.WRITE)) { + val authentication = context.authentication + val endpointPermission = when (authentication.principal) { + is CloudioUserDetails -> permissionManager.resolveEndpointPermission(authentication.userDetails(), modelIdentifier.endpoint) + is AccessTokenManager.ValidEndpointPermissionToken, is AccessTokenManager.ValidEndpointGroupPermissionToken -> + if (permissionManager.hasPermission(authentication, modelIdentifier.endpoint, EndpointPermission.WRITE)) { + EndpointPermission.WRITE + } else { + EndpointPermission.DENY + } + + else -> EndpointPermission.DENY + } + if (!endpointPermission.fulfills(EndpointPermission.WRITE) && + !permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.WRITE) + ) { throw CloudioHttpExceptions.Forbidden("Forbidden.") } diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt index 8ab2639..8de0311 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt @@ -6,10 +6,7 @@ import ch.hevs.cloudio.cloud.extension.userDetails import ch.hevs.cloudio.cloud.model.ActionIdentifier import ch.hevs.cloudio.cloud.model.ModelIdentifier import ch.hevs.cloudio.cloud.restapi.CloudioHttpExceptions -import ch.hevs.cloudio.cloud.security.AccessTokenManager -import ch.hevs.cloudio.cloud.security.CloudioPermissionManager -import ch.hevs.cloudio.cloud.security.CloudioUserDetails -import ch.hevs.cloudio.cloud.security.EndpointModelElementPermission +import ch.hevs.cloudio.cloud.security.* import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.media.ArraySchema @@ -38,15 +35,18 @@ import javax.servlet.http.HttpServletRequest @Profile("rest-api") @Tag(name = "Endpoint History Access", description = "Allows an user to access time series data of endpoints.") @RequestMapping("/api/v1/history") -@SecurityRequirements(value = [ - SecurityRequirement(name = "basicAuth"), - SecurityRequirement(name = "tokenAuth") -]) +@SecurityRequirements( + value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") + ] +) class EndpointHistoryAccessController( private val endpointRepository: EndpointRepository, private val permissionManager: CloudioPermissionManager, private val influx: InfluxDB, - private val influxProperties: CloudioInfluxProperties + private val influxProperties: CloudioInfluxProperties, + private val userDetailsService: CloudioUserDetailsService ) { private val antMatcher = AntPathMatcher() @@ -67,7 +67,7 @@ class EndpointHistoryAccessController( ] ) fun getModelElement( - @Parameter(hidden = true) @CurrentSecurityContext context : SecurityContext, + @Parameter(hidden = true) @CurrentSecurityContext context: SecurityContext, @RequestParam @Parameter(description = "Optional start date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no start date (all data).", required = false) from: String?, @RequestParam @Parameter(description = "Optional end date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no end date (all data).", required = false) to: String?, @RequestParam @Parameter( @@ -103,8 +103,9 @@ class EndpointHistoryAccessController( authentication.principal is CloudioUserDetails && !permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.READ) -> throw CloudioHttpExceptions.Forbidden("Forbidden.") - authentication.principal is AccessTokenManager.ValidEndpointPermissionToken && - (authentication.principal as AccessTokenManager.ValidEndpointPermissionToken).endpointUUID != modelIdentifier.endpoint -> + + (authentication.principal is AccessTokenManager.ValidEndpointPermissionToken || authentication.principal is AccessTokenManager.ValidEndpointGroupPermissionToken) && + !permissionManager.hasPermission(authentication, modelIdentifier.endpoint, EndpointPermission.READ) -> throw CloudioHttpExceptions.Forbidden("Forbidden.") } @@ -133,7 +134,7 @@ class EndpointHistoryAccessController( ] ) fun getModelElementAsCSV( - @Parameter(hidden = true) @CurrentSecurityContext context : SecurityContext, + @Parameter(hidden = true) @CurrentSecurityContext context: SecurityContext, @RequestParam @Parameter(description = "Optional start date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no start date (all data).", required = false) from: String?, @RequestParam @Parameter(description = "Optional end date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no end date (all data).", required = false) to: String?, @RequestParam @Parameter( @@ -170,18 +171,26 @@ class EndpointHistoryAccessController( authentication.principal is CloudioUserDetails && !permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.READ) -> throw CloudioHttpExceptions.Forbidden("Forbidden.") - authentication.principal is AccessTokenManager.ValidEndpointPermissionToken && - (authentication.principal as AccessTokenManager.ValidEndpointPermissionToken).endpointUUID != modelIdentifier.endpoint -> + + (authentication.principal is AccessTokenManager.ValidEndpointPermissionToken || authentication.principal is AccessTokenManager.ValidEndpointGroupPermissionToken) && + !permissionManager.hasPermission(authentication, modelIdentifier.endpoint, EndpointPermission.READ) -> throw CloudioHttpExceptions.Forbidden("Forbidden.") } - return queryInflux(modelIdentifier, from, to, resampleInterval, resampleFunction, fillValue, max)?.values?. - joinToString(separator = "\n") { + return queryInflux(modelIdentifier, from, to, resampleInterval, resampleFunction, fillValue, max)?.values?.joinToString(separator = "\n") { "${it[0] as String}${separator ?: ";"}${it[1]}" }.orEmpty() } - private fun queryInflux(modelIdentifier: ModelIdentifier, from: String?, to: String?, resampleInterval: String?, resampleFunction: ResampleFunction?, fillValue: FillValue?, max: Int?): QueryResult.Series? { + private fun queryInflux( + modelIdentifier: ModelIdentifier, + from: String?, + to: String?, + resampleInterval: String?, + resampleFunction: ResampleFunction?, + fillValue: FillValue?, + max: Int? + ): QueryResult.Series? { val result = influx.query(Query( "SELECT time, ${ if (resampleInterval != null) { diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt index 339593a..237107f 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt @@ -3,6 +3,7 @@ package ch.hevs.cloudio.cloud.restapi.endpoint.permission import ch.hevs.cloudio.cloud.dao.* import ch.hevs.cloudio.cloud.extension.userDetails import ch.hevs.cloudio.cloud.restapi.CloudioHttpExceptions +import ch.hevs.cloudio.cloud.security.AccessTokenManager import ch.hevs.cloudio.cloud.security.EndpointModelElementPermission import ch.hevs.cloudio.cloud.security.EndpointPermission import io.swagger.v3.oas.annotations.Operation @@ -16,9 +17,12 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.Authentication import org.springframework.util.AntPathMatcher import org.springframework.web.bind.annotation.* +import java.util.* import javax.servlet.http.HttpServletRequest @RestController @@ -36,7 +40,8 @@ class EndpointGroupPermissionController( private val userEndpointGroupModelElementPermissionRepository: UserEndpointGroupPermissionRepository, private val userGroupRepository: UserGroupRepository, private val userGroupEndpointGroupPermissionRepository: UserGroupEndpointGroupPermissionRepository, - private val userGroupEndpointGroupModelElementPermissionRepository: UserGroupEndpointGroupPermissionRepository + private val userGroupEndpointGroupModelElementPermissionRepository: UserGroupEndpointGroupPermissionRepository, + private val accessTokenManager: AccessTokenManager ) { private val antMatcher = AntPathMatcher() @@ -119,6 +124,45 @@ class EndpointGroupPermissionController( } } + @GetMapping("/{endpointGroupName}/token", produces = [MediaType.TEXT_PLAIN_VALUE]) + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasPermission(#endpointGroupName, \"EndpointGroup\",T(ch.hevs.cloudio.cloud.security.EndpointPermission).GRANT)") + @Operation(summary = "Generate an access token for the given endpoint group.") + @ApiResponses( + value = [ + ApiResponse( + description = "Access token generated.", responseCode = "200", content = [Content( + schema = Schema( + type = "string", + example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlbmRwb2ludCIsInV1aWQiOiI4ZGQwZjgzNS1jZjQwLTRiM2YtYjRmYi0xMDhiMDBjYmI2ZWMiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUyNjIzOTAyMn0.dPzU5suQ_UKpbeQcXbtIbPahZK04tEa4DOxdE1zc3ew" + ) + )] + ), + ApiResponse(description = "Token can only be generated for READ, WRITE and CONFIGURE permission.", responseCode = "400", content = [Content()]), + ApiResponse(description = "User Group not found.", responseCode = "404", content = [Content()]), + ApiResponse(description = "Forbidden.", responseCode = "403", content = [Content()]) + ] + ) + fun getAccessTokenByGroupName( + @PathVariable @Parameter(description = "Name of the endpoint group.", required = true) endpointGroupName: String, + @RequestParam @Parameter(description = "Permission to grant.", schema = Schema(allowableValues = ["READ", "WRITE", "CONFIGURE"])) permission: EndpointPermission, + @RequestParam @Parameter( + description = "Expiration date and time for the token in ISO-8601 format (yyyy-MM-dd HH:mm:ss).", + schema = Schema(type = "string", example = "2042-01-01 07:15:00") + ) expires: Date, + @Parameter(hidden = true) authentication: Authentication? + ) = authentication?.let { + when (permission) { + EndpointPermission.READ, EndpointPermission.WRITE, EndpointPermission.CONFIGURE -> endpointGroupRepository.findByGroupName(endpointGroupName).orElseThrow { + throw CloudioHttpExceptions.NotFound("User Group not found.") + }.let { endpointGroup -> + accessTokenManager.generateEndpointGroupPermissionAccessToken(it.userDetails(), endpointGroup.id, permission, expires) + } + else -> throw CloudioHttpExceptions.BadRequest("Token can only be generated for READ, WRITE and CONFIGURE permission.") + } + } ?: throw CloudioHttpExceptions.Forbidden("User not found.") + + @PutMapping("/{endpointGroupName}/grant/**") @ResponseStatus(HttpStatus.NO_CONTENT) @Operation(summary = "Grant permission to element of endpoint group's data model to another user or user group.") diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt index 9174634..699397e 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointPermissionController.kt @@ -1,6 +1,7 @@ package ch.hevs.cloudio.cloud.restapi.endpoint.permission import ch.hevs.cloudio.cloud.dao.* +import ch.hevs.cloudio.cloud.extension.userDetails import ch.hevs.cloudio.cloud.restapi.CloudioHttpExceptions import ch.hevs.cloudio.cloud.security.AccessTokenManager import ch.hevs.cloudio.cloud.security.EndpointModelElementPermission @@ -18,6 +19,7 @@ import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.Authentication import org.springframework.util.AntPathMatcher import org.springframework.web.bind.annotation.* import java.util.* @@ -119,13 +121,15 @@ class EndpointPermissionController( ) fun getAccessTokenByUUID( @PathVariable @Parameter(description = "UUID of endpoint.", required = true) uuid: UUID, - @RequestParam @Parameter( - description = "Permission to grant.", - schema = Schema(allowableValues = ["READ", "WRITE", "CONFIGURE"]) - ) permission: EndpointPermission) = when(permission) { - EndpointPermission.READ, EndpointPermission.WRITE, EndpointPermission.CONFIGURE -> accessTokenManager.generateEndpointPermissionAccessToken(uuid, permission) + @RequestParam @Parameter(description = "Permission to grant.", schema = Schema(allowableValues = ["READ", "WRITE", "CONFIGURE"])) permission: EndpointPermission, + @RequestParam @Parameter(description = "Expiration date and time for the token in ISO-8601 format (yyyy-MM-dd HH:mm:ss).", schema = Schema(type = "string", example = "2042-01-01 07:15:00")) expires: Date, + @Parameter(hidden = true) authentication: Authentication? + ) = authentication?.let { + when (permission) { + EndpointPermission.READ, EndpointPermission.WRITE, EndpointPermission.CONFIGURE -> accessTokenManager.generateEndpointPermissionAccessToken(it.userDetails(), uuid, permission, expires) else -> throw CloudioHttpExceptions.BadRequest("Token can only be generated for READ, WRITE and CONFIGURE permission.") } + } ?: throw CloudioHttpExceptions.Forbidden("User not found.") @PutMapping("/{uuid}/grant/**") @ResponseStatus(HttpStatus.NO_CONTENT) diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt index 1babc53..0128207 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenFilter.kt @@ -14,25 +14,33 @@ import javax.servlet.http.HttpServletResponse @Component class AccessTokenFilter( private val accessTokenManager: AccessTokenManager, - private val userDetailsService: CloudioUserDetailsService -): OncePerRequestFilter() { +) : OncePerRequestFilter() { override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { request.getHeader("Authorization").let { header -> if (header != null && header.isNotEmpty() && header.startsWith("Bearer")) { header.split(" ").getOrNull(1)?.trim()?.let { token -> accessTokenManager.validate(token).let { result -> - when(result) { - is AccessTokenManager.ValidUserToken -> userDetailsService.loadUserByID(result.userId).let { userDetails -> - SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities).apply { - details = WebAuthenticationDetailsSource().buildDetails(request) - } - } + try { + when (result) { + is AccessTokenManager.ValidUserToken -> SecurityContextHolder.getContext().authentication = + UsernamePasswordAuthenticationToken(result.userDetails, null, result.userDetails.authorities).apply { + details = WebAuthenticationDetailsSource().buildDetails(request) + } + + is AccessTokenManager.ValidEndpointPermissionToken -> SecurityContextHolder.getContext().authentication = + AnonymousAuthenticationToken(result.hashCode().toString(), result, listOf(Authority.HTTP_ACCESS).map { SimpleGrantedAuthority(it.name) }).apply { + details = WebAuthenticationDetailsSource().buildDetails(request) + } - is AccessTokenManager.ValidEndpointPermissionToken -> - SecurityContextHolder.getContext().authentication = AnonymousAuthenticationToken(result.hashCode().toString(), result, listOf(Authority.HTTP_ACCESS).map { SimpleGrantedAuthority(it.name) }) - is AccessTokenManager.InvalidToken -> {} - } + is AccessTokenManager.ValidEndpointGroupPermissionToken -> SecurityContextHolder.getContext().authentication = + AnonymousAuthenticationToken(result.hashCode().toString(), result, listOf(Authority.HTTP_ACCESS).map { SimpleGrantedAuthority(it.name) }).apply { + details = WebAuthenticationDetailsSource().buildDetails(request) + } + + else -> {} + } + } catch (_: Exception) { } } } } diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt index df96c22..e6b3cfe 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/AccessTokenManager.kt @@ -3,17 +3,23 @@ package ch.hevs.cloudio.cloud.security import io.jsonwebtoken.* import org.apache.juli.logging.LogFactory import org.springframework.beans.factory.annotation.Value +import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Service import java.util.* import javax.annotation.PostConstruct @Service -class AccessTokenManager { +class AccessTokenManager( + private val userDetailsService: CloudioUserDetailsService, + private val permissionManager: CloudioPermissionManager +) { sealed interface ValidationResult object InvalidToken: ValidationResult class ValidProvisioningToken(val endpointUUID: UUID): ValidationResult - class ValidUserToken(val userId: Long): ValidationResult + class ValidUserToken(val userDetails: CloudioUserDetails): ValidationResult data class ValidEndpointPermissionToken(val endpointUUID: UUID, val permission: EndpointPermission): ValidationResult + data class ValidEndpointGroupPermissionToken(val groupName: String, val permission: EndpointPermission): ValidationResult + private val log = LogFactory.getLog(AccessTokenManager::class.java) @Value("\${cloudio.jwt.secret:#{null}}") @@ -36,7 +42,6 @@ class AccessTokenManager { fun generateProvisionToken(endpointUUID: UUID, expirationDuration: Long = tokenExpirationDuration): String = Jwts.builder() .setSubject("provision") .claim("endpoint", endpointUUID.toString()) - .setIssuer("cloud.iO") .setIssuedAt(Date()) .setExpiration(Date(System.currentTimeMillis() + expirationDuration)) .signWith(SignatureAlgorithm.HS512, secretKey) @@ -45,19 +50,28 @@ class AccessTokenManager { fun generateUserAccessToken(user: CloudioUserDetails): String = Jwts.builder() .setSubject("user") .claim("uid", user.id.toString()) - .setIssuer("cloud.iO") .setIssuedAt(Date()) .setExpiration(Date(System.currentTimeMillis() + tokenExpirationDuration)) .signWith(SignatureAlgorithm.HS512, secretKey) .compact() - fun generateEndpointPermissionAccessToken(endpointUUID: UUID, permission: EndpointPermission): String = Jwts.builder() + fun generateEndpointPermissionAccessToken(user: CloudioUserDetails, endpointUUID: UUID, permission: EndpointPermission, expires: Date = Date(System.currentTimeMillis() + tokenExpirationDuration)): String = Jwts.builder() .setSubject("endpoint") .claim("uuid", endpointUUID.toString()) .claim("perm", permission) - .setIssuer("cloud.iO") + .setIssuer(user.id.toString()) .setIssuedAt(Date()) - .setExpiration(Date(System.currentTimeMillis() + tokenExpirationDuration)) + .setExpiration(expires) + .signWith(SignatureAlgorithm.HS512, secretKey) + .compact() + + fun generateEndpointGroupPermissionAccessToken(user: CloudioUserDetails, groupName: String, permission: EndpointPermission, expires: Date = Date(System.currentTimeMillis() + tokenExpirationDuration)): String = Jwts.builder() + .setSubject("endpoint-group") + .claim("group", groupName) + .claim("perm", permission) + .setIssuer(user.id.toString()) + .setIssuedAt(Date()) + .setExpiration(expires) .signWith(SignatureAlgorithm.HS512, secretKey) .compact() @@ -66,16 +80,37 @@ class AccessTokenManager { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).let { claims -> when (claims.body.subject) { "provision" -> ValidProvisioningToken(UUID.fromString(claims.body["endpoint"] as String)) - "user" -> ValidUserToken((claims.body["uid"] as String).toLong()) + + "user" -> ValidUserToken(userDetailsService.loadUserByID((claims.body["uid"] as String).toLong())) + "endpoint" -> { - EndpointPermission.valueOf(claims.body["perm"] as String).let { permission -> - when(permission) { - EndpointPermission.READ, EndpointPermission.WRITE, EndpointPermission.CONFIGURE -> - ValidEndpointPermissionToken(UUID.fromString(claims.body["uuid"] as String), permission) - else -> { - log.warn("Token for endpoint access contains invalid permission") - InvalidToken - } + val endpointUUID = UUID.fromString(claims.body["uuid"] as String) + val permission = EndpointPermission.valueOf(claims.body["perm"] as String) + val userDetails = userDetailsService.loadUserByID(claims.body.issuer.toLong()) + + when { + permissionManager.hasEndpointPermission(userDetails, endpointUUID, EndpointPermission.GRANT) && + setOf(EndpointPermission.READ, EndpointPermission.WRITE, EndpointPermission.CONFIGURE).contains(permission) -> + ValidEndpointPermissionToken(UUID.fromString(claims.body["uuid"] as String), permission) + else -> { + log.warn("Token for endpoint access contains invalid permission or issues has no GRANT permission anymore") + InvalidToken + } + } + } + + "endpoint-group" -> { + val endpointGroupName = claims.body["group"] as String + val permission = EndpointPermission.valueOf(claims.body["perm"] as String) + val userDetails = userDetailsService.loadUserByID(claims.body.issuer.toLong()) + + when { + permissionManager.hasEndpointGroupPermission(userDetails, endpointGroupName, EndpointPermission.GRANT) && + setOf(EndpointPermission.READ, EndpointPermission.WRITE, EndpointPermission.CONFIGURE).contains(permission) -> + ValidEndpointGroupPermissionToken(endpointGroupName, permission) + else -> { + log.warn("Token for endpoint access contains invalid permission or issues has no GRANT permission anymore") + InvalidToken } } } @@ -97,6 +132,8 @@ class AccessTokenManager { log.warn("Signature validation failed") } catch (exception: NumberFormatException) { log.warn("Invalid user ID in token") + } catch (exception: UsernameNotFoundException) { + log.warn("User not found") } return InvalidToken diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt index 40b0fd0..1b58348 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt @@ -11,14 +11,14 @@ import java.util.* @Service class CloudioPermissionManager( - private val userEndpointPermissionRepository: UserEndpointPermissionRepository, - private val userGroupEndpointPermissionRepository: UserGroupEndpointPermissionRepository, - private val endpointGroupRepository: EndpointGroupRepository, - private val userEndpointGroupPermissionRepository: UserEndpointGroupPermissionRepository, - private val endpointRepository: EndpointRepository, - private val userEndpointGroupModelElementPermissionRepository: UserEndpointGroupPermissionRepository, - private val userGroupEndpointGroupPermissionRepository: UserGroupEndpointGroupPermissionRepository, - private val userGroupEndpointGroupModelElementPermissionRepository: UserGroupEndpointGroupPermissionRepository + private val userEndpointPermissionRepository: UserEndpointPermissionRepository, + private val userGroupEndpointPermissionRepository: UserGroupEndpointPermissionRepository, + private val endpointGroupRepository: EndpointGroupRepository, + private val userEndpointGroupPermissionRepository: UserEndpointGroupPermissionRepository, + private val endpointRepository: EndpointRepository, + private val userEndpointGroupModelElementPermissionRepository: UserEndpointGroupPermissionRepository, + private val userGroupEndpointGroupPermissionRepository: UserGroupEndpointGroupPermissionRepository, + private val userGroupEndpointGroupModelElementPermissionRepository: UserGroupEndpointGroupPermissionRepository // The fact that this is not used lets me suspect that there might be a bug... ) : PermissionEvaluator { private val log = LogFactory.getLog(CloudioPermissionManager::class.java) @@ -28,7 +28,16 @@ class CloudioPermissionManager( subject is UUID && permission is EndpointPermission && authentication.principal is AccessTokenManager.ValidEndpointPermissionToken -> (authentication.principal as AccessTokenManager.ValidEndpointPermissionToken).let { token -> - subject == token.endpointUUID && token.permission.fulfills(permission) + token.permission.fulfills(permission) && subject == token.endpointUUID + } + + subject is UUID && permission is EndpointPermission && authentication.principal is AccessTokenManager.ValidEndpointGroupPermissionToken -> + (authentication.principal as AccessTokenManager.ValidEndpointGroupPermissionToken).let { token -> + token.permission.fulfills(permission) && endpointGroupRepository.findByGroupName(token.groupName).let { group -> + group.isPresent && endpointRepository.findByGroupMembershipsContains(group.get()).any { + subject == it.uuid + } + } } subject is String && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> try { @@ -55,9 +64,17 @@ class CloudioPermissionManager( } override fun hasPermission(authentication: Authentication, targetId: Serializable?, targetType: String?, permission: Any?) = when { - targetType == "Endpoint" && targetId is UUID && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> hasEndpointPermission(authentication.principal as CloudioUserDetails, targetId, permission) - targetType == "Model" && targetId is ModelIdentifier && permission is EndpointModelElementPermission && authentication.principal is CloudioUserDetails -> hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, targetId, permission) - targetType == "EndpointGroup" && targetId is String && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> hasEndpointGroupPermission(authentication.principal as CloudioUserDetails, targetId, permission) + targetType == "Endpoint" && targetId is UUID && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> + hasEndpointPermission(authentication.principal as CloudioUserDetails, targetId, permission) + + targetType == "Model" && targetId is ModelIdentifier && permission is EndpointModelElementPermission && authentication.principal is CloudioUserDetails -> + hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, targetId, permission + ) + + targetType == "EndpointGroup" && targetId is String && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> + hasEndpointGroupPermission(authentication.principal as CloudioUserDetails, targetId, permission + ) + else -> { log.error("Unknown permission request: Authentication = $authentication, targetId = $targetId, permission = $permission") false @@ -82,8 +99,8 @@ class CloudioPermissionManager( return hasEndpointModelElementPermission(userDetails, modelID, permission, endpointPermission, permissionList) } - private fun hasEndpointModelElementPermission(userDetails: CloudioUserDetails, modelID: ModelIdentifier, permission: EndpointModelElementPermission - , endpointPermission: EndpointPermission, permissionList: MutableList>): Boolean { + private fun hasEndpointModelElementPermission(userDetails: CloudioUserDetails, modelID: ModelIdentifier, permission: EndpointModelElementPermission, + endpointPermission: EndpointPermission, permissionList: MutableList>): Boolean { // Ensure that the model ID is correct. if (!modelID.valid) { log.warn("Permission $permission to \"$modelID \"rejected for user \"${userDetails.username}\": Invalid model identifier.") @@ -103,10 +120,12 @@ class CloudioPermissionManager( log.debug("Permission VIEW to \"$modelID\" granted for user \"${userDetails.username}\": BROWSE+ permission on endpoint.") return true } + EndpointModelElementPermission.READ -> if (endpointPermission.fulfills(EndpointPermission.READ)) { log.debug("Permission READ to \"$modelID\" granted for user \"${userDetails.username}\" READ+ permission on endpoint.") return true } + EndpointModelElementPermission.WRITE -> if (endpointPermission.fulfills(EndpointPermission.WRITE)) { log.debug("Permission WRITE to \"$modelID\" granted for user \"${userDetails.username}\" WRITE+ permission on endpoint.") return true @@ -118,7 +137,7 @@ class CloudioPermissionManager( var temp = modelID.toString() //if a @ is the first letter, delete the first element - if(temp.first().equals('@', true)){ + if (temp.first().equals('@', true)) { //drop the @something and the "/" temp = temp.dropWhile { !it.equals('/', ignoreCase = true) } temp = temp.drop(1) @@ -130,9 +149,9 @@ class CloudioPermissionManager( //the higher level permission is selected var p = EndpointModelElementPermission.DENY - for (i in 1..modelID.count()){ - permissionList.forEach(){ - if(it.key == temp && it.value.fulfills(p)){ + for (i in 1..modelID.count()) { + permissionList.forEach() { + if (it.key == temp && it.value.fulfills(p)) { p = it.value } } @@ -169,7 +188,7 @@ class CloudioPermissionManager( userEndpointGroupPermissionRepository.findByUserID(userDetails.id).forEach { userEndpointGroupPermission -> endpointGroupRepository.findById(userEndpointGroupPermission.endpointGroupID).ifPresent { endpointGroup -> endpointRepository.findByGroupMembershipsContains(endpointGroup).forEach { - if(it.uuid == endpointUUID){ + if (it.uuid == endpointUUID) { permission = permission.higher(userEndpointGroupPermission.permission) } } @@ -178,12 +197,12 @@ class CloudioPermissionManager( } //check user group on endpoint group - if (!permission.fulfills(EndpointPermission.GRANT)){ + if (!permission.fulfills(EndpointPermission.GRANT)) { userDetails.groupMembershipIDs.forEach { userGroupID -> userGroupEndpointGroupPermissionRepository.findByUserGroupID(userGroupID).forEach { userGroupEndpointGroupPermission -> endpointGroupRepository.findById(userGroupEndpointGroupPermission.endpointGroupID).ifPresent { endpointGroup -> endpointRepository.findByGroupMembershipsContains(endpointGroup).forEach { - if(it.uuid == endpointUUID){ + if (it.uuid == endpointUUID) { permission = permission.higher(userGroupEndpointGroupPermission.permission) } } @@ -195,8 +214,7 @@ class CloudioPermissionManager( return permission } - fun resolveEndpointGroupPermission(userDetails: CloudioUserDetails, endpointGroupName: String): EndpointPermission - { + fun resolveEndpointGroupPermission(userDetails: CloudioUserDetails, endpointGroupName: String): EndpointPermission { var permission = EndpointPermission.DENY endpointGroupRepository.findByGroupName(endpointGroupName).ifPresent { endpointGroup -> @@ -228,7 +246,7 @@ class CloudioPermissionManager( } else { groupPermission.modelPermissions.forEach { (key, permission) -> existingPermission.modelPermissions[key] = (existingPermission.modelPermissions[key] - ?: EndpointModelElementPermission.DENY).higher(permission) + ?: EndpointModelElementPermission.DENY).higher(permission) } } } else { @@ -239,9 +257,9 @@ class CloudioPermissionManager( // Add higher permissions from endpoint groups resolveEndpointGroupsPermissions(userDetails).forEach { userEndpointGroupPermission -> endpointGroupRepository.findById(userEndpointGroupPermission.endpointGroupID).ifPresent { endpointGroup -> - endpointRepository.findByGroupMembershipsContains(endpointGroup).forEach{ endpoint -> + endpointRepository.findByGroupMembershipsContains(endpointGroup).forEach { endpoint -> val existingPermission = permissions.find { it.endpointUUID == endpoint.uuid } - if(existingPermission != null){ + if (existingPermission != null) { existingPermission.permission = existingPermission.permission.higher(userEndpointGroupPermission.permission) if (existingPermission.permission.fulfills(EndpointPermission.WRITE)) { existingPermission.modelPermissions.clear() @@ -250,8 +268,7 @@ class CloudioPermissionManager( existingPermission.modelPermissions[key] = (existingPermission.modelPermissions[key] ?: EndpointModelElementPermission.DENY).higher(permission) } } - } - else{ + } else { permissions.add(UserEndpointPermission(userDetails.id, endpoint.uuid, userEndpointGroupPermission.permission)) } } @@ -261,13 +278,12 @@ class CloudioPermissionManager( return permissions } - fun resolveEndpointGroupsPermissions(userDetails: CloudioUserDetails): Collection - { + fun resolveEndpointGroupsPermissions(userDetails: CloudioUserDetails): Collection { val permissions = userEndpointGroupPermissionRepository.findByUserID(userDetails.id).toMutableList() userGroupEndpointGroupPermissionRepository.findByUserGroupIDIn(userDetails.groupMembershipIDs).forEach { userGroupEndpointGroupPermission -> - val existingPermission = permissions.find{it.endpointGroupID == userGroupEndpointGroupPermission.endpointGroupID} - if (existingPermission != null){ + val existingPermission = permissions.find { it.endpointGroupID == userGroupEndpointGroupPermission.endpointGroupID } + if (existingPermission != null) { existingPermission.permission = existingPermission.permission.higher(userGroupEndpointGroupPermission.permission) if (existingPermission.permission.fulfills(EndpointPermission.WRITE)) { existingPermission.modelPermissions.clear() @@ -276,8 +292,7 @@ class CloudioPermissionManager( existingPermission.modelPermissions[key] = (existingPermission.modelPermissions[key] ?: EndpointModelElementPermission.DENY).higher(permission) } } - } - else{ + } else { permissions.add(UserEndpointGroupPermission(userDetails.id, userGroupEndpointGroupPermission.endpointGroupID, userGroupEndpointGroupPermission.permission)) } } @@ -285,7 +300,7 @@ class CloudioPermissionManager( return permissions } - fun getAllEndpointModelElementPermissions(userDetails:CloudioUserDetails, endpointUUID:UUID): MutableList> { + fun getAllEndpointModelElementPermissions(userDetails: CloudioUserDetails, endpointUUID: UUID): MutableList> { val allPermissionsList = mutableListOf>() var addedPermissions = mutableMapOf() @@ -303,16 +318,15 @@ class CloudioPermissionManager( userGroupEndpointPermission.modelPermissions.forEach { modelPermission -> //add the groups permissions //if a permission for an element already exists, keep the highest permissions level - if(!addedPermissions.containsKey(modelPermission.key)){ + if (!addedPermissions.containsKey(modelPermission.key)) { allPermissionsList.add(modelPermission) - addedPermissions[modelPermission.key]=modelPermission.value - } - else if(modelPermission.value.higher(addedPermissions.getOrDefault(modelPermission.key, EndpointModelElementPermission.DENY)) == modelPermission.value){ + addedPermissions[modelPermission.key] = modelPermission.value + } else if (modelPermission.value.higher(addedPermissions.getOrDefault(modelPermission.key, EndpointModelElementPermission.DENY)) == modelPermission.value) { allPermissionsList.forEach { - if(it.key == modelPermission.key){ + if (it.key == modelPermission.key) { allPermissionsList.remove(it) allPermissionsList.add(modelPermission) - addedPermissions[modelPermission.key]=modelPermission.value + addedPermissions[modelPermission.key] = modelPermission.value } } } @@ -324,17 +338,16 @@ class CloudioPermissionManager( endpointRepository.findById(endpointUUID).ifPresent { endpoint -> endpoint.groupMemberships.forEach { endpointGroup -> userEndpointGroupModelElementPermissionRepository.findByUserIDAndEndpointGroupID(userDetails.id, endpointGroup.id).ifPresent { userEndpointGroupPermission -> - userEndpointGroupPermission.modelPermissions.forEach{ modelPermission -> - if(!addedPermissions.containsKey(modelPermission.key)){ + userEndpointGroupPermission.modelPermissions.forEach { modelPermission -> + if (!addedPermissions.containsKey(modelPermission.key)) { allPermissionsList.add(modelPermission) - addedPermissions[modelPermission.key]=modelPermission.value - } - else if(modelPermission.value.higher(addedPermissions.getOrDefault(modelPermission.key, EndpointModelElementPermission.DENY)) == modelPermission.value){ + addedPermissions[modelPermission.key] = modelPermission.value + } else if (modelPermission.value.higher(addedPermissions.getOrDefault(modelPermission.key, EndpointModelElementPermission.DENY)) == modelPermission.value) { allPermissionsList.forEach { - if(it.key == modelPermission.key){ + if (it.key == modelPermission.key) { allPermissionsList.remove(it) allPermissionsList.add(modelPermission) - addedPermissions[modelPermission.key]=modelPermission.value + addedPermissions[modelPermission.key] = modelPermission.value } } } @@ -347,17 +360,16 @@ class CloudioPermissionManager( endpointRepository.findById(endpointUUID).ifPresent { endpoint -> endpoint.groupMemberships.forEach { endpointGroup -> userGroupEndpointGroupPermissionRepository.findByUserGroupIDIn(userDetails.groupMembershipIDs).forEach { userGroupEndpointGroupPermission -> - userGroupEndpointGroupPermission.modelPermissions.forEach{ modelPermission -> - if(!addedPermissions.containsKey(modelPermission.key)){ + userGroupEndpointGroupPermission.modelPermissions.forEach { modelPermission -> + if (!addedPermissions.containsKey(modelPermission.key)) { allPermissionsList.add(modelPermission) - addedPermissions[modelPermission.key]=modelPermission.value - } - else if(modelPermission.value.higher(addedPermissions.getOrDefault(modelPermission.key, EndpointModelElementPermission.DENY)) == modelPermission.value){ + addedPermissions[modelPermission.key] = modelPermission.value + } else if (modelPermission.value.higher(addedPermissions.getOrDefault(modelPermission.key, EndpointModelElementPermission.DENY)) == modelPermission.value) { allPermissionsList.forEach { - if(it.key == modelPermission.key){ + if (it.key == modelPermission.key) { allPermissionsList.remove(it) allPermissionsList.add(modelPermission) - addedPermissions[modelPermission.key]=modelPermission.value + addedPermissions[modelPermission.key] = modelPermission.value } } } @@ -367,7 +379,7 @@ class CloudioPermissionManager( } allPermissionsList.forEach { - if(it.key.endsWith("/#")){ + if (it.key.endsWith("/#")) { it.key.dropLast(2) } } @@ -381,36 +393,46 @@ class CloudioPermissionManager( /** * filter the given structure on the given EndpointModelElementPermission */ - fun filter(data: Any, cloudioUserDetails: CloudioUserDetails, modelIdentifier: ModelIdentifier, - permission: EndpointModelElementPermission): Any? { + fun filter(data: Any, cloudioUserDetails: CloudioUserDetails, modelIdentifier: ModelIdentifier, permission: EndpointModelElementPermission): Any? { val permissionList = getAllEndpointModelElementPermissions(cloudioUserDetails, modelIdentifier.endpoint) val endpointPermission = resolveEndpointPermission(cloudioUserDetails, modelIdentifier.endpoint) when (data) { is EndpointDataModel -> { - return filterEndpoint(data, cloudioUserDetails, - modelIdentifier, permission, endpointPermission, permissionList) + return filterEndpoint( + data, cloudioUserDetails, + modelIdentifier, permission, endpointPermission, permissionList + ) } + is Node -> { - return filterNode(data, cloudioUserDetails, - modelIdentifier, permission, endpointPermission, permissionList) + return filterNode( + data, cloudioUserDetails, + modelIdentifier, permission, endpointPermission, permissionList + ) } + is CloudioObject -> { - return filterObject(data, cloudioUserDetails, - modelIdentifier, permission, endpointPermission, permissionList) + return filterObject( + data, cloudioUserDetails, + modelIdentifier, permission, endpointPermission, permissionList + ) } + is Attribute -> { - return filterAttribute(data, cloudioUserDetails, - modelIdentifier, permission, endpointPermission, permissionList) + return filterAttribute( + data, cloudioUserDetails, + modelIdentifier, permission, endpointPermission, permissionList + ) } } return null } - private fun filterEndpoint(endpoint: EndpointDataModel, cloudioUserDetails: CloudioUserDetails, modelIdentifier: ModelIdentifier, - permission: EndpointModelElementPermission, endpointPermission: EndpointPermission, permissionList: MutableList>): EndpointDataModel? { + private fun filterEndpoint(endpoint: EndpointDataModel, cloudioUserDetails: CloudioUserDetails, modelIdentifier: ModelIdentifier, permission: EndpointModelElementPermission, + endpointPermission: EndpointPermission, permissionList: MutableList>): EndpointDataModel? { val e = EndpointDataModel() e.messageFormatVersion = endpoint.messageFormatVersion e.supportedFormats = endpoint.supportedFormats @@ -432,11 +454,14 @@ class CloudioPermissionManager( } - private fun filterNode(node: Node, cloudioUserDetails: CloudioUserDetails, modelIdentifier: ModelIdentifier, - permission: EndpointModelElementPermission, endpointPermission: EndpointPermission, permissionList: MutableList>): Node? { + private fun filterNode(node: Node, cloudioUserDetails: CloudioUserDetails, modelIdentifier: ModelIdentifier, permission: EndpointModelElementPermission, endpointPermission: EndpointPermission, + permissionList: MutableList>): Node? { - if (hasEndpointModelElementPermission(cloudioUserDetails, - modelIdentifier, permission, endpointPermission, permissionList)) { + if (hasEndpointModelElementPermission( + cloudioUserDetails, + modelIdentifier, permission, endpointPermission, permissionList + ) + ) { return node } val n = Node() @@ -445,7 +470,7 @@ class CloudioPermissionManager( node.objects.forEach { val objId = ModelIdentifier(modelIdentifier.toString() + "/" + it.key) - val temp = filterObject(it.value, cloudioUserDetails, objId, permission, endpointPermission, permissionList) + val temp = filterObject(it.value, cloudioUserDetails, objId, permission, endpointPermission, permissionList) if (temp is CloudioObject) { n.objects[it.key] = temp } @@ -458,11 +483,14 @@ class CloudioPermissionManager( return null } - private fun filterObject(obj: CloudioObject, cloudioUserDetails: CloudioUserDetails, modelIdentifier: ModelIdentifier, - permission: EndpointModelElementPermission, endpointPermission: EndpointPermission, permissionList: MutableList>): CloudioObject? { + private fun filterObject(obj: CloudioObject, cloudioUserDetails: CloudioUserDetails, modelIdentifier: ModelIdentifier, permission: EndpointModelElementPermission, + endpointPermission: EndpointPermission, permissionList: MutableList>): CloudioObject? { - if (hasEndpointModelElementPermission(cloudioUserDetails, - modelIdentifier, permission, endpointPermission, permissionList)) { + if (hasEndpointModelElementPermission( + cloudioUserDetails, + modelIdentifier, permission, endpointPermission, permissionList + ) + ) { return obj } @@ -471,7 +499,7 @@ class CloudioPermissionManager( obj.objects.forEach { val objId = ModelIdentifier(modelIdentifier.toString() + "/" + it.key) - val temp = filterObject(it.value, cloudioUserDetails, objId, permission, endpointPermission, permissionList) + val temp = filterObject(it.value, cloudioUserDetails, objId, permission, endpointPermission, permissionList) if (temp is CloudioObject) { o.objects[it.key] = temp } @@ -479,7 +507,7 @@ class CloudioPermissionManager( obj.attributes.forEach { val attrId = ModelIdentifier(modelIdentifier.toString() + "/" + it.key) - val temp = filterAttribute(it.value, cloudioUserDetails, attrId, permission, endpointPermission, permissionList) + val temp = filterAttribute(it.value, cloudioUserDetails, attrId, permission, endpointPermission, permissionList) if (temp is Attribute) { o.attributes[it.key] = temp } @@ -492,11 +520,14 @@ class CloudioPermissionManager( return null } - private fun filterAttribute(attribute: Attribute, cloudioUserDetails: CloudioUserDetails, modelIdentifier: ModelIdentifier, - permission: EndpointModelElementPermission, endpointPermission: EndpointPermission, permissionList: MutableList>): Attribute? { + private fun filterAttribute(attribute: Attribute, cloudioUserDetails: CloudioUserDetails, modelIdentifier: ModelIdentifier, permission: EndpointModelElementPermission, + endpointPermission: EndpointPermission, permissionList: MutableList>): Attribute? { - if (hasEndpointModelElementPermission(cloudioUserDetails, - modelIdentifier, permission, endpointPermission, permissionList)) { + if (hasEndpointModelElementPermission( + cloudioUserDetails, + modelIdentifier, permission, endpointPermission, permissionList + ) + ) { return attribute } @@ -508,29 +539,32 @@ class CloudioPermissionManager( * if an element does not exist in data, add it from noDataStructure */ fun merge(data: Any?, noDataStructure: Any?): Any? { - if(data == null){ + if (data == null) { return noDataStructure } - if(noDataStructure == null){ + if (noDataStructure == null) { return data } when (data) { is EndpointDataModel -> { - if (noDataStructure is EndpointDataModel){ + if (noDataStructure is EndpointDataModel) { return mergeEndpoint(data, noDataStructure) } } + is Node -> { - if (noDataStructure is Node){ + if (noDataStructure is Node) { return mergeNode(data, noDataStructure) } } + is CloudioObject -> { - if (noDataStructure is CloudioObject){ + if (noDataStructure is CloudioObject) { return mergeObject(data, noDataStructure) } } + is Attribute -> { return data } diff --git a/src/main/resources/application-rest-api.yml b/src/main/resources/application-rest-api.yml index 547c521..f384ea9 100644 --- a/src/main/resources/application-rest-api.yml +++ b/src/main/resources/application-rest-api.yml @@ -1,3 +1,6 @@ spring: main: - web-application-type: servlet \ No newline at end of file + web-application-type: servlet + mvc: + format: + date: iso \ No newline at end of file From 28f9b1703d681140598af14c561c22e47c043d86 Mon Sep 17 00:00:00 2001 From: Michael Clausen Date: Sat, 17 Sep 2022 19:22:47 +0200 Subject: [PATCH 7/9] ot rid of the annoying login window in browsers when using REST API --- src/main/kotlin/ch/hevs/cloudio/cloud/main.kt | 3 ++- .../endpoint/permission/EndpointGroupPermissionController.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/main.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/main.kt index 0817d62..ce49da0 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/main.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/main.kt @@ -40,6 +40,7 @@ import java.net.InetAddress import java.util.* import javax.net.ssl.KeyManagerFactory import javax.net.ssl.TrustManagerFactory +import javax.servlet.http.HttpServletResponse @SpringBootApplication @@ -175,7 +176,7 @@ class CloudioApplication { "/api/v1/provision/*", "/messageformat/**", "/api/v1/auth/login" ).permitAll() .anyRequest().hasAuthority(Authority.HTTP_ACCESS.name) - .and().httpBasic() + .and().httpBasic().authenticationEntryPoint { _, response, authException -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.message) } .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) http.addFilterBefore(accessTokenFilter, UsernamePasswordAuthenticationFilter::class.java) } diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt index 237107f..b1d2fe1 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/permission/EndpointGroupPermissionController.kt @@ -156,7 +156,7 @@ class EndpointGroupPermissionController( EndpointPermission.READ, EndpointPermission.WRITE, EndpointPermission.CONFIGURE -> endpointGroupRepository.findByGroupName(endpointGroupName).orElseThrow { throw CloudioHttpExceptions.NotFound("User Group not found.") }.let { endpointGroup -> - accessTokenManager.generateEndpointGroupPermissionAccessToken(it.userDetails(), endpointGroup.id, permission, expires) + accessTokenManager.generateEndpointGroupPermissionAccessToken(it.userDetails(), endpointGroup.groupName, permission, expires) } else -> throw CloudioHttpExceptions.BadRequest("Token can only be generated for READ, WRITE and CONFIGURE permission.") } From 13bf978b82bc39a07a39f3c16dbaf81587b5d0fa Mon Sep 17 00:00:00 2001 From: Michael Clausen Date: Mon, 5 Dec 2022 15:01:21 +0100 Subject: [PATCH 8/9] Added back support for token authentication on history API --- .../EndpointHistoryAccessController.kt | 68 +++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt index 1d2533f..211d614 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/restapi/endpoint/history/EndpointHistoryAccessController.kt @@ -6,9 +6,7 @@ import ch.hevs.cloudio.cloud.extension.userDetails import ch.hevs.cloudio.cloud.model.ActionIdentifier import ch.hevs.cloudio.cloud.model.ModelIdentifier import ch.hevs.cloudio.cloud.restapi.CloudioHttpExceptions -import ch.hevs.cloudio.cloud.security.CloudioPermissionManager -import ch.hevs.cloudio.cloud.security.EndpointModelElementPermission -import ch.hevs.cloudio.cloud.security.EndpointPermission +import ch.hevs.cloudio.cloud.security.* import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.media.ArraySchema @@ -17,6 +15,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.security.SecurityRequirements import io.swagger.v3.oas.annotations.tags.Tag import org.influxdb.InfluxDB import org.influxdb.dto.Query @@ -24,7 +23,8 @@ import org.influxdb.dto.QueryResult import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus import org.springframework.http.MediaType -import org.springframework.security.core.Authentication +import org.springframework.security.core.annotation.CurrentSecurityContext +import org.springframework.security.core.context.SecurityContext import org.springframework.util.AntPathMatcher import org.springframework.web.bind.annotation.* import java.text.SimpleDateFormat @@ -35,12 +35,18 @@ import javax.servlet.http.HttpServletRequest @Profile("rest-api") @Tag(name = "Endpoint History Access", description = "Allows an user to access time series data of endpoints.") @RequestMapping("/api/v1/history") -@SecurityRequirement(name = "basicAuth") +@SecurityRequirements( + value = [ + SecurityRequirement(name = "basicAuth"), + SecurityRequirement(name = "tokenAuth") + ] +) class EndpointHistoryAccessController( private val endpointRepository: EndpointRepository, private val permissionManager: CloudioPermissionManager, private val influx: InfluxDB, - private val influxProperties: CloudioInfluxProperties + private val influxProperties: CloudioInfluxProperties, + private val userDetailsService: CloudioUserDetailsService ) { private val antMatcher = AntPathMatcher() @@ -61,7 +67,7 @@ class EndpointHistoryAccessController( ] ) fun getModelElement( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) @CurrentSecurityContext context: SecurityContext, @RequestParam @Parameter(description = "Optional start date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no start date (all data).", required = false) from: String?, @RequestParam @Parameter(description = "Optional end date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no end date (all data).", required = false) to: String?, @RequestParam @Parameter( @@ -91,13 +97,24 @@ class EndpointHistoryAccessController( throw CloudioHttpExceptions.NotFound("Endpoint not found.") } - // Check if user has READ access on the endpoint - if(!permissionManager.resolveEndpointPermission(authentication.userDetails(), modelIdentifier.endpoint) + val authentication = context.authentication + when (authentication.principal) { + is CloudioUserDetails -> { + // Check if user has READ access to the endpoint + if (!permissionManager.resolveEndpointPermission(authentication.userDetails(), modelIdentifier.endpoint) .fulfills(EndpointPermission.READ)) - { - // Check if user has access to the attribute. - if (!permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.READ)) { - throw CloudioHttpExceptions.Forbidden("Forbidden.") + { + // Check if user has access to the attribute. + if (!permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.READ)) { + throw CloudioHttpExceptions.Forbidden("Forbidden.") + } + } + } + + is AccessTokenManager.ValidEndpointPermissionToken, is AccessTokenManager.ValidEndpointGroupPermissionToken -> { + if (!permissionManager.hasPermission(authentication, modelIdentifier.endpoint, EndpointPermission.READ)) { + throw CloudioHttpExceptions.Forbidden("Forbidden.") + } } } @@ -126,7 +143,7 @@ class EndpointHistoryAccessController( ] ) fun getModelElementAsCSV( - @Parameter(hidden = true) authentication: Authentication, + @Parameter(hidden = true) @CurrentSecurityContext context: SecurityContext, @RequestParam @Parameter(description = "Optional start date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no start date (all data).", required = false) from: String?, @RequestParam @Parameter(description = "Optional end date (UTC) in the format 'yyyy-MM-ddTHH:mm:ss'. Default is to no end date (all data).", required = false) to: String?, @RequestParam @Parameter( @@ -157,13 +174,24 @@ class EndpointHistoryAccessController( throw CloudioHttpExceptions.NotFound("Endpoint not found.") } - // Check if user has READ access on the endpoint - if(!permissionManager.resolveEndpointPermission(authentication.userDetails(), modelIdentifier.endpoint) + val authentication = context.authentication + when (authentication.principal) { + is CloudioUserDetails -> { + // Check if user has READ access to the endpoint + if (!permissionManager.resolveEndpointPermission(authentication.userDetails(), modelIdentifier.endpoint) .fulfills(EndpointPermission.READ)) - { - // Check if user has access to the attribute. - if (!permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.READ)) { - throw CloudioHttpExceptions.Forbidden("Forbidden.") + { + // Check if user has access to the attribute. + if (!permissionManager.hasEndpointModelElementPermission(authentication.userDetails(), modelIdentifier, EndpointModelElementPermission.READ)) { + throw CloudioHttpExceptions.Forbidden("Forbidden.") + } + } + } + + is AccessTokenManager.ValidEndpointPermissionToken, is AccessTokenManager.ValidEndpointGroupPermissionToken -> { + if (!permissionManager.hasPermission(authentication, modelIdentifier.endpoint, EndpointPermission.READ)) { + throw CloudioHttpExceptions.Forbidden("Forbidden.") + } } } From 323cd92584be7c89be6f2f5f26507ca7464e7fc1 Mon Sep 17 00:00:00 2001 From: Michael Clausen Date: Mon, 5 Dec 2022 15:35:18 +0100 Subject: [PATCH 9/9] Added back support for token authentication for CloudioPermissionManager --- .../security/CloudioPermissionManager.kt | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt b/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt index dfb6f37..9f0ad4a 100644 --- a/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt +++ b/src/main/kotlin/ch/hevs/cloudio/cloud/security/CloudioPermissionManager.kt @@ -23,20 +23,40 @@ class CloudioPermissionManager( private val log = LogFactory.getLog(CloudioPermissionManager::class.java) override fun hasPermission(authentication: Authentication, subject: Any?, permission: Any?) = when { - subject is UUID && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> hasEndpointPermission(authentication.principal as CloudioUserDetails, subject, permission) + subject is UUID && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> + hasEndpointPermission(authentication.principal as CloudioUserDetails, subject, permission) + + subject is UUID && permission is EndpointPermission && authentication.principal is AccessTokenManager.ValidEndpointPermissionToken -> + (authentication.principal as AccessTokenManager.ValidEndpointPermissionToken).let { token -> + token.permission.fulfills(permission) && subject == token.endpointUUID + } + + subject is UUID && permission is EndpointPermission && authentication.principal is AccessTokenManager.ValidEndpointGroupPermissionToken -> + (authentication.principal as AccessTokenManager.ValidEndpointGroupPermissionToken).let { token -> + token.permission.fulfills(permission) && endpointGroupRepository.findByGroupName(token.groupName).let { group -> + group.isPresent && endpointRepository.findByGroupMembershipsContains(group.get()).any { + subject == it.uuid + } + } + } + subject is String && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> try { hasEndpointPermission(authentication.principal as CloudioUserDetails, UUID.fromString(subject), permission) } catch (e: Exception) { log.error("Invalid endpoint UUID: Authentication = $authentication, subject = $subject, permission = $permission") false } - subject is ModelIdentifier && permission is EndpointModelElementPermission && authentication.principal is CloudioUserDetails -> hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, subject, permission) + + subject is ModelIdentifier && permission is EndpointModelElementPermission && authentication.principal is CloudioUserDetails -> + hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, subject, permission) + subject is String && permission is EndpointModelElementPermission && authentication.principal is CloudioUserDetails -> ModelIdentifier(subject).let { - if (it.valid) hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, it, permission) else { - log.error("Invalid model identifier: Authentication = $authentication, subject = $subject, permission = $permission") - false + if (it.valid) hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, it, permission) else { + log.error("Invalid model identifier: Authentication = $authentication, subject = $subject, permission = $permission") + false + } } - } + else -> { log.error("Unknown permission request: Authentication = $authentication, subject = $subject, permission = $permission") false @@ -44,9 +64,17 @@ class CloudioPermissionManager( } override fun hasPermission(authentication: Authentication, targetId: Serializable?, targetType: String?, permission: Any?) = when { - targetType == "Endpoint" && targetId is UUID && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> hasEndpointPermission(authentication.principal as CloudioUserDetails, targetId, permission) - targetType == "Model" && targetId is ModelIdentifier && permission is EndpointModelElementPermission && authentication.principal is CloudioUserDetails -> hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, targetId, permission) - targetType == "EndpointGroup" && targetId is String && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> hasEndpointGroupPermission(authentication.principal as CloudioUserDetails, targetId, permission) + targetType == "Endpoint" && targetId is UUID && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> + hasEndpointPermission(authentication.principal as CloudioUserDetails, targetId, permission) + + targetType == "Model" && targetId is ModelIdentifier && permission is EndpointModelElementPermission && authentication.principal is CloudioUserDetails -> + hasEndpointModelElementPermission(authentication.principal as CloudioUserDetails, targetId, permission + ) + + targetType == "EndpointGroup" && targetId is String && permission is EndpointPermission && authentication.principal is CloudioUserDetails -> + hasEndpointGroupPermission(authentication.principal as CloudioUserDetails, targetId, permission + ) + else -> { log.error("Unknown permission request: Authentication = $authentication, targetId = $targetId, permission = $permission") false