Skip to content
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -86,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())
Expand Down
28 changes: 0 additions & 28 deletions src/main/kotlin/ch/hevs/cloudio/cloud/dao/ProvisionToken.kt

This file was deleted.

This file was deleted.

23 changes: 15 additions & 8 deletions src/main/kotlin/ch/hevs/cloudio/cloud/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -34,13 +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
import javax.servlet.http.HttpServletResponse


@SpringBootApplication
@ConfigurationPropertiesScan
Expand All @@ -59,6 +60,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()
Expand Down Expand Up @@ -157,7 +163,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)
}
Expand All @@ -167,11 +173,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().httpBasic().authenticationEntryPoint { _, response, authException -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.message) }
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
http.addFilterBefore(accessTokenFilter, UsernamePasswordAuthenticationFilter::class.java)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -37,14 +41,17 @@ 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 {
@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(
Expand All @@ -59,17 +66,21 @@ 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,
@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 {
Expand All @@ -84,30 +95,37 @@ 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 {
@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()

@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,
@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 {
Expand All @@ -123,30 +141,37 @@ 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 {
@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

@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,
@Parameter(hidden = true) authentication: Authentication?,
@RequestBody @Parameter(description = "User's metadata.", schema = Schema(type = "object", example = "{\"location\": \"Sion\", \"position\": \"Manager\"}"))
body: Map<String, Any>
) {
if (authentication == null) throw CloudioHttpExceptions.Forbidden("No user.")

userRepository.findById(authentication.userDetails().id).orElseThrow {
CloudioHttpExceptions.NotFound("User not found.")
}.let {
Expand All @@ -159,13 +184,16 @@ 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 {
@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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,7 +24,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
Loading