Skip to content

Commit

Permalink
- keycloak-idp;
Browse files Browse the repository at this point in the history
  • Loading branch information
alwayswanna committed Dec 17, 2023
1 parent 4295dbf commit 6c8b287
Show file tree
Hide file tree
Showing 55 changed files with 2,002 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Kotlin CI with Gradle

on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]

permissions:
contents: read

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: build
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
.idea
.gradle
card-service/build
statistics-service/build
oauth2-server/build
12 changes: 12 additions & 0 deletions .run/idp-compose.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="idp-compose" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="composeProjectName" value="idp-compose" />
<option name="envFilePath" value="" />
<option name="sourceFilePath" value="docker-compose.yaml" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>
59 changes: 59 additions & 0 deletions card-service/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
idea
id("org.springframework.boot") version "3.1.6"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.20"
kotlin("plugin.spring") version "1.9.20"
kotlin("plugin.jpa") version "1.9.20"
}

group = "a.gleb"
version = "1.0-RELEASE"

java {
sourceCompatibility = JavaVersion.VERSION_17
}

repositories {
mavenCentral()
}

val openApiVersion = "2.0.4"
val loggingUtilsVersion = "3.0.5"
val testContainersVersion = "1.19.3"

dependencies {
/* Spring Boot & Spring Cloud */
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-actuator")

/* Database */
runtimeOnly("org.postgresql:postgresql")
implementation("org.flywaydb:flyway-core")

/* Other */
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("io.github.microutils:kotlin-logging-jvm:$loggingUtilsVersion")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:$openApiVersion")

/* Tests */
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.testcontainers:postgresql:$testContainersVersion")
}

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}

