Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RC 4.2.5
- Loading branch information
Showing
83 changed files
with
854 additions
and
559 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
cam/src/main/kotlin/org/dreamexposure/discal/cam/business/SecurityService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package org.dreamexposure.discal.cam.business | ||
|
||
import org.dreamexposure.discal.core.business.ApiKeyService | ||
import org.dreamexposure.discal.core.business.SessionService | ||
import org.dreamexposure.discal.core.config.Config | ||
import org.dreamexposure.discal.core.extensions.isExpiredTtl | ||
import org.dreamexposure.discal.core.`object`.new.security.Scope | ||
import org.dreamexposure.discal.core.`object`.new.security.TokenType | ||
import org.springframework.http.HttpStatus | ||
import org.springframework.stereotype.Component | ||
|
||
@Component | ||
class SecurityService( | ||
private val sessionService: SessionService, | ||
private val apiKeyService: ApiKeyService, | ||
) { | ||
suspend fun authenticateAndAuthorizeToken(token: String, schemas: List<TokenType>, scopes: List<Scope>): Pair<HttpStatus, String> { | ||
if (!authenticateToken(token)) return Pair(HttpStatus.UNAUTHORIZED, "Unauthenticated") | ||
|
||
if (!validateTokenSchema(token, schemas)) return Pair(HttpStatus.UNAUTHORIZED, "Unsupported schema") | ||
|
||
if (!authorizeToken(token, scopes)) return Pair(HttpStatus.FORBIDDEN, "Access denied") | ||
|
||
return Pair(HttpStatus.OK, "Authorized") | ||
} | ||
|
||
suspend fun authenticateToken(token: String): Boolean { | ||
val schema = getSchema(token) | ||
val tokenStr = token.removePrefix(schema.schema) | ||
|
||
return when (schema) { | ||
TokenType.BEARER -> authenticateUserToken(tokenStr) | ||
TokenType.APP -> authenticateAppToken(tokenStr) | ||
TokenType.INTERNAL -> authenticateInternalToken(tokenStr) | ||
else -> false | ||
} | ||
} | ||
|
||
suspend fun validateTokenSchema(token: String, allowedSchemas: List<TokenType>): Boolean { | ||
if (allowedSchemas.isEmpty()) return true // No schemas required | ||
val schema = getSchema(token) | ||
|
||
return allowedSchemas.contains(schema) | ||
} | ||
|
||
suspend fun authorizeToken(token: String, requiredScopes: List<Scope>): Boolean { | ||
if (requiredScopes.isEmpty()) return true // No scopes required | ||
|
||
val schema = getSchema(token) | ||
val tokenStr = token.removePrefix(schema.schema) | ||
|
||
val scopes = when (schema) { | ||
TokenType.BEARER -> getScopesForUserToken(tokenStr) | ||
TokenType.APP -> getScopesForAppToken(tokenStr) | ||
TokenType.INTERNAL -> getScopesForInternalToken() | ||
else -> return false | ||
} | ||
|
||
return scopes.containsAll(requiredScopes) | ||
} | ||
|
||
|
||
// Authentication based on token type | ||
private suspend fun authenticateUserToken(token: String): Boolean { | ||
val session = sessionService.getSession(token) ?: return false | ||
|
||
return !session.expiresAt.isExpiredTtl() | ||
} | ||
|
||
private suspend fun authenticateAppToken(token: String): Boolean { | ||
val key = apiKeyService.getKey(token) ?: return false | ||
|
||
return !key.blocked | ||
} | ||
|
||
private fun authenticateInternalToken(token: String): Boolean { | ||
return Config.SECRET_DISCAL_API_KEY.getString() == token | ||
} | ||
|
||
// Fetching scopes for tokens | ||
private suspend fun getScopesForUserToken(token: String): List<Scope> { | ||
return sessionService.getSession(token)?.scopes ?: emptyList() | ||
} | ||
|
||
private suspend fun getScopesForAppToken(token: String): List<Scope> { | ||
return apiKeyService.getKey(token)?.scopes ?: emptyList() | ||
} | ||
|
||
private fun getScopesForInternalToken(): List<Scope> = Scope.entries.toList() | ||
|
||
// Various other stuff | ||
private fun getSchema(token: String): TokenType { | ||
return when { | ||
token.startsWith(TokenType.BEARER.schema) -> TokenType.BEARER | ||
token.startsWith(TokenType.APP.schema) -> TokenType.APP | ||
token.startsWith(TokenType.INTERNAL.schema) -> TokenType.INTERNAL | ||
else -> TokenType.NONE | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
cam/src/main/kotlin/org/dreamexposure/discal/cam/controllers/v1/SecurityController.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package org.dreamexposure.discal.cam.controllers.v1 | ||
|
||
import org.dreamexposure.discal.cam.business.SecurityService | ||
import org.dreamexposure.discal.core.annotations.SecurityRequirement | ||
import org.dreamexposure.discal.core.`object`.new.security.Scope.INTERNAL_CAM_VALIDATE_TOKEN | ||
import org.dreamexposure.discal.core.`object`.new.security.TokenType.INTERNAL | ||
import org.dreamexposure.discal.core.`object`.rest.v1.security.ValidateRequest | ||
import org.dreamexposure.discal.core.`object`.rest.v1.security.ValidateResponse | ||
import org.springframework.http.HttpStatus | ||
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("/v1/security") | ||
class SecurityController( | ||
private val securityService: SecurityService, | ||
) { | ||
@SecurityRequirement(schemas = [INTERNAL], scopes = [INTERNAL_CAM_VALIDATE_TOKEN]) | ||
@PostMapping("/validate", produces = ["application/json"]) | ||
suspend fun validate(@RequestBody request: ValidateRequest): ValidateResponse { | ||
val result = securityService.authenticateAndAuthorizeToken( | ||
request.token, | ||
request.schemas, | ||
request.scopes, | ||
) | ||
|
||
return ValidateResponse(result.first == HttpStatus.OK, result.first, result.second) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
cam/src/main/kotlin/org/dreamexposure/discal/cam/security/SecurityWebFilter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package org.dreamexposure.discal.cam.security | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper | ||
import kotlinx.coroutines.reactive.awaitFirstOrNull | ||
import kotlinx.coroutines.reactor.mono | ||
import org.dreamexposure.discal.cam.business.SecurityService | ||
import org.dreamexposure.discal.core.annotations.SecurityRequirement | ||
import org.dreamexposure.discal.core.extensions.spring.writeJsonString | ||
import org.dreamexposure.discal.core.`object`.rest.ErrorResponse | ||
import org.springframework.http.HttpStatus | ||
import org.springframework.stereotype.Component | ||
import org.springframework.web.method.HandlerMethod | ||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping | ||
import org.springframework.web.server.ServerWebExchange | ||
import org.springframework.web.server.WebFilter | ||
import org.springframework.web.server.WebFilterChain | ||
import reactor.core.publisher.Mono | ||
|
||
@Component | ||
class SecurityWebFilter( | ||
private val securityService: SecurityService, | ||
private val handlerMapping: RequestMappingHandlerMapping, | ||
private val objectMapper: ObjectMapper, | ||
) : WebFilter { | ||
|
||
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> { | ||
return mono { | ||
doSecurityFilter(exchange, chain) | ||
}.then(chain.filter(exchange)) | ||
} | ||
|
||
suspend fun doSecurityFilter(exchange: ServerWebExchange, chain: WebFilterChain) { | ||
val handlerMethod = handlerMapping.getHandler(exchange) | ||
.cast(HandlerMethod::class.java) | ||
.onErrorResume { Mono.empty() } | ||
.awaitFirstOrNull() ?: return | ||
|
||
if (!handlerMethod.hasMethodAnnotation(SecurityRequirement::class.java)) { | ||
throw IllegalStateException("No SecurityRequirement annotation!") | ||
} | ||
|
||
val authAnnotation = handlerMethod.getMethodAnnotation(SecurityRequirement::class.java)!! | ||
val authHeader = exchange.request.headers.getOrEmpty("Authorization").firstOrNull() | ||
|
||
|
||
if (authAnnotation.disableSecurity) return | ||
|
||
if (authHeader == null) { | ||
exchange.response.statusCode = HttpStatus.UNAUTHORIZED | ||
exchange.response.writeJsonString( | ||
objectMapper.writeValueAsString(ErrorResponse("Missing Authorization header")) | ||
).awaitFirstOrNull() | ||
return | ||
} | ||
|
||
if (authHeader.equals("teapot", ignoreCase = true)) { | ||
exchange.response.statusCode = HttpStatus.I_AM_A_TEAPOT | ||
exchange.response.writeJsonString( | ||
objectMapper.writeValueAsString(ErrorResponse("I'm a teapot")) | ||
).awaitFirstOrNull() | ||
return | ||
} | ||
|
||
val result = securityService.authenticateAndAuthorizeToken( | ||
authHeader, | ||
authAnnotation.schemas.toList(), | ||
authAnnotation.scopes.toList() | ||
) | ||
if (result.first != HttpStatus.OK) { | ||
exchange.response.statusCode = result.first | ||
exchange.response.writeJsonString( | ||
objectMapper.writeValueAsString(ErrorResponse(result.second)) | ||
).awaitFirstOrNull() | ||
return | ||
} | ||
|
||
// If we made it to the end, everything is good to go. | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.