diff options
7 files changed, 181 insertions, 17 deletions
diff --git a/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt b/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt index 918b1d1f..ec348a25 100644 --- a/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt +++ b/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt @@ -30,3 +30,8 @@ expect open class JoobyServer * A server request. */ expect interface Request + +/** + * A user profile. + */ +expect class Profile diff --git a/kvision-server/build.gradle b/kvision-server/build.gradle index 6d623b3d..aa96a580 100644 --- a/kvision-server/build.gradle +++ b/kvision-server/build.gradle @@ -15,6 +15,7 @@ dependencies { compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" compile "org.jooby:jooby-lang-kotlin" compile "org.jooby:jooby-jackson" + compile "org.jooby:jooby-pac4j2" compile "com.github.andrewoma.kwery:mapper:${kweryVersion}" compile "com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonModuleKotlinVersion}" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlinVersion" diff --git a/kvision-server/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt b/kvision-server/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt index 7d339d04..a529aa27 100644 --- a/kvision-server/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt +++ b/kvision-server/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt @@ -25,9 +25,11 @@ package pl.treksoft.kvision.remote import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import kotlinx.coroutines.experimental.Deferred +import kotlinx.coroutines.experimental.Unconfined import org.jooby.Kooby import org.jooby.Session import org.jooby.json.Jackson +import org.pac4j.core.profile.CommonProfile import kotlinx.coroutines.experimental.async as coroutinesAsync /** @@ -53,10 +55,15 @@ actual open class JoobyServer(init: JoobyServer.() -> Unit) : Kooby() { actual typealias Request = org.jooby.Request /** + * A user profile. + */ +actual typealias Profile = CommonProfile + +/** * A helper extension function for asynchronous request processing. */ fun <RESP> Request?.async(block: (Request) -> RESP): Deferred<RESP> = this?.let { req -> - coroutinesAsync { + coroutinesAsync(Unconfined) { block(req) } } ?: throw IllegalStateException("Request not set!") @@ -64,9 +71,20 @@ fun <RESP> Request?.async(block: (Request) -> RESP): Deferred<RESP> = this?.let /** * A helper extension function for asynchronous request processing with session. */ -fun <RESP> Request?.asyncSession(block: (Request, Session) -> RESP): Deferred<RESP> = this?.let { req -> +fun <RESP> Request?.async(block: (Request, Session) -> RESP): Deferred<RESP> = this?.let { req -> val session = req.session() - coroutinesAsync { + coroutinesAsync(Unconfined) { block(req, session) } } ?: throw IllegalStateException("Request not set!") + +/** + * A helper extension function for asynchronous request processing with session and user profile. + */ +fun <RESP> Request?.async(block: (Request, Session, Profile) -> RESP): Deferred<RESP> = this?.let { req -> + val session = req.session() + val profile = req.require(CommonProfile::class.java) + coroutinesAsync(Unconfined) { + block(req, session, profile) + } +} ?: throw IllegalStateException("Request not set!") diff --git a/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt b/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt index 7d9f4b05..4247b0b7 100644 --- a/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt +++ b/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt @@ -31,6 +31,11 @@ import kotlin.js.undefined import kotlin.js.JSON as NativeJSON /** + * HTTP status unauthorized (401). + */ +const val HTTP_UNAUTHORIZED = 401 + +/** * An agent responsible for remote calls. */ open class CallAgent { @@ -73,7 +78,11 @@ open class CallAgent { } else { errorText } - reject(Exception(message)) + if (xhr.status.toInt() == HTTP_UNAUTHORIZED) { + reject(SecurityException(message)) + } else { + reject(Exception(message)) + } } }) }) @@ -110,7 +119,11 @@ open class CallAgent { } else { errorText } - reject(Exception(message)) + if (xhr.status.toInt() == HTTP_UNAUTHORIZED) { + reject(SecurityException(message)) + } else { + reject(Exception(message)) + } } this.beforeSend = beforeSend }) diff --git a/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt b/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt index a5c580d6..cd62487a 100644 --- a/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt +++ b/src/main/kotlin/pl/treksoft/kvision/remote/Jooby.kt @@ -21,6 +21,8 @@ */ package pl.treksoft.kvision.remote +import kotlinx.serialization.Serializable + /** * A Jooby based server. * Not used on the js platform. @@ -32,3 +34,19 @@ actual open class JoobyServer * Not used on the js platform. */ actual interface Request + +/** + * A user profile. + */ +@Serializable +actual data class Profile( + val id: String? = null, + val username: String? = null, + val linkedId: String? = null, + val firstName: String? = null, + val familyName: String? = null, + val email: String? = null, + val displayName: String? = null, + val pictureUrl: String? = null, + val profileUrl: String? = null +) diff --git a/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt b/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt index b0d74ece..ec1f8e94 100644 --- a/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt +++ b/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt @@ -57,7 +57,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserialize<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer(), it) + JSON.nonstrict.parse(RET::class.serializer(), it) } } } @@ -72,7 +72,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer().list, it) + JSON.nonstrict.parse(RET::class.serializer().list, it) } } } @@ -92,7 +92,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { @Suppress("UNCHECKED_CAST") deserialize<RET>(it, (RET::class as KClass<Any>).js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer(), it) + JSON.nonstrict.parse(RET::class.serializer(), it) } } } @@ -111,7 +111,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer().list, it) + JSON.nonstrict.parse(RET::class.serializer().list, it) } } } @@ -131,7 +131,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserialize<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer(), it) + JSON.nonstrict.parse(RET::class.serializer(), it) } } } @@ -151,7 +151,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer().list, it) + JSON.nonstrict.parse(RET::class.serializer().list, it) } } } @@ -173,7 +173,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserialize<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer(), it) + JSON.nonstrict.parse(RET::class.serializer(), it) } } } @@ -195,7 +195,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer().list, it) + JSON.nonstrict.parse(RET::class.serializer().list, it) } } } @@ -224,7 +224,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserialize<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer(), it) + JSON.nonstrict.parse(RET::class.serializer(), it) } } } @@ -253,7 +253,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer().list, it) + JSON.nonstrict.parse(RET::class.serializer().list, it) } } } @@ -287,7 +287,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserialize<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer(), it) + JSON.nonstrict.parse(RET::class.serializer(), it) } } } @@ -321,7 +321,7 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { - JSON.parse(RET::class.serializer().list, it) + JSON.nonstrict.parse(RET::class.serializer().list, it) } } } diff --git a/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt b/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt new file mode 100644 index 00000000..6373b27a --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/remote/Security.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +import kotlinx.coroutines.experimental.Deferred +import kotlinx.coroutines.experimental.asDeferred +import kotlinx.serialization.Serializable +import pl.treksoft.kvision.utils.obj + +/** + * A security exception. + */ +class SecurityException(message: String) : Exception(message) + +/** + * Username and password credentials. + */ +@Serializable +data class Credentials(val username: String? = null, val password: String? = null) + +/** + * Pac4j form login dispatcher. + */ +class LoginService { + val loginAgent = CallAgent() + + /** + * Login with Pac4j FormClient. + * @param credentials username and password credentials + */ + fun login(credentials: Credentials): Deferred<Boolean> = + loginAgent.remoteCall("callback?client_name=FormClient", obj { + this.username = credentials.username + this.password = credentials.password + }, HttpMethod.POST, "application/x-www-form-urlencoded").then { _: dynamic -> true }.asDeferred() +} + +/** + * Pac4j form login dispatcher. + */ +@Suppress("EXPERIMENTAL_FEATURE_WARNING") +abstract class SecurityMgr { + + private var isLoggedIn = false + + /** + * Executes given block of code after successful authentication. + * @param block a block of code + */ + suspend fun <T> withAuth(block: suspend () -> T): T { + return try { + block().also { + if (!isLoggedIn) { + isLoggedIn = true + afterLogin() + } + } + } catch (e: SecurityException) { + afterError() + isLoggedIn = false + while (!isLoggedIn) { + try { + login().await() + isLoggedIn = true + afterLogin() + } catch (e: SecurityException) { + console.log(e) + } + } + block() + } + } + + /** + * Login user. + * @return true if login is successful + * @throws SecurityException if login is not successful + */ + abstract suspend fun login(): Deferred<Boolean> + + /** + * Method called after successful login. + */ + open suspend fun afterLogin() {} + + /** + * Method called after error. + */ + open suspend fun afterError() {} +} |