tasks.withType<Test> {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package a.gleb.cardservice

import mu.KotlinLogging
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

val logger = KotlinLogging.logger {}

@SpringBootApplication
class CardServiceApplication

fun main(args: Array<String>) {
runApplication<CardServiceApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package a.gleb.cardservice.configuration

import io.swagger.v3.oas.annotations.OpenAPIDefinition
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.info.Info
import io.swagger.v3.oas.annotations.security.OAuthFlow
import io.swagger.v3.oas.annotations.security.OAuthFlows
import io.swagger.v3.oas.annotations.security.OAuthScope
import io.swagger.v3.oas.annotations.security.SecurityScheme
import org.springframework.context.annotation.Configuration

const val OAUTH2_SECURITY_SCHEMA = "myOauth2Security"

@OpenAPIDefinition(
info = Info(
title = "card-manager-backend",
description = "API service for manage cards entities", version = "1"
)
)
@SecurityScheme(
name = OAUTH2_SECURITY_SCHEMA,
type = SecuritySchemeType.OAUTH2,
flows = OAuthFlows(
authorizationCode = OAuthFlow(
authorizationUrl = "\${springdoc.oAuthFlow.authorizationUrl}",
tokenUrl = "\${springdoc.oAuthFlow.tokenUrl}",
scopes = [
OAuthScope(name = "openid", description = "openid scope")
]
)
)
)
@Configuration
class OpenApiConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package a.gleb.cardservice.configuration

import a.gleb.cardservice.configuration.properties.CardServiceConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.web.SecurityFilterChain
import org.springframework.web.cors.UrlBasedCorsConfigurationSource

private val UNPROTECTED_PATTERNS = listOf(
"/actuator/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/v3/api-docs/**"
)
const val ROLE_CLAIM = "role"
const val SCOPE_CLAIM = "scope"
const val CREDENTIAL_TOKEN = "oauth2-server-token"

@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(CardServiceConfigurationProperties::class)
class SecurityConfiguration(
private val properties: CardServiceConfigurationProperties
) {

@Bean
fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
return httpSecurity
.csrf { it.disable() }
.cors {
val urlBasedCorsConfigurationSource = UrlBasedCorsConfigurationSource()
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", properties.corsConfiguration)
it.configurationSource(urlBasedCorsConfigurationSource)
}
.oauth2ResourceServer {
it.jwt { jwtConfigurer ->
jwtConfigurer.jwtAuthenticationConverter { converter -> jwtAuthenticationConverter(converter) }
}
}
.authorizeHttpRequests { customizeRequestMatcher(it) }
.build()

}

private fun customizeRequestMatcher(
matcher: AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry
) {
matcher.requestMatchers(*UNPROTECTED_PATTERNS.toTypedArray()).permitAll()

properties.securityConstraints.forEach { securityConstraint ->

val roles = securityConstraint.authRoles
securityConstraint.securityCollections.forEach { securityCollection ->
val patterns = securityCollection.patterns

if (securityCollection.methods == null || securityCollection.methods!!.isEmpty()) {
matcher.requestMatchers(*patterns.toTypedArray()).hasAnyRole(*roles.toTypedArray())
} else {
securityCollection.methods!!.forEach { method ->
matcher.requestMatchers(HttpMethod.valueOf(method), *patterns.toTypedArray())
.hasAnyRole(*roles.toTypedArray())
}
}

}
}

matcher.anyRequest().authenticated()
}

private fun jwtAuthenticationConverter(converter: Jwt): AbstractAuthenticationToken {
val roles = converter.claims[ROLE_CLAIM] as List<*>
val grantedAuthorities: List<GrantedAuthority> = if (roles.isNotEmpty()) {
roles.asSequence()
.map { "ROLE_$it" }
.map { SimpleGrantedAuthority(it) }
.toList()
} else {
(converter.claims[SCOPE_CLAIM] as List<*>).asSequence()
.map { "ROLE_$it" }
.map { SimpleGrantedAuthority(it) }
.toList()
}

return AbstractAuthenticationTokenImpl(grantedAuthorities, converter)
}

private class AbstractAuthenticationTokenImpl(
authorities: List<GrantedAuthority>,
private val converter: Jwt
) : AbstractAuthenticationToken(authorities) {

override fun getCredentials(): Any {
return CREDENTIAL_TOKEN
}

override fun getPrincipal(): Any {
return converter
}

override fun isAuthenticated(): Boolean {
return true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package a.gleb.cardservice.configuration.properties

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.web.cors.CorsConfiguration

@ConfigurationProperties("card-service")
data class CardServiceConfigurationProperties(
var corsConfiguration: Cors,
var securityConstraints: List<SecurityConstraints>
)

class Cors: CorsConfiguration()

data class SecurityConstraints(
var securityCollections: List<SecurityCollection>,
var authRoles: List<String>
)

data class SecurityCollection(
var patterns: List<String>,
var methods: List<String>?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package a.gleb.cardservice.controller

import a.gleb.cardservice.configuration.OAUTH2_SECURITY_SCHEMA
import a.gleb.cardservice.model.CardRequest
import a.gleb.cardservice.model.CardResponse
import a.gleb.cardservice.service.CardService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.*
import java.util.*

const val CARD_CONTROLLER = "card.controller"

@RestController
@RequestMapping("/api/v1/card")
@Tag(name = CARD_CONTROLLER)
class CardController(
private val cardService: CardService
) {

@Operation(
summary = "Get card by ID",
security = [SecurityRequirement(name = OAUTH2_SECURITY_SCHEMA)]
)
@GetMapping
fun get(@RequestParam id: UUID): CardResponse {
return cardService.get(id)
}

@Operation(
summary = "Create new card",
security = [SecurityRequirement(name = OAUTH2_SECURITY_SCHEMA)]
)
@PostMapping
fun create(@RequestBody request: CardRequest): CardResponse {
return cardService.create(request)
}

@Operation(
summary = "Update card",
security = [SecurityRequirement(name = OAUTH2_SECURITY_SCHEMA)]
)
@PutMapping
fun update(@RequestBody request: CardRequest): CardResponse {
return cardService.update(request)
}

@Operation(
summary = "Delete card",
security = [SecurityRequirement(name = OAUTH2_SECURITY_SCHEMA)]
)
@DeleteMapping
fun delete(@RequestParam id: UUID) {
return cardService.delete(id)
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package a.gleb.cardservice.controller

import a.gleb.cardservice.configuration.OAUTH2_SECURITY_SCHEMA
import a.gleb.cardservice.model.CardResponse
import a.gleb.cardservice.service.CardService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

const val STATISTIC_CONTROLLER = "statistics.controller"

@RestController
@RequestMapping("/api/v1/statistics")
@Tag(name = STATISTIC_CONTROLLER)
class StatisticsController (
private val cardService: CardService
){

@Operation(
summary = "Load statistic by card.",
security = [SecurityRequirement(name = OAUTH2_SECURITY_SCHEMA)]
)
@GetMapping
fun statistics(): List<CardResponse> {
return cardService.statistics()
}
}
Loading

0 comments on commit 6c8b287

Please sign in to comment.