Skip to content

Commit

Permalink
0.0.13
Browse files Browse the repository at this point in the history
  • Loading branch information
babyfish-ct committed Apr 27, 2022
1 parent b6b3c36 commit 29afbb0
Show file tree
Hide file tree
Showing 12 changed files with 91 additions and 50 deletions.
2 changes: 1 addition & 1 deletion example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ repositories {
}

dependencies {
implementation("org.babyfish.graphql.provider:graphql-provider-starter-dgs:0.0.8")
implementation("org.babyfish.graphql.provider:graphql-provider-starter-dgs:0.0.13")
ksp("org.babyfish.kimmer:kimmer-ksp:0.3.3")
runtimeOnly("io.r2dbc:r2dbc-h2:0.8.5.RELEASE")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package org.babyfish.graphql.provider.example.mutation

import org.babyfish.graphql.provider.Mutation
import org.babyfish.graphql.provider.dsl.MutationDSL
import org.babyfish.graphql.provider.example.model.Book
import org.babyfish.graphql.provider.example.security.AppUserDetails
import org.babyfish.graphql.provider.runtime.R2dbcClient
import org.babyfish.graphql.provider.security.authentication
import org.babyfish.graphql.provider.security.currentUserDetails
import org.springframework.stereotype.Service
import java.util.*

Expand All @@ -14,17 +13,9 @@ class FavoriteMutation(
private val r2dbcClient: R2dbcClient
) : Mutation() {

override fun MutationDSL.config() {
security {
not {
anonymous()
}
}
}

suspend fun like(bookId: UUID): Boolean =
runtime.mutate {
val me = (authentication().principal as AppUserDetails).appUser
val me = currentUserDetails<AppUserDetails>().appUser
r2dbcClient.execute {
associations.byList(Book::fans).saveCommand(
bookId,
Expand All @@ -33,9 +24,9 @@ class FavoriteMutation(
}
}

suspend fun unlike(bookId: UUID): Boolean =
suspend fun dislike(bookId: UUID): Boolean =
runtime.mutate {
val me = (authentication().principal as AppUserDetails).appUser
val me = currentUserDetails<AppUserDetails>().appUser
r2dbcClient.execute {
associations.byList(Book::fans).deleteCommand(
bookId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,39 @@ class BookQuery: Query() {
orderBy(table.name)
}
}
}
}

/*
* When authorFirstName is not null or authorLastName is not null,
* a sub-query is used to filter data.
*
* Since kimmer-sql-0.3.3, the middle table hidden by entity model
* can be select and modified directly, so you can also write the
* sub-query like this
*
* where {
* table.id valueIn subQueries.byList(Book::authors) {
* authorFirstName?.let {
* where(table.target.firstName ilike it)
* }
* authorLastName?.let {
* where(table.target.lastName ilike it)
* }
* select(table.source.id)
* }
* }
*
* All of these two methods work fine,
* with the same functionality and same performance.
* (
* "select(table.books.id)" in the example code can be optimized by half-join,
* "select(table.source.id)" here can be optimized by phantom-join.
* So, both of them are high-performance practices.
*
* Please read
* "https://github.com/babyfish-ct/kimmer/blob/main/doc/kimmer-sql/table-joins.md"
* to learn more
* ).
*
* You're free to choose whichever method you prefer.
*/
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
package org.babyfish.graphql.provider.example.query

import org.babyfish.graphql.provider.Query
import org.babyfish.graphql.provider.dsl.QueryDSL
import org.babyfish.graphql.provider.example.model.Book
import org.babyfish.graphql.provider.example.model.`fans ∩`
import org.babyfish.graphql.provider.example.security.AppUserDetails
import org.babyfish.graphql.provider.security.authentication
import org.babyfish.graphql.provider.security.currentUserDetails
import org.springframework.stereotype.Service

@Service
class FavoriteQuery : Query() {

override fun QueryDSL.config() {
security {
not {
anonymous()
}
}
}

suspend fun myFavoriteBooks(): List<Book> {
val me = (authentication().principal as AppUserDetails).appUser
val me = currentUserDetails<AppUserDetails>().appUser
return runtime.queryList {
db {
where(table.`fans ∩`(listOf(me.id)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import org.babyfish.kimmer.sql.ast.eq
import org.springframework.stereotype.Component

@Component
class UserDetailsServiceImpl(
class AppUserDetailsService(
private val r2dbcClient: R2dbcClient
) : AsyncUserDetailsService<AppUserDetails>,
AsyncUserDetailsPasswordService<AppUserDetails> {

// Tell the framework how to get single user with roles.
override suspend fun findByUsername(name: String): AppUserDetails? =
r2dbcClient
.query(AppUser::class) {
Expand All @@ -26,15 +27,16 @@ class UserDetailsServiceImpl(
.firstOrNull()
?.let { user ->
return AppUserDetails(
newAsync(AppUser::class).by(user) {
newAsync(AppUser::class).by (user) {
roles = r2dbcClient.query(Role::class) {
where(table.`appUsers ∩`(listOf(user.id)))
select(table)
}
}
)
}
}

// Tell the framework how to update user's password.
override suspend fun updatePassword(
user: AppUserDetails,
newPassword: String
Expand Down
8 changes: 8 additions & 0 deletions example/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ graphql:
refreshTimeout: PT24H
api:
usernameArgName: email

# Special reminder!
#
# The accessTimeout and refreshTimeout are set to
# be very long for the convenience of experimentation
# and learning.
#
# Do not configure them in this way in real projects!
2 changes: 1 addition & 1 deletion project/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
allprojects {
group = "org.babyfish.graphql.provider"
version = "0.0.8"
version = "0.0.13"
}
1 change: 1 addition & 0 deletions project/graphql-provider/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {

implementation("io.jsonwebtoken:jjwt-api:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")

kspTest("org.babyfish.kimmer:kimmer-ksp:0.3.3")
testImplementation(kotlin("test"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
package org.babyfish.graphql.provider.security

import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import java.rmi.server.UID

suspend fun authentication(): Authentication =
authenticationOrNull() ?: error("No authentication")
authenticationOrNull() ?:
error("No authentication")

suspend fun authenticationOrNull(): Authentication? =
ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()?.authentication
ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()?.authentication

@SuppressWarnings("UNCHECKED_CAST")
suspend fun <UD: UserDetails> currentUserDetails(): UD =
authenticationOrNull()?.principal.let {
when {
it is UserDetails ->
it as UD
it === null ->
throw AccessDeniedException("Access Denied")
else ->
throw AccessDeniedException("Current principal is not UserDetails")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.babyfish.graphql.provider.security.jwt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.reactor.awaitSingleOrNull
import kotlinx.coroutines.withContext
import org.babyfish.graphql.provider.runtime.cfg.GraphQLProviderProperties
import org.babyfish.graphql.provider.security.AuthenticationBehaviorProvider
import org.babyfish.graphql.provider.security.authenticationOrNull
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService
Expand All @@ -11,16 +12,25 @@ import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import java.lang.IllegalStateException

@Service
internal open class JwtAuthenticationServiceImpl(
private val userDetailsService: ReactiveUserDetailsService,
private val userDetailsPasswordService: ReactiveUserDetailsPasswordService,
private val jwtTokenService: JwtTokenService,
private val passwordEncoder: PasswordEncoder,
private val authenticationBehaviorProvider: AuthenticationBehaviorProvider<*>,
authenticationBehaviorProvider: AuthenticationBehaviorProvider<*>?,
) : JwtAuthenticationService {

private val authenticationBehaviorProvider: AuthenticationBehaviorProvider<*> =
authenticationBehaviorProvider
?: throw IllegalStateException(
"A bean whose type is '${AuthenticationBehaviorProvider::class.qualifiedName}' " +
"must be configured when " +
"'${GraphQLProviderProperties.Security.Jwt.PROPERTY_PATH}.enabled' is true"
)

@Suppress("UNCHECKED_CAST")
override suspend fun login(
username: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ internal class JwtTokenService(
private val key = properties.security.jwt.secret.takeIf { it.isNotBlank() }?.let {
Keys.hmacShaKeyFor(it.toByteArray())
} ?: Keys.secretKeyFor(SignatureAlgorithm.HS256).also {
if (logger.isInfoEnabled) {
logger.info(
if (logger.isWarnEnabled) {
logger.warn(
"The generated security key of JWT is '{}'. " +
"If you do not want to use the generated value, " +
"please configure '${
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package org.babyfish.graphql.provider.security.jwt.cfg

import org.babyfish.graphql.provider.runtime.cfg.GraphQLProviderProperties
import org.babyfish.graphql.provider.security.AuthenticationBehaviorProvider
import org.babyfish.graphql.provider.security.jwt.*
import org.babyfish.graphql.provider.security.jwt.JwtAuthenticationServiceImpl
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import java.lang.IllegalStateException

@Configuration
@ConditionalOnProperty("${GraphQLProviderProperties.Security.Jwt.PROPERTY_PATH}.enabled")
Expand All @@ -20,16 +18,4 @@ import java.lang.IllegalStateException
ToolConfiguration::class,
RestApiConfiguration::class
])
open class JwtSecurityConfiguration(
authenticationBehaviorProvider: AuthenticationBehaviorProvider<*>?
) {
init {
if (authenticationBehaviorProvider === null) {
throw IllegalStateException(
"A bean whose type is '${AuthenticationBehaviorProvider::class.qualifiedName}' " +
"must be configured when " +
"'${GraphQLProviderProperties.Security.Jwt.PROPERTY_PATH}.enabled' is true"
)
}
}
}
open class JwtSecurityConfiguration

0 comments on commit 29afbb0

Please sign in to comment.