├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── Ci.kt │ └── Dependencies.kt ├── cibuild.sh ├── codacy └── codacy-coverage-reporter ├── config └── detekt.yml ├── contributing.md ├── doc └── todo.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── nee-cache-caffeine ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── dev │ └── neeffect │ └── nee │ └── effects │ └── cache │ └── caffeine │ └── CaffeineProvider.kt ├── nee-core ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── dev │ │ └── neeffect │ │ └── nee │ │ ├── Coroutines.kt │ │ ├── Effects.kt │ │ ├── NEE.kt │ │ ├── atomic │ │ └── AtomicRef.kt │ │ └── effects │ │ ├── Out.kt │ │ ├── async │ │ ├── AsyncEffect.kt │ │ └── AsyncStack.kt │ │ ├── cache │ │ └── CacheEffect.kt │ │ ├── env │ │ └── FlexibleEnv.kt │ │ ├── monitoring │ │ ├── EntryType.kt │ │ ├── LogsAnalyzer.kt │ │ ├── LogsProvider.kt │ │ ├── MutableInMemLogger.kt │ │ ├── SimpleBufferedLogger.kt │ │ ├── SimpleTraceProvider.kt │ │ └── TraceEffect.kt │ │ ├── security │ │ ├── DummySecurityProvider.kt │ │ ├── FlexSecEffect.kt │ │ └── SecEffect.kt │ │ ├── test │ │ └── TestUtils.kt │ │ ├── time │ │ └── TimeProvider.kt │ │ ├── tx │ │ ├── DummyTxProvider.kt │ │ ├── TxConnection.kt │ │ ├── TxEffect.kt │ │ └── TxFlex.kt │ │ └── utils │ │ ├── Logging.kt │ │ └── Utils.kt │ └── test │ └── kotlin │ ├── dev │ └── neeffect │ │ └── nee │ │ ├── NEETest.kt │ │ ├── UUIDUtils.kt │ │ └── effects │ │ ├── async │ │ ├── AsyncEffectTest.kt │ │ ├── AsyncStackTest.kt │ │ └── AsyncTxTest.kt │ │ ├── cache │ │ └── CacheEffectTest.kt │ │ ├── coroutines │ │ └── CoroutinesTest.kt │ │ ├── env │ │ └── FlexibleEnvTest.kt │ │ ├── monitoring │ │ ├── LogsAnalyzerTest.kt │ │ └── TraceEffectTest.kt │ │ ├── time │ │ └── HasteTimeProviderTest.kt │ │ └── tx │ │ ├── CombinedEffectsTest.kt │ │ ├── DBEffectTest.kt │ │ ├── DBLike.kt │ │ ├── DBLikeResource.kt │ │ ├── DeeperEffectTest.kt │ │ ├── FlexTxEffectTest.kt │ │ ├── TestEffect.kt │ │ └── TrivialSecurityProvider.kt │ └── pl │ └── outside │ └── code │ └── ExternalObject.kt ├── nee-ctx-web-ktor ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── dev │ │ └── neeffect │ │ └── nee │ │ └── ctx │ │ └── web │ │ ├── ApplicationContextProvider.kt │ │ ├── BasicAuth.kt │ │ ├── ErrorHandler.kt │ │ ├── WebContext.kt │ │ ├── WebContextProvider.kt │ │ ├── jwt │ │ └── JwtAuthProvider.kt │ │ ├── oauth │ │ └── OauthSupportApi.kt │ │ ├── pure │ │ └── Ktor.kt │ │ └── util │ │ └── RenderHelper.kt │ └── test │ ├── kotlin │ └── dev │ │ └── neeffect │ │ └── nee │ │ └── ctx │ │ └── web │ │ ├── BaseWebContextSysPathsTest.kt │ │ ├── BasicAuthProviderTest.kt │ │ ├── KtorThreadingModelTest.kt │ │ ├── SimpleApplicationTest.kt │ │ ├── jwt │ │ └── JwtAuthProviderTest.kt │ │ ├── oauth │ │ └── OauthSupportApiTest.kt │ │ └── support │ │ └── EmptyTestContext.kt │ └── resources │ └── google │ └── keys.json ├── nee-jdbc ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── dev │ │ └── neeffect │ │ └── nee │ │ └── effects │ │ └── jdbc │ │ └── JDBCConnection.kt │ └── test │ └── kotlin │ └── dev │ └── neeffect │ └── nee │ ├── atomic │ └── AtomicRefTest.kt │ └── effects │ └── jdbc │ └── JDBCConnectionTest.kt ├── nee-security-jdbc ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── dev │ │ └── neeffect │ │ └── nee │ │ └── security │ │ ├── DBUserRealm.kt │ │ └── PBKDF2Hasher.kt │ └── test │ └── kotlin │ └── dev │ └── neeffect │ └── nee │ └── security │ ├── DBTestConnection.kt │ ├── DBUserRealmTest.kt │ └── PBKDF2HasherTest.kt ├── nee-security ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── dev │ │ └── neeffect │ │ └── nee │ │ └── security │ │ ├── User.kt │ │ ├── UserRealm.kt │ │ ├── jwt │ │ ├── JwtCoder.kt │ │ ├── MultiVerifier.kt │ │ ├── SimpleUserCoder.kt │ │ └── UserCoder.kt │ │ ├── oauth │ │ ├── GoogleOpenId.kt │ │ ├── OauthConfig.kt │ │ ├── OauthConfigModule.kt │ │ ├── OauthProviderName.kt │ │ ├── OauthProviders.kt │ │ ├── OauthService.kt │ │ └── config │ │ │ ├── GithubOAuth.kt │ │ │ ├── OauthConfigLoder.kt │ │ │ └── OauthModule.kt │ │ └── state │ │ └── ServerVerifier.kt │ └── test │ ├── kotlin │ └── dev │ │ └── neeffect │ │ └── nee │ │ ├── effects │ │ └── security │ │ │ ├── SecuredRunEffectTest.kt │ │ │ ├── SimpleSecurityProvider.kt │ │ │ ├── oauth │ │ │ ├── GoogleOpenIdTest.kt │ │ │ ├── OauthServiceTest.kt │ │ │ └── config │ │ │ │ └── OauthConfigLoderTest.kt │ │ │ └── state │ │ │ └── ServerStateCheckTest.kt │ │ └── security │ │ └── jwt │ │ ├── JwtEncoderTest.kt │ │ └── JwtUsersTest.kt │ └── resources │ ├── conf │ ├── jwtConfig.yml │ └── oauthConfig.yml │ ├── google │ └── keys.json │ └── keys │ └── testServerKey.bin ├── nee-test ├── nee-core-test │ └── build.gradle.kts ├── nee-ctx-web-test │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── dev │ │ └── neeffect │ │ └── nee │ │ └── web │ │ └── test │ │ ├── TestServer.kt │ │ └── TestWebContextProvider.kt └── nee-security-jdbc-test │ ├── build.gradle.kts │ └── src │ └── main │ ├── kotlin │ └── dev │ │ └── neeffect │ │ └── nee │ │ └── security │ │ └── test │ │ └── TestDB.kt │ └── resources │ └── db │ └── db.xml ├── publish-mpp.gradle.kts ├── scratchpad ├── build.gradle └── src │ └── main │ └── kotlin │ └── dev │ └── neeffect │ └── nee │ └── scratchpad │ ├── Coroutines.kt │ └── RProblem.kt ├── scripts └── publishLocal.sh └── settings.gradle /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: [ push ] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up JDK 11 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 11 16 | - name: Build with Gradle 17 | run: ./cibuild.sh ${{ secrets.CODACY_COVERAGE_TOKEN }} 18 | buildNewJava: # jacoco 0.8.6 does not work with java 15 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: Set up JDK 14 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: 14 26 | - name: Build with Gradle 27 | run: ./gradlew build 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .gradle/ 3 | out/ 4 | *.iml 5 | .kotlintest/ 6 | build/ 7 | tmp/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/3613db6c1833407d9daa325d110b81ad)](https://www.codacy.com/gh/neeffect/nee/dashboard?utm_source=github.com&utm_medium=referral&utm_content=neeffect/nee&utm_campaign=Badge_Grade) 2 | 3 | # NEE 4 | 5 | Not so enterprisy effects. 6 | 7 | Status: *Work in Progress*. 8 | 9 | Help needed. If you want to contribute read [this](contributing.md) 10 | 11 | ## Goal 12 | 13 | Provide kotlin friendly extensible effects using functional approach. 14 | 15 | It should enable (more or less) features known from aspect oriented frameworks, but in a clean, non magic way. 16 | 17 | Instead of writing: 18 | 19 | ``` kotlin 20 | class Hasiok { 21 | @Resource 22 | val jdbcConnection: Connection 23 | 24 | @Transactional 25 | @Secure 26 | @Cacheable 27 | @Retryable 28 | fun f(p:P) { 29 | //code 30 | } 31 | } 32 | ``` 33 | 34 | It is possible to have similar goodies rewriting code like below: 35 | 36 | ```kotlin 37 | class Hasiok { 38 | 39 | fun enterprisyFunction(x:Int) = Nee.pure( 40 | secure + retryable + cache.of(x) + transactional 41 | ) {jdbcConnection:Connection -> 42 | //code using jdbcConnection 43 | } 44 | //declaration above means security is checked before retrial 45 | //and retrial is made before cache which happens before transaction 46 | } 47 | ``` 48 | 49 | motto : 50 | 51 | 52 | ## Core concept 53 | 54 | ### Business function 55 | 56 | ```kotlin 57 | businessFunction = (R) -> A 58 | ``` 59 | 60 | Where: 61 | **R** - is an environment / infrastructure that a function may use 62 | **A** - is a result of the function 63 | 64 | Typically R would be something like a `database connection`. It might be `security context`. It can be all infrasture 65 | handlers that are relevant in a given context. 66 | 67 | ### Putting inside Nee Monad 68 | 69 | Next step is to put business function inside Nee monad. Nee monad wraps business logic with a given infrastructure. 70 | 71 | ```kotlin 72 | val functionOnRealHardware = Nee.pure(noEffect())(businessFunction) 73 | ``` 74 | 75 | Now `functionOnRealHardware` is blessed with side effects and now is wrapped inside Nee monad. It is enclosed in a monad 76 | to make it "composable" 77 | with other functions. Just think of performing multiple jdbc calls inside one transaction. 78 | 79 | As for side effects we see `noEffect()`... meaning not a real one - but it is time to tell more about `Effects` 80 | 81 | ### Effects 82 | 83 | Effect is a special class that tells how to wrap businessFunction and run it providing infrastructure. 84 | 85 | ```kotlin 86 | interface Effect { 87 | fun wrap(f: (R) -> A): (R) -> Pair, R> 88 | } 89 | ``` 90 | 91 | In order to provide effect we need to implement interface as above. Where: 92 | 93 | - **R** as before is some environment object, think this is how to get DB connection from, 94 | 95 | - **E** is an error that might happen during application of effect 96 | (notice - it does not have to be Exception) 97 | 98 | ```Out``` is special object that represents the final result of calculation. Think of it 99 | as: `Out =~= Future>` 100 | 101 | An effect: 102 | takes a function (businessFunction) which may rely on environment `R`, and on a parameter `P`, giving some result `A`. 103 | Then wraps it into a function that: 104 | takes environment `R` (no change), - runs some infrastructure code (effect), - it also returns changed environment `(R)` 105 | - think that maybe transaction is now started 106 | 107 | ### Monads 108 | 109 | `Nee` is in fact a monad. This means that it is possible to chain various business functions executions. 110 | 111 | `Out` is also a monad. This means it is possible to chain results. 112 | 113 | #### Explanation 114 | 115 | If you want both methods to run inside same transaction: 116 | 117 | ```kotlin 118 | val f1 = Nee.constP(jdbcTransaction) {connection -> 119 | connection.prepareStatement() 120 | [F1 code] 121 | } 122 | 123 | val f2 = Nee.constP(jdbcTransaction) {connection -> 124 | connection.prepareStatement() 125 | [F2 code] 126 | } 127 | 128 | // f has both logic of f1 and f2 enclosed in a single transaction 129 | val f = f1.flatMap { f2 }.perform(jdbcConfig) 130 | ``` 131 | 132 | if you want to run in separate transactions: 133 | 134 | ```kotlin 135 | val f1 = Nee.with(jdbcTransaction) {connection -> 136 | connection.prepareStatement() 137 | [F1 code] 138 | } 139 | 140 | val f2 = Nee.with(jdbcTransaction) {connection -> 141 | connection.prepareStatement() 142 | [F2 code] 143 | } 144 | 145 | //we join results but with separate transactions 146 | val f = f1.perform(jdbcConfig).flatMap { f2.perform(jdbcConfig)} 147 | ``` 148 | 149 | ## TODO 150 | 151 | - Code: 152 | 153 | - naming & long lambdas clean 154 | - Ideas: 155 | - R extract (for effect) - multiple db support 156 | 157 | - arrow? 158 | - Tests: 159 | - real assertions 160 | - unhappy paths 161 | - load tests (sanity) 162 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | //java //- not needed probably 5 | kotlin("jvm") version "1.4.32" 6 | id("io.gitlab.arturbosch.detekt").version("1.16.0") 7 | //`kotlin-dsl` //TODO - read about it 8 | id("jacoco") 9 | id("maven-publish") 10 | // id("java-library")// - not needed probably 11 | signing 12 | id("org.jetbrains.dokka") version "0.10.1" 13 | id("com.bmuschko.nexus") version "2.3.1" 14 | id("io.codearte.nexus-staging") version "0.22.0" 15 | } 16 | 17 | repositories { 18 | mavenLocal() 19 | jcenter() 20 | } 21 | 22 | subprojects { 23 | apply(plugin = "kotlin") 24 | apply(plugin = "java") 25 | apply(plugin = "maven-publish") 26 | apply(plugin = "jacoco") 27 | apply(plugin = "io.gitlab.arturbosch.detekt") 28 | 29 | group = "pl.setblack" 30 | version = Ci.publishVersion 31 | 32 | dependencies { 33 | detektPlugins("pl.setblack:kure-potlin:0.5.0") 34 | detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.15.0") 35 | // https://mvnrepository.com/artifact/org.slf4j/slf4j-api 36 | implementation(Libs.Slf4J.api) 37 | implementation(Libs.Kotlin.coroutinesJdk8) 38 | } 39 | 40 | repositories { 41 | mavenLocal() 42 | mavenCentral() 43 | jcenter() 44 | } 45 | 46 | val compileKotlin: KotlinCompile by tasks 47 | compileKotlin.kotlinOptions.apply { 48 | jvmTarget = "1.8" 49 | javaParameters = true 50 | allWarningsAsErrors = true 51 | } 52 | 53 | val compileTestKotlin: KotlinCompile by tasks 54 | compileTestKotlin.kotlinOptions.apply { 55 | jvmTarget = "1.8" 56 | javaParameters = true 57 | allWarningsAsErrors = false 58 | } 59 | 60 | tasks.withType { 61 | useJUnitPlatform() 62 | } 63 | 64 | tasks.jacocoTestReport { 65 | reports { 66 | html.isEnabled = true 67 | xml.isEnabled = false 68 | csv.isEnabled = false 69 | } 70 | } 71 | //co za wój? 72 | publishing { 73 | publications { 74 | create("maven") { 75 | from(components["java"]) 76 | } 77 | } 78 | } 79 | 80 | detekt { 81 | buildUponDefaultConfig = true // preconfigure defaults 82 | config = files("${rootDir}/config/detekt.yml") 83 | //baseline = file("$projectDir/config/baseline.xml") 84 | reports { 85 | html.enabled = true // observe findings in your browser with structure and code snippets 86 | xml.enabled = true // check(style like format mainly for integrations like Jenkins) 87 | txt.enabled = 88 | true // similar to the console output, contains issue signature to manually edit baseline files 89 | } 90 | } 91 | 92 | tasks.withType().configureEach { 93 | this.jvmTarget = "1.8" 94 | } 95 | } 96 | 97 | tasks.register("generateMergedReport") { 98 | dependsOn(subprojects.map { it.getTasksByName("test", false) }) 99 | additionalSourceDirs.setFrom(files(subprojects.map { it.sourceSets.asMap["main"]?.allSource?.srcDirs })) 100 | sourceDirectories.setFrom(files(subprojects.map { it.sourceSets.asMap["main"]?.allSource?.srcDirs })) 101 | classDirectories.setFrom(files(subprojects.map { it.sourceSets.asMap["main"]?.output })) 102 | //line below if fishy 103 | executionData.setFrom(project.fileTree(Pair("dir", "."), Pair("include", "**/build/jacoco/test.exec"))) 104 | 105 | reports { 106 | xml.isEnabled = true 107 | csv.isEnabled = false 108 | html.isEnabled = true 109 | } 110 | } 111 | 112 | allprojects { 113 | java { 114 | sourceCompatibility = JavaVersion.VERSION_1_8 115 | targetCompatibility = JavaVersion.VERSION_1_8 116 | } 117 | } 118 | 119 | nexusStaging { 120 | packageGroup = "pl.setblack" //optional if packageGroup == project.getGroup() 121 | } 122 | 123 | val publications: PublicationContainer = (extensions.getByName("publishing") as PublishingExtension).publications 124 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | repositories { 2 | jcenter() 3 | } 4 | 5 | plugins { 6 | `kotlin-dsl` 7 | } 8 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Ci.kt: -------------------------------------------------------------------------------- 1 | object Ci { 2 | 3 | // this is the version used for building snapshots 4 | // .buildnumber-snapshot will be appended 5 | private const val snapshotBase = "0.6.8" 6 | 7 | private val githubBuildNumber = System.getenv("GITHUB_RUN_NUMBER") 8 | 9 | private val snapshotVersion = when (githubBuildNumber) { 10 | null -> "$snapshotBase-LOCAL" 11 | else -> "$snapshotBase.${githubBuildNumber}-SNAPSHOT" 12 | } 13 | 14 | private val releaseVersion = System.getenv("RELEASE_VERSION") 15 | 16 | val isRelease = releaseVersion != null 17 | val publishVersion = (releaseVersion ?: snapshotVersion).also { 18 | println("release is ${releaseVersion}") 19 | println("publishVersion is ${it}") 20 | } 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Dependencies.kt: -------------------------------------------------------------------------------- 1 | object Libs { 2 | const val kotlin_version = "1.4.32" 3 | 4 | object Atomic { 5 | private const val version = "0.15.0" 6 | const val atomicFu = "org.jetbrains.kotlinx:atomicfu:$version" 7 | } 8 | 9 | object H2 { 10 | private const val version = "1.4.200" 11 | const val h2 = "com.h2database:h2:$version" 12 | } 13 | 14 | object Kotlin { 15 | const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 16 | private const val coroutinesVersion = "1.4.2" 17 | const val coroutinesJdk8 = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion" 18 | const val reflect = "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 19 | const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" 20 | } 21 | 22 | object Ktor { 23 | private const val version = "1.5.1" 24 | const val serverCore = "io.ktor:ktor-server-core:$version" 25 | const val serverHostCommon = "io.ktor:ktor-server-host-common:$version" 26 | const val serverNetty = "io.ktor:ktor-server-netty:$version" 27 | const val clientCore = "io.ktor:ktor-client-core:$version" 28 | const val clientMockJvm = "io.ktor:ktor-client-mock-jvm:$version" 29 | const val clientJsonJvm = "io.ktor:ktor-client-json-jvm:$version" 30 | const val clientJson = "io.ktor:ktor-client-json:$version" 31 | const val clientJackson = "io.ktor:ktor-client-jackson:$version" 32 | const val jackson = "io.ktor:ktor-jackson:$version" 33 | const val serverTestHost = "io.ktor:ktor-server-test-host:$version" 34 | } 35 | 36 | object Vavr { 37 | private const val version = "0.10.2" 38 | const val kotlin = "io.vavr:vavr-kotlin:$version" 39 | const val jackson = "io.vavr:vavr-jackson:$version" 40 | } 41 | 42 | object Haste { 43 | private const val version = "0.3.1" 44 | const val haste = "io.github.krasnoludkolo:haste:$version" 45 | } 46 | 47 | object Jackson { 48 | private const val version = "2.12.1" 49 | const val jacksonModuleKotlin = "com.fasterxml.jackson.module:jackson-module-kotlin:$version" 50 | const val jacksonAnnotations = "com.fasterxml.jackson.core:jackson-annotations:$version" 51 | const val jacksonJsr310 = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.0" 52 | 53 | } 54 | 55 | object Kotest { 56 | private const val version = "4.4.1" 57 | const val runnerJunit5Jvm = "io.kotest:kotest-runner-junit5-jvm:$version" 58 | const val assertionsCoreJvm = "io.kotest:kotest-assertions-core-jvm:$version" 59 | } 60 | 61 | object Slf4J { 62 | private const val version = "1.7.30" 63 | const val api = "org.slf4j:slf4j-api:$version" 64 | } 65 | 66 | 67 | object Liquibase { 68 | private const val version = "4.3.1" 69 | const val core = "org.liquibase:liquibase-core:$version" 70 | } 71 | 72 | object Hoplite { 73 | private const val version = "1.4.0" 74 | const val core = "com.sksamuel.hoplite:hoplite-core:$version" 75 | const val yaml = "com.sksamuel.hoplite:hoplite-yaml:$version" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cibuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export CODACY_PROJECT_TOKEN=$1 3 | ./gradlew build jacocoTestReport generateMergedReport 4 | #codacy/codacy-coverage-reporter report -l Kotlin -r build/reports/jacoco/generateMergedReport/generateMergedReport.xml 5 | bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r build/reports/jacoco/generateMergedReport/generateMergedReport.xml 6 | -------------------------------------------------------------------------------- /codacy/codacy-coverage-reporter: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neeffect/nee/f44d60abab751da1d07529b532fc39ae6a95056d/codacy/codacy-coverage-reporter -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We like any help. 4 | 5 | Issues, tests, questions, code, typos (and fixes of those typos) 6 | all can help. 7 | 8 | Feel free to crete issue or do pull request. 9 | 10 | ## Rule no 1 11 | 12 | Do not be a jerk. 13 | 14 | ## Branching 15 | 16 | We try to follow gitflow standard 17 | 18 | 19 | 20 | ## Syncing Cheat sheet 21 | 22 | - Fork this repo and do clone (of the fork) 23 | 24 | - add this repository as an upstream to your repository: 25 | 26 | `git remote add upstream git@github.com:neeffect/nee.git` 27 | 28 | - fetch branches and commits from this repository to your local repository: 29 | 30 | `git fetch upstream` 31 | 32 | - check if you are on develop branch out: 33 | 34 | `git checkout develop` 35 | 36 | - merge new develop changes to your fork: 37 | 38 | `git merge upstream/develop` 39 | 40 | `git push origin develop 41 | 42 | - create pull request for develop branch` 43 | -------------------------------------------------------------------------------- /doc/todo.md: -------------------------------------------------------------------------------- 1 | remove simpleFixedThreadPool 2 | 3 | check supplyAsync 4 | 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | #notice in $HOME/.gradle/gradle.properties 3 | # variables need a prefix 4 | # systemProp.org.gradle.project.ossrhUsername 5 | # (do not know why - this is not even documented IMO) 6 | ossrhUsername=none 7 | ossrhPassword=none 8 | ## setting env variable RELEASE_VERSION and running publish - may publish to sonatype 9 | signing.keyId=jratajski@gmail.com 10 | signing.gnupg.keyName=jratajski@gmail.com 11 | org.gradle.caching=true 12 | org.gradle.parallel=true 13 | org.gradle.caching.debug=false 14 | org.gradle.configureondemand=false 15 | org.gradle.daemon.idletimeout=10800000 16 | org.gradle.console=auto 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neeffect/nee/f44d60abab751da1d07529b532fc39ae6a95056d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /nee-cache-caffeine/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.config.KotlinCompilerVersion 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.jvm") 5 | } 6 | 7 | 8 | dependencies { 9 | implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION)) 10 | implementation(project(":nee-core")) 11 | api("com.github.ben-manes.caffeine:caffeine:2.5.5") 12 | } 13 | 14 | apply(from = "../publish-mpp.gradle.kts") 15 | -------------------------------------------------------------------------------- /nee-cache-caffeine/src/main/kotlin/dev/neeffect/nee/effects/cache/caffeine/CaffeineProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.cache.caffeine 2 | 3 | import com.github.benmanes.caffeine.cache.Cache 4 | import com.github.benmanes.caffeine.cache.Caffeine 5 | 6 | import dev.neeffect.nee.effects.cache.CacheProvider 7 | import java.util.concurrent.TimeUnit 8 | 9 | 10 | class CaffeineProvider(private val cache: Cache = defaultCache()) : CacheProvider { 11 | 12 | @Suppress("UNCHECKED_CAST") 13 | override fun computeIfAbsent(key: K, func: (K) -> V): V = 14 | cache.get(key as Any, func as (Any) -> Any) as V 15 | 16 | companion object { 17 | fun defaultCache() = Caffeine.newBuilder() 18 | .expireAfterWrite(2, TimeUnit.MINUTES) 19 | .maximumSize(10000) 20 | .build() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /nee-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api(Libs.Vavr.kotlin) { 3 | exclude("org.jetbrains.kotlin") 4 | } 5 | api(Libs.Haste.haste) 6 | 7 | implementation(Libs.Jackson.jacksonAnnotations) 8 | implementation(Libs.Kotlin.kotlinStdLib) 9 | implementation(Libs.Kotlin.coroutinesTest) 10 | implementation(Libs.Atomic.atomicFu) 11 | testImplementation(project(":nee-test:nee-core-test")) 12 | testImplementation(Libs.Kotest.runnerJunit5Jvm) 13 | testImplementation(Libs.Kotest.assertionsCoreJvm) 14 | } 15 | 16 | 17 | apply(from = "../publish-mpp.gradle.kts") 18 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/Coroutines.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee 2 | 3 | import dev.neeffect.nee.effects.Out 4 | import dev.neeffect.nee.effects.toFuture 5 | import io.vavr.concurrent.Promise 6 | import io.vavr.control.Either 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.future.await 9 | import kotlin.coroutines.Continuation 10 | import kotlin.coroutines.CoroutineContext 11 | import kotlin.coroutines.EmptyCoroutineContext 12 | import kotlin.coroutines.startCoroutine 13 | import kotlin.coroutines.suspendCoroutine 14 | 15 | // This is just preview/sketch how to integrate nee with coroutines 16 | fun Nee.go(): suspend R.() -> A = this.let { nee -> 17 | { 18 | val r = this 19 | suspendCoroutine { continuation -> 20 | nee.perform(r).toFuture().onComplete { result -> 21 | result.forEach { either -> 22 | continuation.resumeWith(Result.success(either.get())) 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | suspend fun R.go(n: Nee): A = 30 | n.perform(this).k()().get() 31 | 32 | suspend fun R.goSafe(n: Nee): Either = 33 | n.perform(this).k()() 34 | 35 | fun runNee(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): 36 | Nee = run { 37 | val coroutine = NeeCoroutine(context) 38 | val promise = Promise.make>() 39 | block.startCoroutine(coroutine, object : Continuation { 40 | override val context: CoroutineContext 41 | get() = context 42 | 43 | override fun resumeWith(result: Result) { 44 | if (result.isSuccess) { 45 | promise.success(Either.right(result.getOrThrow())) 46 | } else { 47 | promise.success(Either.left(result.exceptionOrNull() ?: NullPointerException("unknown error"))) 48 | } 49 | } 50 | }) 51 | val future = promise.future() 52 | val out = Out.FutureOut(future) 53 | Nee.fromOut(out) 54 | } 55 | 56 | private class NeeCoroutine( 57 | val parentContext: CoroutineContext 58 | ) : CoroutineScope { 59 | override val coroutineContext: CoroutineContext 60 | get() = parentContext 61 | } 62 | 63 | fun Out.k(): suspend () -> Either = 64 | when (this) { 65 | is Out.InstantOut -> { -> this.v } 66 | is Out.FutureOut -> { -> this.futureVal.toCompletableFuture().await() } 67 | } 68 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/Effects.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee 2 | 3 | import dev.neeffect.nee.effects.Out 4 | import dev.neeffect.nee.effects.flatMap 5 | import dev.neeffect.nee.effects.utils.merge 6 | import io.vavr.control.Either 7 | 8 | /** 9 | * An effect, or maybe aspect :-) 10 | * 11 | * @param R some environment param used by effect and business function 12 | * @param E error caused by this effect 13 | */ 14 | interface Effect { 15 | /** 16 | * Wrap a business function in a given effect 17 | * 18 | * Gives back "wrapped function" 19 | */ 20 | fun wrap(f: (R) -> A): (R) -> Pair, R> 21 | 22 | /** 23 | * Installs error handler (kind of mapLeft). 24 | * 25 | * Returns new Effect. 26 | */ 27 | fun handleError(handler: (E) -> XE): Effect = 28 | HandleErrorEffect(this, handler) 29 | } 30 | 31 | /** 32 | * Composition of effects. 33 | * 34 | * (Notice: it is actually like kleisli composition of monads... 35 | * maybe in fact Nee and Effect should be a one type?) 36 | */ 37 | fun Effect.andThen(otherEffect: Effect) = Effects.combine(otherEffect, this) 38 | 39 | infix fun Effect.then(otherEffect: Effect) = Effects.combine(otherEffect, this) 40 | 41 | infix fun Effect.with(otherEffect: Effect) = 42 | Effects.combine(otherEffect, this).handleError { error: Either -> 43 | error.map { it as E1 }.merge() 44 | } 45 | 46 | operator fun Effect.plus(otherEffect: Effect) = 47 | this.with(otherEffect) 48 | 49 | data class Effects( 50 | private val inner: Effect, 51 | private val outer: Effect 52 | ) : Effect> 53 | where R2 : R1 { 54 | 55 | override fun wrap(f: (R2) -> A): (R2) -> Pair, A>, R2> = run { 56 | @Suppress("UNCHECKED_CAST") 57 | val internalFunct = { r: R2 -> f(r) } as (R1) -> A 58 | val innerWrapped: (R1) -> Pair, A>, R1> = 59 | inner.handleError { error: E1 -> Either.left(error) } 60 | .wrap(internalFunct) 61 | val outerF = 62 | { rn: R2 -> 63 | val z = innerWrapped(rn) 64 | z.first 65 | } 66 | 67 | val outerWrapped: (R2) -> Pair, Out, A>>, R2> = outer 68 | .handleError { error -> Either.right(error) } 69 | .wrap(outerF) 70 | 71 | val result = { r: R2 -> 72 | val res = outerWrapped(r) 73 | val finalR = res.second 74 | // TODO - finalR or r? 75 | val called = res.first 76 | val x: Out, A> = called.flatMap { it } 77 | 78 | Pair(x, finalR) 79 | } 80 | result 81 | } 82 | 83 | companion object { 84 | fun combine(outer: Effect, inner: Effect): Effect> = 85 | Effects(outer, inner) 86 | } 87 | } 88 | 89 | class NoEffect : Effect { 90 | override fun wrap(f: (R) -> A): (R) -> Pair, R> = 91 | { r -> Pair(Out.right(f(r)), r) } 92 | 93 | companion object { 94 | private val singleInstance = NoEffect() 95 | 96 | @Suppress("UNCHECKED_CAST") 97 | fun get() = singleInstance as NoEffect 98 | } 99 | } 100 | 101 | @Suppress("NOTHING_TO_INLINE") 102 | inline fun noEffect() = NoEffect.get() 103 | 104 | data class HandleErrorEffect( 105 | private val innerEffect: Effect, 106 | private val handler: (E) -> E1 107 | ) : Effect { 108 | override fun wrap(f: (R) -> A): (R) -> Pair, R> = { r: R -> 109 | val result = innerEffect.wrap(f)(r) 110 | Pair(result.first.mapLeft(handler), result.second) 111 | } 112 | } 113 | 114 | fun Effect.anyError(): Effect = HandleErrorEffect(this) { 115 | foldErrors(it as Any) 116 | } 117 | 118 | // TODO tests 119 | private fun foldErrors(e: Any): Any = 120 | when (e) { 121 | is Either<*, *> -> { 122 | e.mapLeft { foldErrors(it as Any) } 123 | .map { foldErrors(it as Any) } 124 | .merge() 125 | } 126 | else -> e 127 | } 128 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/atomic/AtomicRef.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.atomic 2 | 3 | import dev.neeffect.nee.IO 4 | import dev.neeffect.nee.Nee 5 | import java.util.concurrent.atomic.AtomicReference 6 | 7 | @Suppress("UNUSED_PARAMETER") 8 | class AtomicRef(value: A) { 9 | 10 | private val internal: AtomicReference = AtomicReference(value) 11 | 12 | fun get(): IO = Nee.pure { 13 | internal.get() 14 | } 15 | 16 | fun set(a: A): IO = Nee.pure { 17 | internal.set(a) 18 | } 19 | 20 | fun getAndSet(a: A): IO = Nee.pure { 21 | internal.getAndSet(a) 22 | } 23 | 24 | fun update(f: (A) -> A): IO = Nee.pure { 25 | internal.updateAndGet(f) 26 | } 27 | 28 | fun getAndUpdate(f: (A) -> A): IO = Nee.pure { 29 | internal.getAndUpdate(f) 30 | } 31 | 32 | fun updateAndGet(f: (A) -> A): IO = Nee.pure { 33 | internal.updateAndGet(f) 34 | } 35 | 36 | fun modify(f: (A) -> Pair): IO = modifyGet(f).map(Pair::second) 37 | 38 | fun modifyGet(f: (A) -> Pair): IO> = Nee.pure { 39 | modifyImpure(f) 40 | } 41 | 42 | fun tryUpdate(f: (A) -> A): IO = Nee.pure { 43 | val start = internal.get() 44 | val result = f(start) 45 | internal.compareAndSet(start, result) 46 | } 47 | 48 | private fun modifyImpure(f: (A) -> Pair): Pair = run { 49 | val start = internal.get() 50 | val result = f(start) 51 | if (internal.compareAndSet(start, result.first)) { 52 | result 53 | } else { 54 | modifyImpure(f) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/Out.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects 2 | 3 | import dev.neeffect.nee.effects.utils.merge 4 | import io.vavr.concurrent.Future 5 | import io.vavr.control.Either 6 | import kotlinx.coroutines.future.await 7 | 8 | /** 9 | * Outcome of business function. 10 | * 11 | * It is ~ Future> (in vavr style0 12 | * the reason for not using vavr here was: 13 | * - making critical api less depending on vavr 14 | * - some efficiency (when result is in fdact immediate (see InstantOut) 15 | * 16 | * 17 | */ 18 | sealed class Out { 19 | 20 | abstract fun map(f: (A) -> B): Out 21 | 22 | abstract fun mapLeft(f: (E) -> E1): Out 23 | 24 | fun handle(fe: (E) -> Out, fa: (A) -> B): Out = 25 | 26 | this.mapLeft(fe).map { a -> Out.right(fa(a)) }.let { result: Out, Out> -> 27 | when (result) { 28 | is FutureOut -> FutureOut(result.futureVal.map { 29 | Either.right>(it.merge()) 30 | }).flatMap { it } 31 | is InstantOut -> result.v.merge() 32 | } 33 | } 34 | 35 | companion object { 36 | fun left(e: E): Out = InstantOut(Either.left(e)) 37 | fun right(a: A): Out = InstantOut(Either.right(a)) 38 | 39 | fun fromFuture(future: Future>): Out = FutureOut(future) 40 | fun right(future: Future): Out = fromFuture(future.map { Either.right(it) }) 41 | } 42 | 43 | internal class InstantOut(internal val v: Either) : Out() { 44 | fun toFutureInternal(): Future> = Future.successful(v) 45 | 46 | // override fun onComplete(f: (Either) -> Unit) = f(v) 47 | 48 | override fun map(f: (A) -> B): Out = InstantOut(v.map(f)) 49 | 50 | override fun mapLeft(f: (E) -> E1): Out = InstantOut(v.mapLeft(f)) 51 | 52 | fun flatMapInternal(f: (A) -> Out): Out = 53 | v.map { a: A -> 54 | when (val res = f(a)) { 55 | is InstantOut -> InstantOut(res.v) 56 | is FutureOut -> FutureOut(res.futureVal) 57 | } 58 | }.mapLeft { _: E -> 59 | @Suppress("UNCHECKED_CAST") 60 | this as Out 61 | }.merge() 62 | 63 | fun k(): suspend () -> Either = { v } 64 | } 65 | 66 | internal class FutureOut(internal val futureVal: Future>) : Out() { 67 | fun toFutureInternal(): Future> = futureVal 68 | 69 | override fun map(f: (A) -> B): Out = FutureOut(futureVal.map { it.map(f) }) 70 | 71 | override fun mapLeft(f: (E) -> E1): Out = FutureOut(futureVal.map { it.mapLeft(f) }) 72 | 73 | fun flatMapInternal(f: (A) -> Out): Out = 74 | FutureOut(futureVal.flatMap { e: Either -> 75 | e.map { a: A -> 76 | val z: Future> = when (val res = f(a)) { 77 | is FutureOut -> res.futureVal 78 | is InstantOut -> Future.successful(futureVal.executor(), res.v) 79 | } 80 | z 81 | }.mapLeft { e1 -> Future.successful(futureVal.executor(), Either.left(e1)) } 82 | .merge() 83 | }) 84 | 85 | fun k(): suspend () -> Either = { 86 | futureVal.toCompletableFuture().await() 87 | } 88 | } 89 | } 90 | 91 | fun Out.flatMap(f: (A) -> Out): Out = 92 | when (this) { 93 | is Out.InstantOut -> this.flatMapInternal(f) 94 | is Out.FutureOut -> this.flatMapInternal(f) 95 | } 96 | 97 | fun Out.toFuture(): Future> = when (this) { 98 | is Out.InstantOut -> this.toFutureInternal() 99 | is Out.FutureOut -> this.toFutureInternal() 100 | } 101 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/async/AsyncEffect.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.async 2 | 3 | import dev.neeffect.nee.Effect 4 | import dev.neeffect.nee.effects.Out 5 | import dev.neeffect.nee.effects.utils.Logging 6 | import dev.neeffect.nee.effects.utils.logger 7 | import io.vavr.concurrent.Future 8 | import io.vavr.concurrent.Promise 9 | import io.vavr.control.Either 10 | import io.vavr.control.Option 11 | import java.util.concurrent.Executor 12 | import java.util.concurrent.Executors 13 | import java.util.concurrent.atomic.AtomicLong 14 | 15 | /** 16 | * Technical interface for threading model. 17 | * 18 | * Allows intrhead or real async execution. 19 | */ 20 | interface ExecutionContext { 21 | fun execute(f: () -> T): Future 22 | } 23 | 24 | /** 25 | * Provider of execution context. 26 | */ 27 | interface ExecutionContextProvider { 28 | /** 29 | * Find correct execution context. 30 | * 31 | * Implementation may choose to allow overriding by local. 32 | */ 33 | fun findExecutionContext(local: Option): ExecutionContext 34 | } 35 | 36 | /** 37 | * In thread execution (immediate). 38 | */ 39 | class SyncExecutionContext : ExecutionContext { 40 | override fun execute(f: () -> T): Future = 41 | Future.successful(f()) 42 | } 43 | 44 | @Suppress("ReturnUnit") 45 | object InPlaceExecutor : Executor { 46 | override fun execute(command: Runnable) = command.run() 47 | } 48 | 49 | /* maybe we do not need this radical one 50 | object NoGoExecutor : Executor { 51 | override fun execute(command: Runnable) { 52 | System.err.println("someone called NoGoExecutor") 53 | Thread.dumpStack() 54 | exitProcess(2) 55 | } 56 | } 57 | */ 58 | 59 | class ExecutorExecutionContext(private val executor: Executor) : ExecutionContext, Logging { 60 | @Suppress("TooGenericExceptionCaught") 61 | override fun execute(f: () -> T): Future = 62 | Promise.make(InPlaceExecutor).let { promise -> 63 | executor.execute { 64 | // LESSON not property handled exception 65 | try { 66 | val result = f() 67 | promise.success(result) 68 | } catch (e: Exception) { 69 | // NO TEST 70 | promise.failure(e) 71 | } catch (e: Throwable) { 72 | logger().error("Unhandled throwable in executor", e) 73 | promise.failure(e) 74 | } 75 | } 76 | promise.future() 77 | } 78 | } 79 | 80 | class ECProvider(private val ectx: ExecutionContext, private val localWins: Boolean = true) : ExecutionContextProvider { 81 | override fun findExecutionContext(local: Option): ExecutionContext = 82 | local.map { localCtx -> 83 | if (localWins) { 84 | localCtx 85 | } else { 86 | ectx 87 | } 88 | }.getOrElse(ectx) 89 | } 90 | 91 | /** 92 | * Aynchority effect. 93 | * 94 | * Local execution context might be allowed. 95 | */ 96 | class AsyncEffect( 97 | val localExecutionContext: Option = Option.none() 98 | ) : Effect, Logging { 99 | 100 | @Suppress("TooGenericExceptionCaught", "ThrowExpression") 101 | override fun wrap(f: (R) -> A): (R) -> Pair, R> = 102 | { r: R -> 103 | val asyncNmb = asyncCounter.getAndIncrement() 104 | 105 | Pair(run { 106 | val ec = r.findExecutionContext(this.localExecutionContext) 107 | logger().debug("initiated async ($asyncNmb)") 108 | val async = AsyncSupport.initiateAsync(r) 109 | val result = ec.execute { 110 | logger().debug("started async ($asyncNmb)") 111 | try { 112 | f(r) 113 | } catch (e: Throwable) { 114 | logger().error("error in async handling", e) 115 | throw e 116 | } finally { 117 | logger().debug("done async ($asyncNmb)") 118 | } 119 | } 120 | Out.FutureOut(result.map { 121 | Either.right(it.also { 122 | logger().debug("cleaning async ($asyncNmb)") 123 | async.closeAsync(r) 124 | }) 125 | }) 126 | }, r) 127 | } 128 | 129 | companion object { 130 | private val asyncCounter = AtomicLong() 131 | } 132 | } 133 | 134 | class ThreadedExecutionContextProvider(threads: Int = 4) : ExecutionContextProvider { 135 | val executor = Executors.newFixedThreadPool(threads) 136 | val executorUsingContext = ExecutorExecutionContext(executor) 137 | override fun findExecutionContext(local: Option): ExecutionContext 138 | = local.getOrElse(executorUsingContext) 139 | 140 | } 141 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/cache/CacheEffect.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.cache 2 | 3 | import dev.neeffect.nee.Effect 4 | import dev.neeffect.nee.effects.Out 5 | import java.util.concurrent.ConcurrentHashMap 6 | 7 | class CacheEffect( 8 | private val p: P, 9 | private val cacheProvider: CacheProvider 10 | ) : Effect { 11 | override fun wrap(f: (R) -> A): (R) -> Pair, R> = { r: R -> 12 | Pair( 13 | Out.right(cacheProvider.computeIfAbsent(p, { f(r) })), r 14 | ) 15 | } 16 | } 17 | 18 | interface CacheProvider { 19 | fun computeIfAbsent(key: K, func: (K) -> V): V 20 | } 21 | 22 | @Suppress("MutableCollections") 23 | class NaiveCacheProvider : CacheProvider { 24 | private val map: ConcurrentHashMap = ConcurrentHashMap() 25 | 26 | @Suppress("UNCHECKED_CAST") 27 | override fun computeIfAbsent(key: K, func: (K) -> V): V = 28 | map.computeIfAbsent(key as Any) { k: Any -> func(k as K) as Any } as V 29 | } 30 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/env/FlexibleEnv.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.env 2 | 3 | import io.vavr.control.Option 4 | import io.vavr.control.Option.some 5 | import kotlin.reflect.KClass 6 | 7 | /** 8 | * Key for a resource to get. 9 | */ 10 | data class ResourceId(val clazz: KClass, val key: Any = DefaultKey) { 11 | object DefaultKey 12 | } 13 | 14 | /** 15 | * Allows for runtime expandable Environment. 16 | * 17 | * This neglects type safety of R, but might be in fact way easier to use. 18 | * TODO actually needed sealed class but it did not work 19 | */ 20 | interface FlexibleEnv { 21 | fun get(id: ResourceId): Option 22 | fun set(id: ResourceId, t: T): FlexibleEnv 23 | 24 | companion object { 25 | inline fun create( 26 | id: ResourceId, 27 | t: T 28 | ): FlexibleEnv = 29 | WrappedEnv(t, id, EnvLeaf) 30 | 31 | inline fun create( 32 | t: T 33 | ): FlexibleEnv = create(ResourceId(T::class), t) 34 | 35 | fun empty(): FlexibleEnv = EnvLeaf 36 | } 37 | } 38 | 39 | /** 40 | * Add next type to env. 41 | */ 42 | inline fun FlexibleEnv.with( 43 | t: T 44 | ): FlexibleEnv = with(ResourceId(T::class), t) 45 | 46 | inline fun FlexibleEnv.with( 47 | id: ResourceId, 48 | t: T 49 | ): FlexibleEnv = WrappedEnv(t, id, this) 50 | 51 | @Suppress("ThrowExpression") 52 | object EnvLeaf : FlexibleEnv { 53 | override fun get(id: ResourceId): Option = Option.none() 54 | 55 | override fun set(id: ResourceId, t: T): FlexibleEnv = 56 | throw IllegalArgumentException("Impossible to set resource of type $id") 57 | } 58 | 59 | /** 60 | * Node instance of flexibleEnv 61 | */ 62 | data class WrappedEnv( 63 | private val env: Y, 64 | private val resId: ResourceId, 65 | private val inner: FlexibleEnv 66 | ) : FlexibleEnv { 67 | override fun get(id: ResourceId): Option = 68 | if (id == resId) { 69 | @Suppress("UNCHECKED_CAST") 70 | some(env) as Option 71 | } else { 72 | inner.get(id) 73 | } 74 | 75 | override fun set(id: ResourceId, t: T): FlexibleEnv = 76 | if (id == resId) { 77 | WrappedEnv(t, id, inner) 78 | } else { 79 | WrappedEnv(env, resId, inner.set(id, t)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/monitoring/EntryType.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.monitoring 2 | 3 | import com.fasterxml.jackson.annotation.JsonTypeInfo 4 | 5 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) 6 | sealed class EntryType { 7 | object Begin : EntryType() 8 | 9 | data class End(val elapsedTime: Long) : EntryType() 10 | 11 | data class InternalError(val msg: String) : EntryType() { 12 | override fun toString(): String = "Error($msg)" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/monitoring/LogsAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.monitoring 2 | 3 | import io.vavr.collection.HashMap 4 | import io.vavr.collection.List 5 | import io.vavr.collection.Map 6 | import io.vavr.collection.Seq 7 | import io.vavr.collection.Stream 8 | import java.util.UUID 9 | import kotlin.collections.fold 10 | 11 | class LogsAnalyzer { 12 | fun processLogs(logs: Seq): LogsReport = logs.foldLeft(InvocationAccumulator()) { a, b -> 13 | a.addLog(b.traceEntry) 14 | }.group() 15 | } 16 | 17 | data class LogsReport(val classes: Seq = List.empty()) 18 | 19 | data class ClassReport(val className: String, val functions: Stream) 20 | 21 | data class FunctionReport( 22 | val codeLocation: CodeLocation, 23 | val invocationCount: Long, 24 | val totalTime: Long, 25 | val errorsCount: Long 26 | ) { 27 | operator fun plus(other: FunctionReport) = 28 | FunctionReport( 29 | this.codeLocation, 30 | this.invocationCount + other.invocationCount, 31 | this.totalTime + other.totalTime, 32 | this.errorsCount + other.errorsCount 33 | ) 34 | } 35 | 36 | data class InvocationAccumulator( 37 | val starts: Map = HashMap.empty(), 38 | val reports: HashMap = HashMap.empty() 39 | ) { 40 | 41 | fun addLog(logEntry: LogEntry): InvocationAccumulator = 42 | when (logEntry.message) { 43 | is EntryType.Begin -> this.copy(starts = starts.put(logEntry.uuid, logEntry)) 44 | is EntryType.End -> markEnd(logEntry) 45 | is EntryType.InternalError -> addError(logEntry) 46 | } 47 | 48 | private fun addError(logEntry: LogEntry) = 49 | FunctionReport(logEntry.codeLocation, 0, 0, 1).let { errorInv -> 50 | this.copy(reports = addReports(this.reports, errorInv)) 51 | } 52 | 53 | private fun markEnd(logEntry: LogEntry): InvocationAccumulator = 54 | starts.get(logEntry.uuid).map { existingEntry -> 55 | addInvocation(existingEntry, logEntry) 56 | }.getOrElse { 57 | addError(logEntry) 58 | } 59 | 60 | private fun addInvocation(existingEntry: LogEntry, logEntry: LogEntry): InvocationAccumulator = 61 | makeReport(existingEntry, logEntry).let { invocationReport -> 62 | addReports(this.reports, invocationReport) 63 | }.let { reports -> 64 | this.copy(starts = starts.remove(existingEntry.uuid), reports = reports) 65 | } 66 | 67 | private fun addReports(reports: HashMap, rep: FunctionReport) = 68 | reports.put(rep.codeLocation, rep) { prev, newVal -> 69 | prev + newVal 70 | } 71 | 72 | private fun makeReport(existingEntry: LogEntry, logEntry: LogEntry): FunctionReport = 73 | FunctionReport(logEntry.codeLocation, 1, logEntry.time - existingEntry.time, 0) 74 | 75 | fun group(): LogsReport = this.reports.values().groupBy { 76 | it.codeLocation.className 77 | }.iterator().fold(LogsReport()) { rep, b -> 78 | rep.copy(classes = rep.classes.append(ClassReport(b._1 ?: "???", b._2))) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/monitoring/LogsProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.monitoring 2 | 3 | import io.vavr.collection.Seq 4 | 5 | interface LogsProvider { 6 | fun getLogs(): Seq 7 | 8 | fun getReport(): LogsReport 9 | } 10 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/monitoring/MutableInMemLogger.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.monitoring 2 | 3 | import io.vavr.collection.Seq 4 | import java.util.concurrent.atomic.AtomicReference 5 | 6 | class MutableInMemLogger : Logger, LogsProvider { 7 | private val logsAnalyzer = LogsAnalyzer() 8 | 9 | private val internal = AtomicReference(SimpleBufferedLogger()) 10 | 11 | override fun log(entry: LogEntry): MutableInMemLogger = 12 | internal.updateAndGet { it.log(entry) }.let { this } 13 | 14 | override fun getLogs(): Seq = internal.get().getLogs() 15 | 16 | override fun getReport(): LogsReport = logsAnalyzer.processLogs(internal.get().getLogs()) 17 | } 18 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/monitoring/SimpleBufferedLogger.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.monitoring 2 | 3 | import io.vavr.collection.Seq 4 | import io.vavr.collection.Vector 5 | 6 | data class SimpleBufferedLogger(private val buffer: Vector = Vector.empty()) : 7 | Logger { 8 | 9 | override fun log(entry: LogEntry) = 10 | this.copy(buffer.append(LogMessage(entry))) 11 | 12 | fun getLogs(): Seq = buffer 13 | } 14 | 15 | data class LogMessage(val traceEntry: LogEntry) 16 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/monitoring/SimpleTraceProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.monitoring 2 | 3 | class SimpleTraceProvider(val res: TraceResource) : TraceProvider { 4 | override fun getTrace(): TraceResource = res 5 | 6 | override fun setTrace(newState: TraceResource): SimpleTraceProvider = SimpleTraceProvider(newState) 7 | } 8 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/security/DummySecurityProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.security 2 | 3 | import dev.neeffect.nee.effects.Out 4 | 5 | class DummySecurityProvider : SecurityProvider { 6 | override fun getSecurityContext(): Out> = 7 | Out.left(SecurityErrorType.NoSecurityCtx) 8 | } 9 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/security/FlexSecEffect.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.security 2 | 3 | import dev.neeffect.nee.Effect 4 | import dev.neeffect.nee.effects.Out 5 | import dev.neeffect.nee.effects.env.FlexibleEnv 6 | import dev.neeffect.nee.effects.env.ResourceId 7 | import io.vavr.collection.List 8 | 9 | /** 10 | * Security effect - flex version. 11 | */ 12 | class FlexSecEffect(private val roles: List) : 13 | Effect { 14 | private val internal = 15 | SecuredRunEffect>( 16 | roles 17 | ) 18 | 19 | override fun wrap(f: (FlexibleEnv) -> A): 20 | (FlexibleEnv) -> Pair, FlexibleEnv> = { env: FlexibleEnv -> 21 | val secProviderChance = env.get(ResourceId(SecurityProvider::class)) 22 | secProviderChance.map { _ -> 23 | val flexSecProvider = 24 | FlexSecurityProvider(env) 25 | val internalF = { _: SecurityProvider -> 26 | f(env) 27 | } 28 | val wrapped = internal.wrap(internalF) 29 | val result = wrapped(flexSecProvider) 30 | Pair(result.first, env.set(ResourceId(SecurityProvider::class), result.second)) 31 | }.getOrElse( 32 | Pair( 33 | Out.left( 34 | SecurityErrorType.NoSecurityCtx 35 | ), env 36 | ) 37 | ) 38 | } 39 | } 40 | 41 | /** 42 | * Provider of flex sec. 43 | */ 44 | class FlexSecurityProvider(private val env: FlexibleEnv) : 45 | FlexibleEnv by env, 46 | SecurityProvider { 47 | @Suppress("UNCHECKED_CAST") 48 | override fun getSecurityContext(): Out> = 49 | env.get(ResourceId(SecurityProvider::class)).map { it.getSecurityContext() } 50 | .getOrElse(Out.left>(SecurityErrorType.NoSecurityCtx)) 51 | as Out> 52 | } 53 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/security/SecEffect.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.security 2 | 3 | import dev.neeffect.nee.Effect 4 | import dev.neeffect.nee.effects.Out 5 | import dev.neeffect.nee.effects.flatMap 6 | import io.vavr.collection.List 7 | 8 | interface SecurityCtx { 9 | fun getCurrentUser(): Out 10 | fun hasRole(role: ROLE): Boolean 11 | } 12 | 13 | interface SecurityProvider { 14 | fun getSecurityContext(): Out> 15 | } 16 | 17 | interface SecurityError { 18 | fun secError(): SecurityErrorType 19 | } 20 | 21 | /** 22 | * Error on security check. 23 | */ 24 | sealed class SecurityErrorType : SecurityError { 25 | override fun secError() = this 26 | 27 | /** 28 | * Credentials were wrong. 29 | */ 30 | class WrongCredentials(val message: String = "") : SecurityErrorType() 31 | 32 | /** 33 | * User not recognized. 34 | */ 35 | object UnknownUser : SecurityErrorType() 36 | 37 | /** 38 | * Security context nott available. 39 | */ 40 | object NoSecurityCtx : SecurityErrorType() 41 | 42 | /** 43 | * Credential cannot be parsed. 44 | */ 45 | data class MalformedCredentials(val message: String = "") : SecurityErrorType() 46 | 47 | /** 48 | * Expected role is missing. 49 | */ 50 | data class MissingRole(val roles: List) : SecurityErrorType() 51 | } 52 | 53 | class SecuredRunEffect>( 54 | private val roles: List 55 | ) : Effect { 57 | 58 | constructor(singleRole: ROLE) : this(List.of(singleRole)) 59 | 60 | override fun wrap(f: (R) -> A): (R) -> Pair, R> = { provider: R -> 61 | Pair( // TODO - fail faster? 62 | 63 | provider.getSecurityContext().flatMap { securityCtx -> 64 | val missingRoles = roles.filter { role -> 65 | !securityCtx.hasRole(role) 66 | } 67 | if (missingRoles.isEmpty) { 68 | Out.right(f(provider)) 69 | } else { 70 | Out.left(SecurityErrorType.MissingRole(missingRoles)) 71 | } 72 | }, provider 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/test/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.test 2 | 3 | import dev.neeffect.nee.effects.Out 4 | import io.vavr.control.Either 5 | 6 | fun Out.get(): A = when (this) { 7 | is Out.InstantOut -> this.v.get() 8 | is Out.FutureOut -> this.futureVal.get().get() 9 | } 10 | 11 | fun Out.getLeft(): E = when (this) { 12 | is Out.InstantOut -> this.v.left 13 | is Out.FutureOut -> this.futureVal.get().left 14 | } 15 | 16 | fun Out.getAny(): Either = when (this) { 17 | is Out.InstantOut -> this.v 18 | is Out.FutureOut -> this.futureVal.get() 19 | } 20 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/time/TimeProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.time 2 | 3 | import io.haste.Haste 4 | import io.haste.TimeSource 5 | 6 | interface TimeProvider { 7 | fun getTimeSource(): TimeSource 8 | } 9 | 10 | class HasteTimeProvider(private val timeSource: TimeSource = Haste.TimeSource.systemTimeSource()) : TimeProvider { 11 | override fun getTimeSource(): TimeSource = timeSource 12 | } 13 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/tx/DummyTxProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.tx 2 | 3 | import dev.neeffect.nee.effects.utils.invalid 4 | 5 | object DummyTxProvider : TxProvider { 6 | override fun getConnection(): TxConnection = invalid() 7 | 8 | override fun setConnectionState(newState: TxConnection): DummyTxProvider = 9 | this 10 | } 11 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/tx/TxConnection.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.tx 2 | 3 | import io.vavr.control.Either 4 | import io.vavr.control.Option 5 | import java.io.Closeable 6 | 7 | interface TxConnection : Closeable { 8 | fun begin(): Either> 9 | 10 | fun continueTx(): Either> 11 | 12 | fun hasTransaction(): Boolean 13 | 14 | fun getResource(): R 15 | } 16 | 17 | interface TxStarted : TxConnection { 18 | fun commit(): Pair, TxConnection> 19 | 20 | fun rollback(): Pair, TxConnection> 21 | } 22 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/tx/TxFlex.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.tx 2 | 3 | import dev.neeffect.nee.Effect 4 | import dev.neeffect.nee.effects.Out 5 | import dev.neeffect.nee.effects.env.FlexibleEnv 6 | import dev.neeffect.nee.effects.env.ResourceId 7 | import dev.neeffect.nee.effects.env.with 8 | import dev.neeffect.nee.effects.tx.FlexTxProvider.Companion.txProviderResource 9 | import io.vavr.control.Option 10 | 11 | /** 12 | * Transaction (flexible env version). 13 | */ 14 | class FlexTxEffect : Effect { 15 | private val internal = TxEffect>() 16 | 17 | override fun wrap(f: (FlexibleEnv) -> A): (FlexibleEnv) -> Pair, FlexibleEnv> = 18 | { env: FlexibleEnv -> 19 | @Suppress("UNCHECKED_CAST") 20 | val providerChance = env.get(txProviderResource) 21 | as Option> 22 | providerChance.map { _ -> 23 | val flexProvider = FlexTxProvider(env) 24 | val internalF = { _: TxProvider -> 25 | f(env) 26 | } 27 | val wrapped = internal.wrap(internalF) 28 | val result = wrapped(flexProvider) 29 | Pair(result.first, result.second.env) 30 | }.getOrElse(Pair(Out.left(TxErrorType.NoConnection), env)) 31 | } 32 | } 33 | 34 | internal class FlexTxProvider(internal val env: FlexibleEnv) : 35 | TxProvider> { 36 | override fun getConnection(): TxConnection = 37 | env.get(txProviderResource).map { 38 | @Suppress("UNCHECKED_CAST") 39 | it.getConnection() as TxConnection 40 | }.getOrElseThrow { 41 | IllegalStateException("no connection for tx") 42 | } 43 | 44 | @Suppress("UNCHECKED_CAST") 45 | override fun setConnectionState(newState: TxConnection): FlexTxProvider = env.get(txProviderResource) 46 | .map { provider -> 47 | val p = provider as TxProvider 48 | val newProvider = p.setConnectionState(newState) as TxProvider 49 | val newEnv = env.set(txProviderResource, newProvider) 50 | FlexTxProvider(newEnv) 51 | }.getOrElseThrow { 52 | IllegalStateException("no connection provider") 53 | } 54 | 55 | companion object { 56 | val txProviderResource = ResourceId(TxProvider::class) 57 | 58 | @Suppress("UNCHECKED_CAST") 59 | fun connection(env: FlexibleEnv): R = 60 | env.get(txProviderResource).map { 61 | it.getConnection() as TxConnection 62 | }.map { 63 | it.getResource() 64 | }.getOrElseThrow { java.lang.IllegalStateException("Connection provider must be available") } 65 | } 66 | } 67 | 68 | fun > FlexibleEnv.withTxProvider(provider: TxProvider) = 69 | this.with(txProviderResource, provider) 70 | 71 | @Suppress("UNCHECKED_CAST") 72 | inline fun ((T) -> A).flex(): (FlexibleEnv) -> A = 73 | this as (FlexibleEnv) -> A 74 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/utils/Logging.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.utils 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory 5 | 6 | /** 7 | * Marker interface for diagnostic log support. 8 | */ 9 | interface Logging 10 | 11 | /** 12 | * Use it to log using slf4j. 13 | */ 14 | inline fun T.logger(): Logger = 15 | LoggerFactory.getLogger(T::class.java) 16 | -------------------------------------------------------------------------------- /nee-core/src/main/kotlin/dev/neeffect/nee/effects/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.utils 2 | 3 | import dev.neeffect.nee.effects.Out 4 | import dev.neeffect.nee.effects.monitoring.CodeNameFinder.guessCodePlaceName 5 | import dev.neeffect.nee.effects.monitoring.TraceProvider 6 | import io.vavr.Tuple2 7 | import io.vavr.Tuple3 8 | import io.vavr.control.Either 9 | 10 | @Suppress("BranchStatement") 11 | internal fun trace(f: (R) -> A) = guessCodePlaceName(2).let { placeName -> 12 | { r: R -> 13 | Out.right(f(r)).also { 14 | if (r is TraceProvider<*>) { 15 | r.getTrace().putGuessedPlace(placeName, f) 16 | } 17 | } 18 | } 19 | } 20 | 21 | @Suppress("BranchStatement") 22 | internal fun constR(f: A) = guessCodePlaceName(2).let { placeName -> 23 | { r: R -> 24 | Out.right(f).also { 25 | if (r is TraceProvider<*>) { 26 | r.getTrace().putGuessedPlace(placeName, f) 27 | } 28 | } 29 | } 30 | } 31 | 32 | // TODO - this is for lazy - rename it 33 | @Suppress("BranchStatement") 34 | internal fun ignoreR(f: () -> A) = guessCodePlaceName(2).let { placeName -> 35 | { r: Any -> 36 | f().also { 37 | if (r is TraceProvider<*>) { 38 | r.getTrace().putGuessedPlace(placeName, f) 39 | } 40 | } 41 | } 42 | } 43 | 44 | fun Either.merge() = getOrElseGet { it } 45 | 46 | fun tupled2(f: (ENV) -> (A, B) -> R) = 47 | { env: ENV -> 48 | { p: Tuple2 -> 49 | f(env)(p._1, p._2) 50 | } 51 | } 52 | 53 | fun tupled(f: (ENV) -> (A, B) -> R) = tupled2(f) 54 | 55 | fun tupled3(f: (ENV) -> (A, B, C) -> R) = 56 | { env: ENV -> 57 | { p: Tuple3 -> 58 | f(env)(p._1, p._2, p._3) 59 | } 60 | } 61 | 62 | /** 63 | * Marks invalid function (expected to not be called). 64 | */ 65 | @Suppress("ThrowExpression") 66 | fun invalid(): Nothing = throw NotImplementedError() 67 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/NEETest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee 2 | 3 | import dev.neeffect.nee.effects.test.get 4 | import dev.neeffect.nee.effects.tx.TestEffect 5 | import dev.neeffect.nee.effects.tx.TestResource 6 | import io.kotest.core.spec.style.BehaviorSpec 7 | import io.kotest.matchers.shouldBe 8 | 9 | internal class NEETest : BehaviorSpec({ 10 | Given("test effect and resource") { 11 | val effectLog = mutableListOf() 12 | val res = TestResource(1) 13 | val effect = TestEffect("neetest", effectLog) 14 | val m1 = Nee.with(effect) { _ -> 15 | 1 16 | } 17 | val m2 = { _: Int -> 18 | Nee.with(effect) { r -> 19 | r.version 20 | } 21 | } 22 | When("flatMapped") { 23 | val resutl = m1.flatMap(m2) 24 | .perform(res).get() 25 | Then("have correct env version") { 26 | resutl shouldBe 21 27 | } 28 | } 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/UUIDUtils.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee 2 | 3 | import java.util.* 4 | 5 | fun Pair.toUUID() = UUID(this.first, this.second) 6 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/async/AsyncEffectTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.async 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.effects.utils.ignoreR 5 | import io.kotest.core.spec.style.DescribeSpec 6 | import io.kotest.matchers.shouldBe 7 | import io.vavr.collection.List 8 | import io.vavr.concurrent.Future 9 | import io.vavr.concurrent.Promise 10 | import io.vavr.control.Option 11 | import java.util.concurrent.Executor 12 | import java.util.concurrent.atomic.AtomicBoolean 13 | import java.util.concurrent.atomic.AtomicReference 14 | 15 | class AsyncEffectTest : DescribeSpec({ 16 | describe("async context") { 17 | val controllableExecutionContext = ControllableExecutionContext() 18 | val ecProvider = ECProvider(controllableExecutionContext) 19 | val eff = AsyncEffect() 20 | describe("test function") { 21 | val runned = AtomicBoolean(false) 22 | val testFunction = { runned.set(true) } 23 | val async = Nee.Companion.with(eff, ignoreR(testFunction)) 24 | async.perform(ecProvider) 25 | 26 | it("does not run before executor calls") { 27 | runned.get() shouldBe false 28 | } 29 | it("runs after async trigerred") { 30 | controllableExecutionContext.runSingle() 31 | runned.get() shouldBe true 32 | } 33 | } 34 | describe("with local ec") { 35 | val localEC = ControllableExecutionContext() 36 | //val localProvider = ECProvider(controllableExecutionContext) 37 | val localEff = AsyncEffect(Option.some(localEC)) 38 | val runned = AtomicBoolean(false) 39 | val testFunction = { runned.set(true) } 40 | val async = Nee.Companion.with( 41 | localEff, 42 | ignoreR(testFunction) 43 | ) 44 | it("will not run on global") { 45 | async.perform(ecProvider) 46 | controllableExecutionContext.runSingle() 47 | runned.get() shouldBe false 48 | } 49 | 50 | it("will run on local ec") { 51 | localEC.runSingle() 52 | runned.get() shouldBe true 53 | } 54 | } 55 | } 56 | }) 57 | 58 | /** 59 | * Use it to test async code (async is called inThread). 60 | */ 61 | class ControllableExecutionContext : ExecutionContext, Executor { 62 | override fun execute(f: () -> T): Future = executef(f) 63 | 64 | override fun execute(command: Runnable): Unit = executef({ command.run() }).let { Unit } 65 | 66 | private val computations = AtomicReference(Computations()) 67 | 68 | private fun executef(f: () -> T): Future = 69 | Promise.make(InPlaceExecutor).let { promise -> 70 | val computation: Runnable = Runnable { 71 | try { 72 | val result = f() 73 | promise.success(result) 74 | } catch (e: Exception) { 75 | promise.failure(e) 76 | } 77 | } 78 | computations.updateAndGet { it.addOne(computation) } 79 | promise.future() 80 | } 81 | 82 | internal fun runSingle() = computations.updateAndGet { list -> 83 | list.removeOne() 84 | }.lastOne?.run() 85 | 86 | internal fun assertEmpty() = assert(this.computations.get().computations.isEmpty) 87 | } 88 | 89 | internal data class Computations(val computations: List = List.empty(), val lastOne: Runnable? = null) { 90 | fun addOne(f: Runnable) = copy( 91 | computations = computations.append(f), 92 | lastOne = null 93 | ) 94 | 95 | fun removeOne() = computations.headOption().map { runnable -> 96 | copy(computations = this.computations.pop(), lastOne = runnable) 97 | }.getOrElse(Computations()) 98 | } 99 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/async/AsyncStackTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.async 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | internal class AsyncStackTest : DescribeSpec({ 7 | describe("async stack") { 8 | val stack = CleanAsyncStack() 9 | val env = MyEnv(1) 10 | it("executes action on cleanup") { 11 | val dirty = stack.onClose { env -> 12 | env.copy(test = env.test + 7) 13 | } 14 | val cleaned = dirty.cleanUp(env) 15 | cleaned.second.test shouldBe 8 16 | } 17 | it("executes 2 actions on cleanup") { 18 | val dirty = stack.onClose { env -> 19 | env.copy(test = env.test + 7) 20 | }.onClose { env -> 21 | env.copy(test = env.test + 11) 22 | } 23 | val cleaned = dirty.cleanUp(env) 24 | 25 | cleaned.second.test shouldBe 19 26 | } 27 | } 28 | }) { 29 | 30 | data class MyEnv(val test: Long) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/async/AsyncTxTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.async 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.andThen 5 | import dev.neeffect.nee.anyError 6 | import dev.neeffect.nee.effects.test.get 7 | import dev.neeffect.nee.effects.test.getAny 8 | import dev.neeffect.nee.effects.tx.DBLike 9 | import dev.neeffect.nee.effects.tx.DBLikeProvider 10 | import dev.neeffect.nee.effects.tx.TxConnection 11 | import dev.neeffect.nee.effects.tx.TxEffect 12 | import dev.neeffect.nee.effects.tx.TxProvider 13 | import io.kotest.assertions.assertSoftly 14 | import io.kotest.core.spec.style.DescribeSpec 15 | import io.kotest.matchers.shouldBe 16 | import io.vavr.control.Either 17 | 18 | internal class AsyncTxTest : DescribeSpec({ 19 | describe("combined effect") { 20 | 21 | val action = Nee.Companion.with(combinedEffect) { env -> 22 | val connection = env.getConnection() 23 | if (connection.hasTransaction()) { 24 | "is trx" 25 | } else { 26 | "no trx" 27 | } 28 | } 29 | it("works in tx normally") { 30 | val db = DBLike() 31 | val initialEnv = AsyncEnv(DBLikeProvider(db), ecProvider) 32 | val result = action.perform(initialEnv) 33 | controllableExecutionContext.runSingle() 34 | val r1 = result.get() 35 | r1 shouldBe "is trx" 36 | } 37 | val nestedF = { prevResult: String -> 38 | Nee.with(combinedEffect) { env: AsyncEnv -> 39 | val connection = env.getConnection() 40 | val res = connection.getResource() 41 | if (connection.hasTransaction()) { 42 | "$prevResult+is trx" 43 | } else { 44 | "$prevResult+no trx" 45 | } 46 | } 47 | } 48 | 49 | val nestedAction = action.flatMap(nestedF) 50 | it("works in nested tx") { 51 | val db = DBLike() 52 | val initialEnv = AsyncEnv(DBLikeProvider(db), ecProvider) 53 | val result = nestedAction.perform(initialEnv) 54 | controllableExecutionContext.runSingle() 55 | controllableExecutionContext.runSingle() 56 | val r1 = result.getAny() 57 | r1 shouldBe Either.right("is trx+is trx") 58 | } 59 | it("works in double nested tx") { 60 | val dblNested = nestedAction.flatMap(nestedF) 61 | val db = DBLike() 62 | val initialEnv = AsyncEnv(DBLikeProvider(db), ecProvider) 63 | val result = dblNested.perform(initialEnv) 64 | controllableExecutionContext.runSingle() 65 | controllableExecutionContext.runSingle() 66 | controllableExecutionContext.runSingle() 67 | 68 | controllableExecutionContext.assertEmpty() 69 | 70 | val r1 = result.getAny() 71 | assertSoftly { 72 | r1 shouldBe Either.right("is trx+is trx+is trx") 73 | db.transactionLevel() shouldBe 0 74 | } 75 | } 76 | } 77 | }) { 78 | internal companion object { 79 | 80 | val dbLikeEffect = TxEffect() 81 | val controllableExecutionContext = ControllableExecutionContext() 82 | val ecProvider = ECProvider(controllableExecutionContext) 83 | val asyncEffect = AsyncEffect() 84 | val combinedEffect = asyncEffect.andThen(dbLikeEffect).anyError() 85 | 86 | } 87 | } 88 | 89 | internal data class AsyncEnv( 90 | val db: DBLikeProvider, 91 | val ex: ExecutionContextProvider, 92 | val asyncEnv: AsyncEnvWrapper = AsyncEnvWrapper() 93 | ) : 94 | TxProvider, ExecutionContextProvider by ex, AsyncSupport by asyncEnv { 95 | 96 | override fun getConnection(): TxConnection = this.db.getConnection() 97 | 98 | override fun setConnectionState(newState: TxConnection) = this.copy(db = db.setConnectionState(newState)) 99 | 100 | } 101 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/cache/CacheEffectTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.cache 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.effects.flatMap 5 | import dev.neeffect.nee.effects.test.get 6 | import io.kotest.core.spec.style.BehaviorSpec 7 | import io.kotest.matchers.shouldBe 8 | 9 | internal class CacheEffectTest : BehaviorSpec({ 10 | Given("cache effect and naive implementation") { 11 | val cacheProvider = NaiveCacheProvider() 12 | val cache = { p: Int -> CacheEffect(p, cacheProvider) } 13 | When("function called twice using same param and different env") { 14 | fun businessFunction(p: Int) = 15 | Nee.with( 16 | cache(p), ::returnEnvIgnoringParam 17 | ) 18 | 19 | val x1 = businessFunction(1).perform(env = Env.SomeValue) 20 | val x2 = businessFunction(1).perform(env = Env.OtherValue) 21 | Then("second call should ignore different env") { 22 | x2.get() shouldBe x1.get() 23 | } 24 | Then("second call should return first stored value") { 25 | x2.get() shouldBe Env.SomeValue 26 | } 27 | } 28 | When("function called twice using different params and env") { 29 | fun businessFunction(p: Int) = 30 | Nee.with( 31 | cache(p), ::returnEnvIgnoringParam 32 | ) 33 | 34 | val x2 = businessFunction(1).perform(env = Env.SomeValue).flatMap { _ -> 35 | businessFunction(2).perform(env = Env.OtherValue) 36 | } 37 | Then("second call should return other env value") { 38 | x2.get() shouldBe Env.OtherValue 39 | } 40 | } 41 | } 42 | }) 43 | 44 | fun returnEnvIgnoringParam(env: Env) = env 45 | 46 | sealed class Env { 47 | object SomeValue : Env() 48 | object OtherValue : Env() 49 | } 50 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/coroutines/CoroutinesTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.coroutines 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.effects.test.get 5 | import dev.neeffect.nee.go 6 | import dev.neeffect.nee.runNee 7 | import io.kotest.core.spec.style.DescribeSpec 8 | import io.kotest.matchers.shouldBe 9 | import kotlinx.coroutines.runBlocking 10 | 11 | class CoroutinesTest : DescribeSpec({ 12 | describe("Nee in coroutines") { 13 | val x = Nee.pure { 14 | 1.also { 15 | println("x") 16 | } 17 | } 18 | val y = 19 | Nee.pure { 20 | 2.also { 21 | println("y") 22 | } 23 | } 24 | it("should be processed") { 25 | val z: Int = runBlocking { 26 | with(1) { 27 | val a: Int = x.go()() 28 | val b = y.go()() + a 29 | b 30 | } 31 | } 32 | z shouldBe 3 33 | } 34 | } 35 | 36 | describe("Nee from coroutines") { 37 | it("should create Nee instance") { 38 | val x = suspend { 1 } 39 | val result = runNee { 40 | x() 41 | } 42 | result.perform(Unit).get() shouldBe 1 43 | } 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/env/FlexibleEnvTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.env 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.vavr.control.Option 6 | import io.vavr.kotlin.some 7 | 8 | class FlexibleEnvTest : BehaviorSpec({ 9 | Given("Simple env") { 10 | val res = MyResource("test value") 11 | val env = FlexibleEnv.create(res) 12 | When("asked for env") { 13 | val value = env.get(ResourceId(MyResource::class)) 14 | Then("resource is returned") { 15 | value shouldBe some(res) 16 | } 17 | } 18 | When("asked for non existing res") { 19 | val value = env.get(ResourceId(String::class)) 20 | Then("no result is given") { 21 | value shouldBe Option.none() 22 | } 23 | } 24 | And("extended with another env") { 25 | val another = MyOtherResource("test2") 26 | val anotherResKey = ResourceId(MyOtherResource::class) 27 | val env2 = env.with(anotherResKey, another) 28 | When("asked for another") { 29 | val anotherVal = env2.get(anotherResKey) 30 | Then("another value is given") { 31 | anotherVal shouldBe some(another) 32 | } 33 | } 34 | When("asked for initial val") { 35 | val value = env2.get(ResourceId(MyResource::class)) 36 | Then("resource is returned") { 37 | value shouldBe some(res) 38 | } 39 | } 40 | } 41 | } 42 | }) { 43 | 44 | data class MyResource(val internalVal: String) 45 | data class MyOtherResource(val internalVal: String) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/monitoring/LogsAnalyzerTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.monitoring 2 | 3 | import dev.neeffect.nee.toUUID 4 | import io.kotest.core.spec.style.DescribeSpec 5 | import io.kotest.matchers.shouldBe 6 | import io.vavr.collection.List 7 | 8 | internal class LogsAnalyzerTest : DescribeSpec({ 9 | describe("logsAnalyzer") { 10 | val logsAnalyzer = LogsAnalyzer() 11 | describe("simple report") { 12 | val simpleReport = logsAnalyzer.processLogs(simpleSeq.map(::LogMessage)) 13 | it("registers single invocation ") { 14 | simpleReport.classes[0].functions[0].invocationCount shouldBe 1 15 | } 16 | 17 | it("calculates single invocation time") { 18 | simpleReport.classes[0].functions[0].totalTime shouldBe 100 19 | } 20 | } 21 | describe("report with 3 invocations") { 22 | val report = logsAnalyzer.processLogs(tripleSeq.map(::LogMessage)) 23 | it("registers 3 invocation ") { 24 | report.classes[0].functions[0].invocationCount shouldBe 3 25 | } 26 | it("registers total time ") { 27 | report.classes[0].functions[0].totalTime shouldBe 300 28 | } 29 | it("has only one class") { 30 | report.classes.size() shouldBe 1 31 | } 32 | } 33 | describe("nested calls report") { 34 | val report = logsAnalyzer.processLogs(nestedSeq.map(::LogMessage)) 35 | it("registers single invocation per each function ") { 36 | report.classes[0].functions[0].invocationCount shouldBe 1 37 | } 38 | 39 | it("finds two functions") { 40 | report.classes[0].functions.size() shouldBe 2 41 | } 42 | } 43 | describe("broken calls report (missing start)") { 44 | val report = logsAnalyzer.processLogs(brokenSeq.map(::LogMessage)) 45 | it("registers single error ") { 46 | report.classes[0].functions[0].errorsCount shouldBe 1 47 | } 48 | } 49 | 50 | describe("error calls is reported") { 51 | val report = logsAnalyzer.processLogs(errorSeq.map(::LogMessage)) 52 | it("registers single error ") { 53 | report.classes[0].functions[0].errorsCount shouldBe 1 54 | } 55 | } 56 | 57 | 58 | } 59 | }) { 60 | companion object { 61 | val a1CodeLocation = CodeLocation(functionName = "a1", className = "c1") 62 | val b1CodeLocation = CodeLocation(functionName = "b1", className = "c1") 63 | 64 | val a1Entry = LogEntry("x", Pair(0L, 1L).toUUID(), 1, a1CodeLocation, EntryType.Begin) 65 | val a1EntryEnd = a1Entry.copy(time = a1Entry.time + 100, message = EntryType.End(100)) 66 | val b1Entry = LogEntry("x", Pair(0L, 2L).toUUID(), 10, b1CodeLocation, EntryType.Begin) 67 | val b1EntryEnd = b1Entry.copy(time = b1Entry.time + 50, message = EntryType.End(40)) 68 | 69 | val a1EntryError = a1Entry.copy(time = a1Entry.time + 100, message = EntryType.InternalError("test")) 70 | 71 | 72 | val simpleSeq = List.of(a1Entry, a1EntryEnd) 73 | val tripleSeq = simpleSeq.prependAll(simpleSeq).prependAll(simpleSeq) 74 | val nestedSeq = List.of(a1Entry, b1Entry, b1EntryEnd, a1EntryEnd) 75 | 76 | val brokenSeq = List.of(a1Entry, b1EntryEnd, a1EntryEnd) 77 | 78 | val errorSeq = List.of(a1Entry, a1EntryError, a1EntryEnd) 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/monitoring/TraceEffectTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.monitoring 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.NoEffect 5 | import dev.neeffect.nee.effects.test.get 6 | import dev.neeffect.nee.effects.utils.ignoreR 7 | import io.kotest.core.spec.style.BehaviorSpec 8 | import io.kotest.matchers.shouldBe 9 | import io.kotest.matchers.string.shouldContain 10 | import io.kotest.matchers.string.shouldStartWith 11 | import io.vavr.collection.List 12 | import pl.outside.code.ExternalObject 13 | import java.util.concurrent.atomic.AtomicLong 14 | 15 | internal class TraceEffectTest : BehaviorSpec({ 16 | Given("Trace") { 17 | val eff = TraceEffect("tracerA") 18 | 19 | When("simple function process") { 20 | val logger = StoringLogger() 21 | var time = AtomicLong(100) 22 | val res = TraceResource("z1", logger, { time.get() }) 23 | val f = Nee.Companion.with(eff, ignoreR({ plainFunction(5) })) 24 | val result = f.perform(SimpleTraceProvider(res)) 25 | Then("result is ok") { 26 | result.get() shouldBe 6 27 | } 28 | } 29 | When("monitored function process") { 30 | val logger = StoringLogger() 31 | var time = AtomicLong(100) 32 | val res = TraceResource("z1", logger, { time.get() }) 33 | val f = Nee.Companion.with(eff, traceableFunction(5)) 34 | val result = f.perform(SimpleTraceProvider(res)) 35 | Then("result is ok") { 36 | result.get() shouldBe 6 37 | } 38 | } 39 | When("function is processed 100ms") { 40 | val logger = StoringLogger() 41 | var time = AtomicLong(100) 42 | val res = TraceResource("z1", logger, { time.get() }) 43 | val f = 44 | Nee.Companion.with(eff) { r -> 45 | time.updateAndGet { it + 100 * 1000 } 46 | } 47 | val result = f.perform(SimpleTraceProvider(res)) 48 | Then("time is measured") { 49 | logger.entries[1].time shouldBe (100L + 100000L) 50 | } 51 | } 52 | When("simple function in external code obj process") { 53 | val logger = StoringLogger() 54 | var time = AtomicLong(100) 55 | val res = TraceResource("z1", logger, { time.get() }) 56 | val f = Nee.Companion.with( 57 | eff, 58 | ignoreR({ ExternalObject.plainFunction(5) }) 59 | ) 60 | val result = f.perform(SimpleTraceProvider(res)) 61 | Then("result is ok") { 62 | result.get() shouldBe 6 63 | } 64 | Then("logs contain correctly guessed name") { 65 | logger.entries[1].codeLocation.className shouldContain "addWhen" 66 | } 67 | 68 | } 69 | When("monitored function in obj process") { 70 | val logger = StoringLogger() 71 | var time = AtomicLong(100) 72 | val res = TraceResource("z1", logger, { time.get() }) 73 | val f = Nee.Companion.with(eff, ExternalObject.traceableFunction(5)) 74 | val result = f.perform(SimpleTraceProvider(res)) 75 | Then("result is ok") { 76 | result.get() shouldBe 6 77 | } 78 | Then("logs contain correctly guessed name") { 79 | logger.entries[1].codeLocation.toString() shouldStartWith "pl.outside.code.ExternalObject\$traceableFunction" 80 | } 81 | } 82 | } 83 | Given("guessing code name function") { 84 | When("called simple function in Nee") { 85 | val noEffect = NoEffect() 86 | val result = Nee.constR(noEffect, ExternalObject::checkWhereCodeIsSimple).perform(Unit) 87 | Then("name of function recognized") { 88 | result.get() 89 | .toString() shouldStartWith "fun pl.outside.code.ExternalObject.checkWhereCodeIsSimple(kotlin.Unit)" 90 | } 91 | } 92 | 93 | When("called wrapped function in Nee") { 94 | val result = ExternalObject.checkWhereCodeIsNee().perform(Unit) 95 | Then("name of wrapped function recognized") { 96 | result.get().toString() shouldStartWith "pl.outside.code.ExternalObject\$checkWhereCodeIsNee\$" 97 | } 98 | } 99 | } 100 | }) { 101 | class StoringLogger(internal var entries: List = List.empty()) : Logger { 102 | override fun log(entry: LogEntry): StoringLogger { 103 | entries = entries.append(entry) 104 | return this 105 | } 106 | } 107 | } 108 | 109 | 110 | fun plainFunction(i: Int) = i + 1 111 | 112 | fun > traceableFunction(p: Int): (R) -> Int = 113 | { mon: R -> mon.getTrace().putNamedPlace().let { plainFunction(p) } } 114 | 115 | 116 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/time/HasteTimeProviderTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.time 2 | 3 | import io.haste.Haste 4 | import io.kotest.core.spec.style.DescribeSpec 5 | import io.kotest.matchers.shouldBe 6 | import java.time.Clock 7 | import java.time.Instant 8 | import java.time.ZoneId 9 | import java.util.concurrent.TimeUnit 10 | 11 | /** 12 | * trivial test of library 13 | * (does not work unless haste is fixed) 14 | */ 15 | internal class HasteTimeProviderTest : DescribeSpec({ 16 | describe("haste timeSource") { 17 | val haste = Haste.TimeSource.withFixedClock( 18 | Clock.fixed(Instant.parse("2020-10-24T22:22:03.00Z"), ZoneId.of("Europe/Berlin")) 19 | ) 20 | val provider = HasteTimeProvider(haste) 21 | it("gives back fixed time") { 22 | provider.getTimeSource().now().toString() shouldBe "2020-10-25T00:22:03+02:00[Europe/Berlin]" 23 | } 24 | it("gives back moved time") { 25 | haste.advanceTimeBy(6, TimeUnit.HOURS) 26 | provider.getTimeSource().now().toString() shouldBe "2020-10-25T05:22:03+01:00[Europe/Berlin]" 27 | } 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/tx/CombinedEffectsTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.tx 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.andThen 5 | import dev.neeffect.nee.effects.async.AsyncStack 6 | import dev.neeffect.nee.effects.async.CleanAsyncStack 7 | import dev.neeffect.nee.effects.security.SecuredRunEffect 8 | import dev.neeffect.nee.effects.security.SecurityError 9 | import dev.neeffect.nee.effects.security.SecurityErrorType 10 | import dev.neeffect.nee.effects.security.SecurityProvider 11 | import dev.neeffect.nee.effects.test.get 12 | import dev.neeffect.nee.effects.test.getLeft 13 | import dev.neeffect.nee.effects.utils.merge 14 | import io.kotest.core.spec.style.BehaviorSpec 15 | import io.kotest.matchers.shouldBe 16 | import io.vavr.collection.List 17 | 18 | internal class CombinedEffectsTest : BehaviorSpec({ 19 | Given("Combined effects for admin") { 20 | val dbEff = TxEffect() 21 | .handleError { e -> CombinedError.TxError(e) as CombinedError } 22 | val secEff = SecuredRunEffect("admin") 23 | .handleError { e -> CombinedError.SecurityError(e) as CombinedError } 24 | val combined = secEff.andThen(dbEff) 25 | When("Called with admin role") { 26 | val simpleAction = Nee.with(combined, function1) 27 | val db = DBLike() 28 | db.appendAnswer("6") 29 | val dbProvider = DBLikeProvider(db) 30 | val secProvider = TrivialSecurityProvider("irreg", List.of("admin")) 31 | val env = CombinedProviders(secProvider, dbProvider) 32 | val result = simpleAction.perform(env) 33 | Then("result should be 6") { 34 | 35 | result.get().get() shouldBe 6 36 | } 37 | } 38 | 39 | When("Called with no roles") { 40 | val simpleAction = Nee.with(combined, function1) 41 | val db = DBLike() 42 | db.appendAnswer("6") 43 | val dbProvider = DBLikeProvider(db) 44 | val secProvider = TrivialSecurityProvider("marreq", List.empty()) 45 | val env = CombinedProviders(secProvider, dbProvider) 46 | val result = simpleAction.perform(env) 47 | Then("result should be Insufficient roles") { 48 | result.getLeft().merge().secError() shouldBe SecurityErrorType.MissingRole(List.of("admin")) 49 | } 50 | } 51 | } 52 | }) { 53 | companion object { 54 | val function1 = { db: CombinedProviders -> 55 | val resource = db.getConnection().getResource() 56 | val result = resource.query("SELECT * FROM all1") 57 | result.map { 58 | Integer.parseInt(it) 59 | } 60 | } 61 | } 62 | } 63 | 64 | sealed class CombinedError : TxError, SecurityError { 65 | class TxError(val internal: dev.neeffect.nee.effects.tx.TxError) : CombinedError() { 66 | override fun txError(): TxErrorType = internal.txError() 67 | override fun secError(): SecurityErrorType = TODO("??? maybe nullability") 68 | } 69 | 70 | class SecurityError(val internal: dev.neeffect.nee.effects.security.SecurityError) : CombinedError() { 71 | override fun txError(): TxErrorType = TODO() 72 | override fun secError(): SecurityErrorType = internal.secError() 73 | } 74 | } 75 | 76 | internal class CombinedProviders( 77 | val secProvider: SecurityProvider, 78 | val txProvider: TxProvider, 79 | val asyncStack: AsyncStack = CleanAsyncStack() 80 | ) : SecurityProvider by secProvider, 81 | TxProvider { 82 | override fun getConnection(): TxConnection = txProvider.getConnection() 83 | 84 | override fun setConnectionState(newState: TxConnection): CombinedProviders = 85 | CombinedProviders(secProvider, txProvider.setConnectionState(newState), asyncStack) 86 | } 87 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/tx/DBLikeResource.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.tx 2 | 3 | import io.vavr.control.Either 4 | import io.vavr.control.Option 5 | 6 | internal class DBLikeProvider( 7 | val db: DBLike, 8 | val conn: TxConnection = DBConnection(db) 9 | ) : 10 | TxProvider { 11 | override fun getConnection(): TxConnection = 12 | if (!db.connected()) { 13 | if (db.connect()) { 14 | conn 15 | } else { 16 | throw IllegalStateException("cannot connect DB") 17 | } 18 | } else { 19 | conn 20 | } 21 | 22 | override fun setConnectionState(newState: TxConnection) = 23 | DBLikeProvider(db, newState) 24 | 25 | 26 | } 27 | 28 | 29 | internal open class DBConnection(val db: DBLike, val level: Int = 0) : TxConnection { 30 | override fun hasTransaction(): Boolean = false 31 | 32 | 33 | override fun begin(): Either> = 34 | if (db.begin()) { 35 | Either.right(DBTxConnection(db, level + 1)) 36 | } else { 37 | Either.left>( 38 | TxErrorType.CannotStartTransaction 39 | ) 40 | } 41 | 42 | override fun continueTx(): Either> = 43 | if (db.continueTransaction()) { 44 | Either.right(DBTxConnection(db, level)) 45 | } else { 46 | Either.left>( 47 | TxErrorType.CannotContinueTransaction 48 | ) 49 | } 50 | 51 | override fun getResource(): DBLike = db 52 | 53 | override fun close() { 54 | db.close() 55 | } 56 | } 57 | 58 | internal class DBTxConnection(db: DBLike, level: Int) : DBConnection(db, level), TxStarted { 59 | 60 | override fun hasTransaction(): Boolean = true 61 | 62 | 63 | override fun commit(): Pair, TxConnection> = 64 | if (db.commit()) { 65 | Pair(Option.none(), DBConnection(db, level - 1)) 66 | } else { 67 | Pair(Option.some(TxErrorType.CannotCommitTransaction), this) 68 | } 69 | 70 | override fun rollback(): Pair, TxConnection> = 71 | if (db.rollback()) { 72 | Pair(Option.none(), DBConnection(db, level - 1)) 73 | } else { 74 | Pair(Option.some(TxErrorType.CannotRollbackTransaction), this) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/tx/DeeperEffectTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.tx 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.andThen 5 | import dev.neeffect.nee.effects.test.get 6 | import io.kotest.core.spec.style.BehaviorSpec 7 | import io.kotest.matchers.be 8 | import io.kotest.matchers.should 9 | 10 | class DeeperEffectTest : BehaviorSpec({ 11 | Given("TestEffect") { 12 | val log = mutableListOf() 13 | val eff = TestEffect("ef1", log) 14 | val nee = Nee.Companion.with(eff, function1(log)) 15 | When("called") { 16 | val res = nee.perform(TestResource(1)) 17 | Then("log is ok") { 18 | ///TODO() write expectations on order of items in log 19 | println(res) 20 | println(log) 21 | res.get() should be("OK") 22 | } 23 | } 24 | } 25 | Given("Two TestEffects") { 26 | val log = mutableListOf() 27 | val eff1 = TestEffect("ef1", log) 28 | val eff2 = TestEffect("ef2", log) 29 | val eff = eff1.andThen(eff2) 30 | val nee = Nee.Companion.with(eff, function1(log)) 31 | When("called") { 32 | val res = nee.perform(TestResource(1)) 33 | Then("log is ok") { 34 | println(res) 35 | println(log) 36 | res.get() should be("OK") 37 | } 38 | } 39 | } 40 | 41 | }) { 42 | companion object { 43 | fun function1(log: MutableList) = { db: TestResource -> 44 | log.add("function1 called with: $db") 45 | "OK" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/tx/FlexTxEffectTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.tx 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.effects.env.FlexibleEnv 5 | import dev.neeffect.nee.effects.test.get 6 | import io.kotest.core.spec.style.BehaviorSpec 7 | import io.kotest.matchers.shouldBe 8 | 9 | internal class FlexTxEffectTest : BehaviorSpec({ 10 | Given("FlexTxEffects") { 11 | val eff = FlexTxEffect() 12 | val simpleAction = Nee.with(eff, function1.flex()) 13 | When("run on db") { 14 | val db = DBLike() 15 | db.appendAnswer("6") 16 | val provider = DBLikeProvider(db) 17 | val env = FlexibleEnv.empty().withTxProvider(provider) 18 | val result = simpleAction.perform(env) 19 | Then("correct res") { 20 | result.get() shouldBe 6 21 | } 22 | } 23 | } 24 | }) { 25 | companion object { 26 | val function1 = { env: FlexibleEnv -> 27 | val resource = FlexTxProvider.connection(env) 28 | val result = resource.query("SELECT * FROM all1") 29 | result.map { 30 | Integer.parseInt(it) 31 | }.get() 32 | } 33 | 34 | // val function2 = { orig: Int -> 35 | // { db: FlexTxProvider -> 36 | // val resource = db.getConnection().getResource() 37 | // val result = resource.query("SELECT * FROM all2 LIMIT ${orig})") 38 | // result.map { 39 | // Integer.parseInt(it) + 1000 + orig 40 | // }.get() 41 | // } 42 | // } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/tx/TestEffect.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.tx 2 | 3 | import dev.neeffect.nee.Effect 4 | import dev.neeffect.nee.effects.Out 5 | 6 | data class TestResource(val version: Int) 7 | 8 | class TestEffect(val name: String, val log: MutableList) : Effect { 9 | override fun wrap(f: (TestResource) -> A): (TestResource) -> Pair, TestResource> = 10 | { r: TestResource -> 11 | log("enter test effect. Res $r") 12 | Pair( 13 | run { 14 | val newR = r.copy(version = r.version + 10) 15 | log("calling test effect. Res $newR") 16 | Out.right(f(newR)) 17 | }, r.copy(version = r.version + 100) 18 | ) 19 | } 20 | 21 | fun log(msg: String) = log.add("$name: $msg") 22 | } 23 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/dev/neeffect/nee/effects/tx/TrivialSecurityProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.tx 2 | 3 | import dev.neeffect.nee.effects.Out 4 | import dev.neeffect.nee.effects.security.SecurityCtx 5 | import dev.neeffect.nee.effects.security.SecurityError 6 | import dev.neeffect.nee.effects.security.SecurityProvider 7 | import io.vavr.collection.List 8 | 9 | internal class TrivialSecurityProvider(user: USER, roles: List) : SecurityProvider { 10 | private val ctx = SimpleSecurityContext(user, roles) 11 | override fun getSecurityContext(): Out> = Out.right(ctx) 12 | 13 | internal class SimpleSecurityContext(private val user: USER, private val roles: List) : 14 | SecurityCtx { 15 | override fun getCurrentUser(): Out = Out.right(user) 16 | override fun hasRole(role: ROLE): Boolean = roles.contains(role) 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /nee-core/src/test/kotlin/pl/outside/code/ExternalObject.kt: -------------------------------------------------------------------------------- 1 | package pl.outside.code 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.NoEffect 5 | import dev.neeffect.nee.effects.monitoring.CodeNameFinder.guessCodePlaceName 6 | import dev.neeffect.nee.effects.monitoring.TraceProvider 7 | 8 | object ExternalObject { 9 | fun plainFunction(i: Int) = i + 1 10 | 11 | fun > traceableFunction(p: Int) = { mon: R -> 12 | mon.getTrace().putNamedPlace(guessCodePlaceName(1)).let { plainFunction(p) } 13 | } 14 | 15 | fun checkWhereCodeIsSimple(a: Unit) = guessCodePlaceName() 16 | 17 | fun checkWhereCodeIsNee() = Nee.Companion.with(NoEffect()) { 18 | guessCodePlaceName() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.config.KotlinCompilerVersion 2 | 3 | 4 | plugins { 5 | id("org.jetbrains.kotlin.jvm") 6 | } 7 | 8 | dependencies { 9 | implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION)) 10 | 11 | api(project(":nee-core")) 12 | api(project(":nee-jdbc")) 13 | implementation(project(":nee-security")) 14 | implementation(project(":nee-security-jdbc")) 15 | implementation(project(":nee-cache-caffeine")) 16 | 17 | implementation(Libs.Ktor.serverCore) 18 | implementation(Libs.Vavr.jackson) 19 | implementation(Libs.Jackson.jacksonModuleKotlin) 20 | implementation(Libs.Jackson.jacksonJsr310) 21 | implementation(Libs.Ktor.jackson) 22 | implementation(Libs.Ktor.serverHostCommon) 23 | implementation(Libs.Ktor.serverNetty) 24 | 25 | 26 | testImplementation(project(":nee-test:nee-security-jdbc-test")) 27 | testImplementation(Libs.Ktor.serverTestHost) 28 | testImplementation(Libs.Ktor.clientJackson) 29 | testImplementation(Libs.Ktor.clientMockJvm) 30 | testImplementation(Libs.Kotest.runnerJunit5Jvm) 31 | 32 | 33 | } 34 | apply(from = "../publish-mpp.gradle.kts") 35 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/main/kotlin/dev/neeffect/nee/ctx/web/ApplicationContextProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web 2 | 3 | import dev.neeffect.nee.ANee 4 | 5 | /** 6 | * Generic app context. 7 | */ 8 | interface ApplicationContextProvider { 9 | suspend fun serve(businessFunction: ANee, localParam: LOCAL) 10 | } 11 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/main/kotlin/dev/neeffect/nee/ctx/web/BasicAuth.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web 2 | 3 | import dev.neeffect.nee.effects.Out 4 | import dev.neeffect.nee.effects.Out.Companion.left 5 | import dev.neeffect.nee.effects.security.SecurityCtx 6 | import dev.neeffect.nee.effects.security.SecurityError 7 | import dev.neeffect.nee.effects.security.SecurityErrorType 8 | import dev.neeffect.nee.effects.security.SecurityProvider 9 | import dev.neeffect.nee.security.UserRealm 10 | import io.ktor.request.ApplicationRequest 11 | import io.ktor.request.header 12 | import io.vavr.control.Option 13 | import io.vavr.control.Try 14 | import io.vavr.kotlin.option 15 | import java.nio.charset.Charset 16 | import java.util.Base64 17 | 18 | /** 19 | * Basic auth implementation. 20 | * 21 | * This is not very secure type of credential delivery. 22 | * Use JWT or other method if possible. 23 | */ 24 | object BasicAuth { 25 | const val authorizationHeader = "Authorization" 26 | 27 | /** 28 | * Context for basic auth check. 29 | */ 30 | class BasicAuthCtx(private val userRealm: UserRealm) { 31 | fun createSecurityProviderFromRequest(request: ApplicationRequest): SecurityProvider = 32 | BasicAuthProvider( 33 | request.header(authorizationHeader).option(), userRealm 34 | ) 35 | } 36 | } 37 | 38 | class BasicAuthProvider( 39 | private val headerVal: Option, 40 | private val userRealm: UserRealm 41 | ) : SecurityProvider { 42 | 43 | private val base64Decoder = Base64.getDecoder() 44 | 45 | override fun getSecurityContext(): Out> = 46 | headerVal.map { baseAuth: String -> 47 | Try.of { 48 | parseHeader(baseAuth) 49 | }.getOrElseGet { e -> left(SecurityErrorType.MalformedCredentials(e.localizedMessage)) } 50 | }.getOrElse { 51 | Out.right(AnonymousSecurityContext()) 52 | } 53 | 54 | private fun parseHeader(baseAuth: String): Out> = 55 | if (baseAuth.startsWith(basicAuthHeaderPrefix)) { 56 | val decodedAut = base64Decoder.decode(baseAuth.substring(basicAuthHeaderPrefix.length)) 57 | val colonIndex = decodedAut.indexOf(':'.toByte()) 58 | if (colonIndex > 0) { 59 | val login = decodedAut.sliceArray(0 until colonIndex).toString(Charset.forName("UTF-8")) 60 | val pass = decodedAut.sliceArray(colonIndex + 1 until decodedAut.size) 61 | .toCharArray() 62 | userRealm.loginUser(login, pass).map { user -> 63 | pass.fill(0.toChar()) // I know that cleaning password in such insecure protocol is useless 64 | Out.right>(UserSecurityContext(user, userRealm)) 65 | }.getOrElse { 66 | left>(SecurityErrorType.WrongCredentials(login)) 67 | } 68 | } else { 69 | left>( 70 | SecurityErrorType.MalformedCredentials("no colon inside header: $baseAuth") 71 | ) 72 | } 73 | } else { 74 | left>( 75 | SecurityErrorType.MalformedCredentials("no basic auth header: $baseAuth") 76 | ) 77 | } 78 | 79 | class AnonymousSecurityContext : SecurityCtx { 80 | override fun getCurrentUser(): Out = 81 | left(SecurityErrorType.UnknownUser) 82 | 83 | override fun hasRole(role: ROLE): Boolean = false 84 | } 85 | 86 | class UserSecurityContext( 87 | private val user: USERID, 88 | private val userRealm: UserRealm 89 | ) : SecurityCtx { 90 | override fun getCurrentUser(): Out = Out.right(user) 91 | 92 | override fun hasRole(role: ROLE): Boolean = 93 | userRealm.hasRole(user, role) 94 | } 95 | 96 | companion object { 97 | const val basicAuthHeaderPrefix = "Basic " 98 | } 99 | } 100 | 101 | internal fun ByteArray.toCharArray() = String(this).toCharArray() 102 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/main/kotlin/dev/neeffect/nee/ctx/web/ErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web 2 | 3 | import dev.neeffect.nee.effects.security.SecurityError 4 | import io.ktor.http.ContentType 5 | import io.ktor.http.HttpStatusCode 6 | import io.ktor.http.content.OutgoingContent 7 | import io.ktor.http.content.TextContent 8 | 9 | typealias ErrorHandler = (Any) -> OutgoingContent 10 | 11 | object DefaultErrorHandler : ErrorHandler { 12 | override fun invoke(error: Any): OutgoingContent = 13 | when (error) { 14 | is SecurityError -> TextContent( 15 | text = "security error: ${error.secError()}", 16 | contentType = ContentType.Text.Plain, 17 | status = HttpStatusCode.Unauthorized 18 | ) 19 | else -> TextContent( 20 | text = "error: $error", 21 | contentType = ContentType.Text.Plain, 22 | status = HttpStatusCode.InternalServerError 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/main/kotlin/dev/neeffect/nee/ctx/web/WebContext.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web 2 | 3 | import dev.neeffect.nee.ANee 4 | import dev.neeffect.nee.ctx.web.util.RenderHelper 5 | import dev.neeffect.nee.effects.Out 6 | import dev.neeffect.nee.effects.async.AsyncEnvWrapper 7 | import dev.neeffect.nee.effects.async.AsyncSupport 8 | import dev.neeffect.nee.effects.async.ExecutionContextProvider 9 | import dev.neeffect.nee.effects.monitoring.TraceProvider 10 | import dev.neeffect.nee.effects.monitoring.TraceResource 11 | import dev.neeffect.nee.effects.security.SecurityProvider 12 | import dev.neeffect.nee.effects.time.TimeProvider 13 | import dev.neeffect.nee.effects.tx.TxConnection 14 | import dev.neeffect.nee.effects.tx.TxProvider 15 | import dev.neeffect.nee.effects.utils.Logging 16 | import dev.neeffect.nee.security.User 17 | import dev.neeffect.nee.security.UserRole 18 | import io.ktor.application.ApplicationCall 19 | 20 | data class WebContext>( 21 | private val jdbcProvider: TxProvider, 22 | private val securityProvider: SecurityProvider, 23 | private val executionContextProvider: ExecutionContextProvider, 24 | private val errorHandler: ErrorHandler = DefaultErrorHandler, 25 | private val contextProvider: WebContextProvider, 26 | private val traceProvider: TraceProvider<*>, 27 | private val timeProvider: TimeProvider, 28 | private val applicationCall: ApplicationCall, 29 | private val asyncEnv: AsyncEnvWrapper> = AsyncEnvWrapper() 30 | ) : TxProvider>, 31 | SecurityProvider by securityProvider, 32 | ExecutionContextProvider by executionContextProvider, 33 | TraceProvider>, 34 | TimeProvider by timeProvider, 35 | Logging, 36 | AsyncSupport> by asyncEnv { 37 | 38 | private val renderHelper = RenderHelper(contextProvider.jacksonMapper(), errorHandler) 39 | 40 | override fun getTrace(): TraceResource = traceProvider.getTrace() 41 | 42 | override fun setTrace(newState: TraceResource): WebContext = 43 | this.copy(traceProvider = traceProvider.setTrace(newState)) 44 | 45 | override fun getConnection(): TxConnection = jdbcProvider.getConnection() 46 | 47 | override fun setConnectionState(newState: TxConnection) = 48 | this.copy(jdbcProvider = jdbcProvider.setConnectionState(newState)) 49 | 50 | suspend fun serveText(businessFunction: ANee, String>) = 51 | businessFunction.perform(this).let { result -> 52 | renderHelper.serveText(applicationCall, result) 53 | } 54 | 55 | suspend fun serveMessage(msg: Out): Unit = 56 | renderHelper.serveMessage(applicationCall, msg) 57 | 58 | suspend fun serveMessage(businessFunction: ANee, Any>) = 59 | serveMessage(businessFunction.perform(this)) 60 | } 61 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/main/kotlin/dev/neeffect/nee/ctx/web/jwt/JwtAuthProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web.jwt 2 | 3 | import dev.neeffect.nee.effects.Out 4 | import dev.neeffect.nee.effects.security.SecurityCtx 5 | import dev.neeffect.nee.effects.security.SecurityError 6 | import dev.neeffect.nee.effects.security.SecurityErrorType 7 | import dev.neeffect.nee.effects.security.SecurityProvider 8 | import dev.neeffect.nee.effects.utils.merge 9 | import dev.neeffect.nee.security.jwt.JwtConfigurationModule 10 | import dev.neeffect.nee.security.jwt.JwtUsersCoder 11 | import io.vavr.control.Option 12 | 13 | class JwtAuthProvider( 14 | private val headerVal: Option, 15 | private val jwtConf: JwtConfigurationModule 16 | ) : SecurityProvider { 17 | override fun getSecurityContext(): Out> = 18 | headerVal.map { fullHeader -> 19 | if (fullHeader.startsWith(bearerAuthHeaderPrefix)) { 20 | val jwtToken = fullHeader.substring(bearerAuthHeaderPrefix.length) 21 | jwtConf.jwtCoder.decodeJwt(jwtToken).map { jwt -> 22 | jwtConf.jwtUsersCoder.decodeUser(jwt).map { user -> 23 | Out.right>( 24 | TokenSecurityContext(user, jwtConf.jwtUsersCoder) 25 | ) 26 | }.getOrElse { 27 | Out.left(SecurityErrorType.MalformedCredentials("user not decoded from $jwt")) 28 | } 29 | }.mapLeft { jwtError -> 30 | Out.left>( 31 | SecurityErrorType.MalformedCredentials(jwtError.toString()) 32 | ) 33 | }.merge() 34 | } else { 35 | Out.left>( 36 | SecurityErrorType.MalformedCredentials("wrong header $fullHeader") 37 | ) 38 | } 39 | }.getOrElse { 40 | Out.left>(SecurityErrorType.NoSecurityCtx) 41 | } 42 | 43 | companion object { 44 | const val bearerAuthHeaderPrefix = "Bearer " 45 | } 46 | } 47 | 48 | class TokenSecurityContext(val user: USER, val jwtCoder: JwtUsersCoder) : 49 | SecurityCtx { 50 | override fun getCurrentUser(): Out = 51 | Out.right(user) 52 | 53 | override fun hasRole(role: ROLE): Boolean = jwtCoder.hasRole(user, role) 54 | } 55 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/main/kotlin/dev/neeffect/nee/ctx/web/oauth/OauthSupportApi.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web.oauth 2 | 3 | import dev.neeffect.nee.ctx.web.DefaultErrorHandler 4 | import dev.neeffect.nee.ctx.web.DefaultJacksonMapper 5 | import dev.neeffect.nee.ctx.web.util.ApiError 6 | import dev.neeffect.nee.ctx.web.util.RenderHelper 7 | import dev.neeffect.nee.effects.Out 8 | import dev.neeffect.nee.effects.security.SecurityErrorType 9 | import dev.neeffect.nee.effects.utils.merge 10 | import dev.neeffect.nee.security.User 11 | import dev.neeffect.nee.security.UserRole 12 | import dev.neeffect.nee.security.oauth.LoginResult 13 | import dev.neeffect.nee.security.oauth.OauthProviderName 14 | import dev.neeffect.nee.security.oauth.OauthService 15 | import io.ktor.application.ApplicationCall 16 | import io.ktor.application.call 17 | import io.ktor.request.receive 18 | import io.ktor.routing.Route 19 | import io.ktor.routing.get 20 | import io.ktor.routing.post 21 | import io.ktor.routing.route 22 | import io.ktor.util.pipeline.PipelineContext 23 | import io.vavr.control.Either 24 | import io.vavr.control.Try 25 | import io.vavr.kotlin.option 26 | 27 | class OauthSupportApi(private val oauthService: OauthService) { 28 | 29 | private val renderHelper = RenderHelper(DefaultJacksonMapper.mapper, DefaultErrorHandler) 30 | 31 | fun oauthApi(): Route.() -> Unit = { 32 | 33 | route("/oauth") { 34 | get("/generateUrl/{provider}") { 35 | val result = call.request.queryParameters["redirect"].option().toEither( 36 | ApiError.WrongArguments("redirect not set") 37 | ).flatMap { redirectUrl -> 38 | extractProvider().flatMap { provider -> 39 | oauthService.generateApiCall(provider, redirectUrl).toEither( 40 | ApiError.WrongArguments("cannot generate oauth call") 41 | ) 42 | } 43 | } 44 | renderHelper.renderResponse(call, result) 45 | } 46 | post("/loginUser/{provider}") { 47 | 48 | val loginData = call.receive() 49 | val result = extractProvider().map { provider -> 50 | oauthService.login(loginData.code, loginData.state, loginData.redirectUri, provider) 51 | .perform(Unit) 52 | }.mapLeft { apiError -> 53 | Out.left( 54 | SecurityErrorType.MalformedCredentials("$apiError") 55 | ) 56 | }.merge() 57 | renderHelper.serveMessage(call, result) 58 | } 59 | } 60 | } 61 | 62 | private fun PipelineContext.extractProvider(): Either = 63 | call.parameters["provider"].option().toEither( 64 | ApiError.WrongArguments("provider not set") 65 | ).flatMap { providerName -> 66 | Try.of { 67 | OauthProviderName.valueOf(providerName) 68 | }.toEither().mapLeft { 69 | ApiError.WrongArguments("provider $providerName is unknown") 70 | } 71 | } 72 | } 73 | 74 | data class OauthLoginData( 75 | val code: String, 76 | val state: String, 77 | val redirectUri: String 78 | ) 79 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/main/kotlin/dev/neeffect/nee/ctx/web/util/RenderHelper.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web.util 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import dev.neeffect.nee.ctx.web.ErrorHandler 5 | import dev.neeffect.nee.effects.Out 6 | import dev.neeffect.nee.effects.toFuture 7 | import dev.neeffect.nee.effects.utils.Logging 8 | import dev.neeffect.nee.effects.utils.logger 9 | import dev.neeffect.nee.effects.utils.merge 10 | import io.ktor.application.ApplicationCall 11 | import io.ktor.http.ContentType 12 | import io.ktor.http.HttpStatusCode 13 | import io.ktor.http.content.ByteArrayContent 14 | import io.ktor.http.content.OutgoingContent 15 | import io.ktor.http.content.TextContent 16 | import io.ktor.response.respond 17 | import io.vavr.control.Either 18 | import kotlinx.coroutines.future.await 19 | 20 | @Suppress("ReturnUnit") 21 | class RenderHelper( 22 | val objectMapper: ObjectMapper, 23 | val errorHandler: ErrorHandler 24 | ) : Logging { 25 | 26 | suspend fun renderText(call: ApplicationCall, text: String) = 27 | TextContent( 28 | text = text, 29 | contentType = ContentType.Text.Plain, 30 | status = HttpStatusCode.OK 31 | ).let { 32 | call.respond(it) 33 | } 34 | 35 | suspend fun renderResponse(call: ApplicationCall, resp: Either) = 36 | resp.mapLeft { error -> 37 | TextContent( 38 | text = error.toString(), 39 | contentType = ContentType.Text.Plain, 40 | status = error.status 41 | ) 42 | }.map { result -> 43 | when (result) { 44 | is String -> 45 | TextContent( 46 | text = result, 47 | contentType = ContentType.Text.Plain, 48 | status = HttpStatusCode.OK 49 | ) 50 | else -> TODO() 51 | } 52 | }.merge().let { content -> 53 | call.respond(content) 54 | } 55 | 56 | @Suppress("TooGenericExceptionCaught") 57 | suspend fun serveMessage(applicationCall: ApplicationCall, msg: Out): Unit = 58 | msg.toFuture().toCompletableFuture().await().let { outcome -> 59 | val message = outcome.bimap({ serveError(it as Any) }, { regularResult -> 60 | val bytes = objectMapper.writeValueAsBytes(regularResult) 61 | ByteArrayContent( 62 | bytes = bytes, 63 | contentType = ContentType.Application.Json, 64 | status = HttpStatusCode.OK 65 | ) 66 | }).merge() 67 | try { 68 | applicationCall.respond(message) 69 | } catch (e: Exception) { 70 | logger().warn("exception in sending response", e) 71 | } 72 | } 73 | 74 | @Suppress("TooGenericExceptionCaught") 75 | suspend fun serveText(applicationCall: ApplicationCall, msg: Out): Unit = 76 | msg.toFuture().toCompletableFuture().await().let { outcome -> 77 | val message = outcome.bimap({ serveError(it as Any) }, { regularResult -> 78 | TextContent( 79 | text = regularResult, 80 | contentType = ContentType.Text.Plain, 81 | status = HttpStatusCode.OK 82 | ) 83 | }).merge() 84 | try { 85 | applicationCall.respond(message) 86 | } catch (e: Exception) { 87 | logger().warn("exception in sending response", e) 88 | } 89 | } 90 | 91 | internal fun serveError(errorResult: Any): OutgoingContent = errorHandler(errorResult) 92 | } 93 | 94 | sealed class ApiError { 95 | open val status: HttpStatusCode = HttpStatusCode.InternalServerError 96 | 97 | data class WrongArguments(val msg: String) : ApiError() { 98 | override val status = HttpStatusCode.BadRequest 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/test/kotlin/dev/neeffect/nee/ctx/web/BaseWebContextSysPathsTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web 2 | 3 | import dev.neeffect.nee.ctx.web.support.EmptyTestContextProvider 4 | import dev.neeffect.nee.effects.Out 5 | import dev.neeffect.nee.effects.security.SecurityCtx 6 | import dev.neeffect.nee.effects.security.SecurityError 7 | import dev.neeffect.nee.effects.security.SecurityErrorType 8 | import dev.neeffect.nee.effects.security.SecurityProvider 9 | import dev.neeffect.nee.security.User 10 | import dev.neeffect.nee.security.UserRole 11 | import io.kotest.core.spec.style.DescribeSpec 12 | import io.kotest.matchers.shouldBe 13 | import io.ktor.application.Application 14 | import io.ktor.application.ApplicationCall 15 | import io.ktor.http.HttpHeaders 16 | import io.ktor.http.HttpMethod 17 | import io.ktor.http.HttpStatusCode 18 | import io.ktor.routing.routing 19 | import io.ktor.server.testing.TestApplicationEngine 20 | import io.ktor.server.testing.createTestEnvironment 21 | import io.ktor.server.testing.handleRequest 22 | import io.vavr.collection.List 23 | import java.util.* 24 | import kotlin.random.Random 25 | 26 | internal class BaseWebContextSysPathsTest : DescribeSpec({ 27 | describe("sys routing paths") { 28 | val engine = TestApplicationEngine(createTestEnvironment()) 29 | engine.start(wait = false) 30 | engine.application.sysApp(TestSysContext.contexProvider) 31 | it("returns ok healthCheck") { 32 | val status = engine.handleRequest(HttpMethod.Get, "/sys/healthCheck").response.status() 33 | status shouldBe (HttpStatusCode.OK) 34 | } 35 | it("returns login OK check") { 36 | val content = engine.handleRequest(HttpMethod.Get, "/sys/currentUser") { 37 | this.addHeader(HttpHeaders.Authorization, "testUser ygrek") 38 | }.response.content 39 | val user = TestSysContext.contexProvider.jacksonMapper.readValue(content, User::class.java) 40 | user.login shouldBe ("ygrek") 41 | } 42 | it("returns login failed login check") { 43 | val resp = engine.handleRequest(HttpMethod.Get, "/sys/currentUser") 44 | .response 45 | resp.status() shouldBe (HttpStatusCode.Unauthorized) 46 | } 47 | it("returns role check ok") { 48 | val status = engine.handleRequest( 49 | HttpMethod.Get, 50 | "/sys/hasRoles?roles=admin" 51 | ) { 52 | this.addHeader(HttpHeaders.Authorization, "testUser admin") 53 | }.response.status() 54 | status shouldBe (HttpStatusCode.OK) 55 | } 56 | it("returns role check failed") { 57 | val status = engine.handleRequest( 58 | HttpMethod.Get, 59 | "/sys/hasRoles?roles=admin" 60 | ) { 61 | this.addHeader(HttpHeaders.Authorization, "testUser ygrek") 62 | }.response.status() 63 | status shouldBe (HttpStatusCode.Unauthorized) 64 | } 65 | } 66 | }) { 67 | object TestSysContext : EmptyTestContextProvider() { 68 | val userMatcher = ("""^testUser (\w+)$""").toRegex() 69 | private val rng = Random(55L) 70 | override fun security(call: ApplicationCall): SecurityProvider = 71 | object : SecurityProvider { 72 | 73 | override fun getSecurityContext(): Out> = 74 | call.request.headers.get(HttpHeaders.Authorization)?.let { authHeader -> 75 | userMatcher.find(authHeader)?.let { matchRes -> 76 | val userName = matchRes.groupValues[1] 77 | val uuid = UUID(rng.nextLong(), rng.nextLong()) 78 | val user = User(uuid, userName, List.of(UserRole(userName))) 79 | Out.right(TestCTX(user)) 80 | } ?: Out.left(SecurityErrorType.MalformedCredentials("header=$authHeader")) 81 | } ?: Out.left(SecurityErrorType.MalformedCredentials("no header")) 82 | } 83 | 84 | class TestCTX(private val user: User) : SecurityCtx { 85 | 86 | override fun getCurrentUser(): Out = 87 | Out.right(user) 88 | 89 | override fun hasRole(role: UserRole): Boolean = user.roles.contains(role) 90 | } 91 | } 92 | } 93 | 94 | fun Application.sysApp(ctx: WebContextProvider<*, *>) { 95 | routing(ctx.sysApi()) 96 | } 97 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/test/kotlin/dev/neeffect/nee/ctx/web/BasicAuthProviderTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web 2 | 3 | import dev.neeffect.nee.effects.security.SecurityErrorType 4 | import dev.neeffect.nee.effects.toFuture 5 | import dev.neeffect.nee.security.InMemoryUserRealm 6 | import io.kotest.core.spec.style.DescribeSpec 7 | import io.kotest.matchers.be 8 | import io.kotest.matchers.should 9 | import io.kotest.matchers.shouldBe 10 | import io.kotest.matchers.types.shouldBeTypeOf 11 | import io.vavr.control.Option.none 12 | import io.vavr.control.Option.some 13 | 14 | internal class BasicAuthProviderTest : DescribeSpec({ 15 | describe("basic auth") { 16 | val userRealm = InMemoryUserRealm().withPassword("test1", "test2".toCharArray()) 17 | .withRole("test1", "unfixer") 18 | describe("with correct auth header") { 19 | val provider = BasicAuthProvider(some("Basic dGVzdDE6dGVzdDI="), userRealm) 20 | it("should find role context") { 21 | provider.getSecurityContext() 22 | .toFuture().get().get() 23 | .getCurrentUser().toFuture().get().get() should be("test1") 24 | } 25 | it("should find role") { 26 | provider.getSecurityContext() 27 | .toFuture().get().get() 28 | .hasRole("unfixer") should be(true) 29 | } 30 | it("should reject unknown role") { 31 | provider.getSecurityContext() 32 | .toFuture().get().get() 33 | .hasRole("admin") should be(false) 34 | } 35 | } 36 | describe("with no header") { 37 | val provider = BasicAuthProvider(none(), userRealm) 38 | it("should not find user") { 39 | provider.getSecurityContext() 40 | .toFuture().get().get() 41 | .getCurrentUser().toFuture().get().swap().get() shouldBe (SecurityErrorType.UnknownUser) 42 | } 43 | it("should have no roles") { 44 | provider.getSecurityContext() 45 | .toFuture().get().get() 46 | .hasRole("unfixer") should be(false) 47 | } 48 | } 49 | describe("with broken header") { 50 | val provider = BasicAuthProvider(some("Basic dGVzd!@sa222DE6dGVzdDI="), userRealm) 51 | it("should result in error user") { 52 | provider.getSecurityContext() 53 | .toFuture().get().swap().get() 54 | .shouldBeTypeOf() 55 | } 56 | it("should have no roles") { 57 | provider.getSecurityContext() 58 | .toFuture().get().swap().get() 59 | .shouldBeTypeOf() 60 | } 61 | } 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/test/kotlin/dev/neeffect/nee/ctx/web/KtorThreadingModelTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.ctx.web.support.EmptyTestContext 5 | import io.kotest.core.spec.style.BehaviorSpec 6 | import io.kotest.matchers.longs.shouldBeGreaterThan 7 | import io.kotest.matchers.longs.shouldBeLessThan 8 | import io.ktor.application.Application 9 | import io.ktor.application.call 10 | import io.ktor.http.HttpMethod 11 | import io.ktor.response.respondText 12 | import io.ktor.routing.get 13 | import io.ktor.routing.routing 14 | import io.ktor.server.testing.TestApplicationEngine 15 | import io.ktor.server.testing.createTestEnvironment 16 | import io.ktor.server.testing.handleRequest 17 | import kotlinx.coroutines.newFixedThreadPoolContext 18 | import java.util.concurrent.CountDownLatch 19 | import java.util.concurrent.Executors 20 | 21 | fun Application.slowApp() { 22 | 23 | routing { 24 | get("/slow") { 25 | Thread.sleep(100) 26 | println("waited 100 ${System.currentTimeMillis()} ${Thread.currentThread().name}") 27 | call.respondText { "ok" } 28 | } 29 | get("/fast") { 30 | val wc = EmptyTestContext.contexProvider.create(call) 31 | val result = Nee.with(EmptyTestContext.contexProvider.fx().async) { 32 | Thread.sleep(100) 33 | "ok" 34 | }.perform(wc) 35 | wc.serveMessage(result) 36 | } 37 | } 38 | } 39 | 40 | class KtorThreadingModelTest : BehaviorSpec({ 41 | 42 | Given("ktor app") { 43 | 44 | val engine = TestApplicationEngine(createTestEnvironment()) { 45 | this.dispatcher = newFixedThreadPoolContext(2, "test ktor dispatcher") 46 | } 47 | engine.start(wait = false) 48 | engine.application.slowApp() 49 | When("slow req bombarded with 100 threads") { 50 | val countdown = CountDownLatch(reqs) 51 | val initTime = System.currentTimeMillis() 52 | (0..reqs).forEach { 53 | 54 | reqExecutor.submit { 55 | engine.handleRequest(HttpMethod.Get, "/slow").response.content 56 | countdown.countDown() 57 | } 58 | } 59 | then("slow is slow") { 60 | countdown.await() 61 | val totalTime = System.currentTimeMillis() - initTime 62 | println(totalTime) 63 | totalTime shouldBeGreaterThan 2000 64 | } 65 | } 66 | When("fast req bombarded with 100 threads") { 67 | val countdown = CountDownLatch(reqs) 68 | val initTime = System.currentTimeMillis() 69 | (0..reqs).forEach { 70 | reqExecutor.submit { 71 | engine.handleRequest(HttpMethod.Get, "/fast").response.content 72 | countdown.countDown() 73 | } 74 | } 75 | then("fast is faster") { 76 | countdown.await() 77 | val totalTime = System.currentTimeMillis() - initTime 78 | println(totalTime) 79 | totalTime shouldBeLessThan 2000 80 | } 81 | } 82 | } 83 | }) { 84 | companion object { 85 | val reqs = 100 86 | val reqExecutor = Executors.newFixedThreadPool(reqs) 87 | 88 | init { 89 | println("ok") 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/test/kotlin/dev/neeffect/nee/ctx/web/SimpleApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.effects.jdbc.JDBCProvider 5 | import dev.neeffect.nee.security.UserRole 6 | import dev.neeffect.nee.security.test.TestDB 7 | import io.kotest.core.spec.style.BehaviorSpec 8 | import io.ktor.application.Application 9 | import io.ktor.application.call 10 | import io.ktor.http.HttpMethod 11 | import io.ktor.http.HttpStatusCode 12 | import io.ktor.routing.get 13 | import io.ktor.routing.routing 14 | import io.ktor.server.testing.TestApplicationEngine 15 | import io.ktor.server.testing.createTestEnvironment 16 | import io.ktor.server.testing.handleRequest 17 | import io.vavr.collection.List 18 | import java.sql.Connection 19 | import kotlin.test.assertEquals 20 | 21 | fun Application.main(wctxProvider: JDBCBasedWebContextProvider) { 22 | 23 | routing { 24 | get("/") { 25 | val function: Nee, Any, String> = 26 | Nee.with(wctxProvider.fx().tx) { webCtx -> 27 | webCtx.getConnection().getResource() 28 | .prepareStatement("select 41 from dual").use { preparedStatement -> 29 | preparedStatement.executeQuery().use { resultSet -> 30 | if (resultSet.next()) { 31 | val result = resultSet.getString(1) 32 | "Hello! Result is $result" 33 | } else { 34 | "Bad result" 35 | } 36 | } 37 | } 38 | }.anyError() 39 | wctxProvider.create(call).serveText(function) 40 | } 41 | get("/secured") { 42 | val function = Nee.with(wctxProvider.fx().secured(List.of(UserRole("badmin")))) { _ -> 43 | "Secret message" 44 | }.anyError() 45 | wctxProvider.create(call).serveText(function) 46 | } 47 | } 48 | } 49 | 50 | class SimpleApplicationTest : BehaviorSpec({ 51 | Given("Test ktor app") { 52 | val engine = TestApplicationEngine(createTestEnvironment()) 53 | 54 | TestDB().initializeDb().use { testDb -> 55 | 56 | testDb.addUser("test", "test", List.of("badmin")) 57 | val ctxProvider = object : JDBCBasedWebContextProvider() { 58 | override val jdbcProvider: JDBCProvider by lazy { 59 | JDBCProvider(testDb.connection) 60 | } 61 | } 62 | engine.start(wait = false) 63 | engine.application.main(ctxProvider) 64 | 65 | When("requested") { 66 | Then("db connection works") { 67 | assertEquals("Hello! Result is 41", engine.handleRequest(HttpMethod.Get, "/").response.content) 68 | } 69 | 70 | } 71 | When("request with authentication") { 72 | Then("db connection works") { 73 | engine.handleRequest(HttpMethod.Get, "/secured") { 74 | addHeader(BasicAuth.authorizationHeader, "Basic dGVzdDp0ZXN0") 75 | }.let { call -> 76 | assertEquals("Secret message", call.response.content) 77 | } 78 | } 79 | 80 | } 81 | When("request with invalid authentication") { 82 | Then("db connection works") { 83 | engine.handleRequest(HttpMethod.Get, "/secured") { 84 | addHeader(BasicAuth.authorizationHeader, "Basic blablador") 85 | }.let { call -> 86 | assertEquals(HttpStatusCode.Unauthorized, call.response.status()) 87 | } 88 | } 89 | 90 | } 91 | } 92 | } 93 | }) 94 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/test/kotlin/dev/neeffect/nee/ctx/web/jwt/JwtAuthProviderTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web.jwt 2 | 3 | import dev.neeffect.nee.ctx.web.oauth.OauthSupportApiTest 4 | import dev.neeffect.nee.effects.security.SecurityErrorType 5 | import dev.neeffect.nee.effects.test.get 6 | import dev.neeffect.nee.effects.test.getLeft 7 | import dev.neeffect.nee.security.UserRole 8 | import io.kotest.core.spec.style.DescribeSpec 9 | import io.kotest.matchers.shouldBe 10 | import io.kotest.matchers.types.shouldBeTypeOf 11 | import io.vavr.kotlin.none 12 | import io.vavr.kotlin.option 13 | 14 | internal class JwtAuthProviderTest : DescribeSpec({ 15 | describe("using standard config") { 16 | val jwtConfigModule = OauthSupportApiTest.oauthConfigModule.jwtConfigModule 17 | describe("no auth header ") { 18 | val provider = JwtAuthProvider(none(), jwtConfigModule) 19 | provider.getSecurityContext().getLeft() shouldBe SecurityErrorType.NoSecurityCtx 20 | } 21 | describe("broken auth header ") { 22 | val provider = JwtAuthProvider("broken code".option(), jwtConfigModule) 23 | provider.getSecurityContext().getLeft().shouldBeTypeOf() 24 | } 25 | 26 | describe("broken jwt token ") { 27 | val provider = JwtAuthProvider("Bearer nonsensecode".option(), jwtConfigModule) 28 | provider.getSecurityContext().getLeft().shouldBeTypeOf() 29 | } 30 | 31 | describe("correct jwt token ") { 32 | val provider = JwtAuthProvider("Bearer $exampleJwtToken".option(), jwtConfigModule) 33 | it("has role oauthUser") { 34 | provider.getSecurityContext().get().hasRole(UserRole("oauthUser")) shouldBe true 35 | } 36 | it("does not have role admin") { 37 | provider.getSecurityContext().get().hasRole(UserRole("admin")) shouldBe false 38 | } 39 | 40 | it("has user name") { 41 | provider.getSecurityContext().get().getCurrentUser().get().displayName shouldBe "Jarek Ratajski" 42 | } 43 | 44 | } 45 | } 46 | }) { 47 | companion object { 48 | const val exampleJwtToken = 49 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDM1NzkxMjMsImlhdCI6MTYwMzU3ODEyMywiaXNzIjoidGVzdCIsInN1YiI6ImJhNDE5ZDM1LTBkZmUtOGFmNy1hZWU3LWJiZTEwYzQ1YzAyOCIsImxvZ2luIjoiZ29vZ2xlOjEwODg3NDQ1NDY3NjI0NDcwMDM4MCIsImRpc3BsYXlOYW1lIjoiSmFyZWsgUmF0YWpza2kiLCJpZCI6ImJhNDE5ZDM1LTBkZmUtOGFmNy1hZWU3LWJiZTEwYzQ1YzAyOCIsInJvbGVzIjoib2F1dGhVc2VyIn0.5ZsttU5WgJKRFTxBFso4ETqrc-loViGxku539hI5SvY" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/test/kotlin/dev/neeffect/nee/ctx/web/support/EmptyTestContext.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.ctx.web.support 2 | 3 | import dev.neeffect.nee.ctx.web.BaseWebContextProvider 4 | import dev.neeffect.nee.ctx.web.KtorThreadingModelTest 5 | import dev.neeffect.nee.effects.Out 6 | import dev.neeffect.nee.effects.async.ECProvider 7 | import dev.neeffect.nee.effects.async.ExecutorExecutionContext 8 | import dev.neeffect.nee.effects.jdbc.JDBCProvider 9 | import dev.neeffect.nee.effects.security.SecurityCtx 10 | import dev.neeffect.nee.effects.security.SecurityError 11 | import dev.neeffect.nee.effects.security.SecurityProvider 12 | import dev.neeffect.nee.effects.tx.TxConnection 13 | import dev.neeffect.nee.effects.tx.TxProvider 14 | import dev.neeffect.nee.effects.utils.invalid 15 | import dev.neeffect.nee.security.User 16 | import dev.neeffect.nee.security.UserRole 17 | import io.ktor.application.ApplicationCall 18 | import java.sql.Connection 19 | import java.util.concurrent.Executors 20 | 21 | internal open class EmptyTestContextProvider { 22 | open val myTxProvider = object : TxProvider { 23 | override fun getConnection(): TxConnection = 24 | invalid() 25 | 26 | override fun setConnectionState(newState: TxConnection): JDBCProvider = 27 | invalid() 28 | } 29 | 30 | open fun security(call: ApplicationCall) = object : SecurityProvider { 31 | override fun getSecurityContext(): Out> = 32 | invalid() 33 | } 34 | 35 | val serverExecutor = Executors.newFixedThreadPool(KtorThreadingModelTest.reqs) 36 | val ec = ExecutorExecutionContext(serverExecutor) 37 | 38 | val contexProvider = object : BaseWebContextProvider() { 39 | override val txProvider = myTxProvider 40 | override fun authProvider(call: ApplicationCall): SecurityProvider = 41 | security(call) 42 | 43 | override val executionContextProvider = ECProvider(ec) 44 | } 45 | } 46 | 47 | internal object EmptyTestContext : EmptyTestContextProvider() 48 | -------------------------------------------------------------------------------- /nee-ctx-web-ktor/src/test/resources/google/keys.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "alg": "RS256", 5 | "kty": "RSA", 6 | "use": "sig", 7 | "n": "syWuIlYmoWSl5rBQGOtYGwO5OCCZnhoWBCyl-x5gby5ofc4HNhBoVVMUggk-f_MH-pyMI5yRYsS_aPQ2bmSox2s4i9cPhxqtSAYMhTPwSwQ2BROC7xxi_N0ovp5Ivut5q8TwAn5kQZa_jR9d7JO20BUB7UqbMkBsqg2J8QTtMJ9YtA5BmUn4Y6vhIjTFtvrA6iM4i1cKoUD5Rirt5CYpcKwsLxBZbVk4E4rqgv7G0UlWt6NAs-z7XDkchlNBVpMUuiUBzxHl4LChc7dsWXRaO5vhu3j_2WnxuWCQZPlGoB51jD_ynZ027hhIcoa_tXg28_qb5Al78ZttiRCQDKueAQ", 8 | "kid": "2e3025f26b595f96eac907cc2b9471422bcaeb93", 9 | "e": "AQAB" 10 | }, 11 | { 12 | "use": "sig", 13 | "e": "AQAB", 14 | "alg": "RS256", 15 | "kty": "RSA", 16 | "n": "s44bQ6JmMh-9YBCyCdpbfslwFQ9mloCTgBiX3mwzrBUkliwBRBt5-jJKTXNz_IKERRf43grdSBb3mUiNwq-I6H6EHU0ueyiliGS38rTOrZSK9LM0qy-I8mSNc7p-5MA4Yu-gkBBfvicQ9GZfwlFZpoXt6UIVXywtvNuQNtRsx5oJ8PtbmMPCcA5aFkFl-8YS-4lM6ZNTc9Q6UgWFap3sM9kfCiuISmJs0_SNOzlbLu4FJEA2ZIEqM-aV7kciE4jTeR0W3ks3SotiwitHTvQF89mADa8qEzh5xA0HagKDWnoT0TdF80hdT2lsvggL2r5tllw3gyCVL0LT_pjb12841w", 17 | "kid": "d4cba25e563660a9009d820a1c02022070574e82" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /nee-jdbc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.config.KotlinCompilerVersion 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.jvm") 5 | } 6 | dependencies { 7 | api(project(":nee-core")) 8 | implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION)) 9 | implementation(group = "com.mchange", name = "c3p0", version = "0.9.5.5") 10 | testRuntimeOnly(Libs.H2.h2) 11 | 12 | testImplementation(Libs.Kotest.runnerJunit5Jvm) 13 | } 14 | 15 | apply(from = "../publish-mpp.gradle.kts") 16 | -------------------------------------------------------------------------------- /nee-jdbc/src/main/kotlin/dev/neeffect/nee/effects/jdbc/JDBCConnection.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.jdbc 2 | 3 | import com.mchange.v2.c3p0.ComboPooledDataSource 4 | import dev.neeffect.nee.effects.tx.TxConnection 5 | import dev.neeffect.nee.effects.tx.TxError 6 | import dev.neeffect.nee.effects.tx.TxProvider 7 | import dev.neeffect.nee.effects.tx.TxStarted 8 | import dev.neeffect.nee.effects.utils.Logging 9 | import dev.neeffect.nee.effects.utils.logger 10 | import io.vavr.control.Either 11 | import io.vavr.control.Option 12 | import io.vavr.control.Option.none 13 | import io.vavr.kotlin.some 14 | import java.sql.Connection 15 | import java.sql.Savepoint 16 | 17 | /** 18 | * Standard jdbc connection. 19 | */ 20 | class JDBCConnection( 21 | private val connection: Connection, 22 | private val close: Boolean = false 23 | ) : TxConnection, 24 | Logging { 25 | override fun begin(): Either> = 26 | if (hasTransaction()) { 27 | val savepoint = getResource().setSavepoint() 28 | JDBCTransaction(this, some(savepoint)) 29 | } else { 30 | getResource().autoCommit = false 31 | JDBCTransaction(this) 32 | }.let { Either.right>(it) } 33 | 34 | // TODO handle in nested trx when 35 | override fun continueTx(): Either> = 36 | Either.right>(JDBCTransaction(this)).also { 37 | if (!hasTransaction()) { 38 | getResource().autoCommit = false 39 | } 40 | } 41 | 42 | override fun hasTransaction(): Boolean = !this.getResource().autoCommit 43 | 44 | override fun getResource(): Connection = this.connection 45 | 46 | 47 | override fun close(): Unit = getResource().let { conn -> 48 | if (conn.isClosed) { 49 | logger().warn("connection already closed") 50 | } else { 51 | if (close) { 52 | conn.close() 53 | } 54 | } 55 | } 56 | } 57 | 58 | class JDBCTransaction(val conn: JDBCConnection, val savepoint: Option = none()) : 59 | TxConnection by conn, 60 | TxStarted, 61 | Logging { 62 | override fun commit(): Pair, TxConnection> = 63 | getResource().commit().let { 64 | Pair(Option.none(), conn) // TODO what about autocommit? 65 | } 66 | 67 | override fun rollback(): Pair, TxConnection> = 68 | this.savepoint.map { sp -> 69 | getResource().rollback(sp) 70 | Pair(Option.none(), conn) 71 | }.getOrElse { 72 | getResource().rollback() 73 | Pair(Option.none(), conn) 74 | } 75 | 76 | @Suppress("ReturnUnit") 77 | override fun close() { 78 | logger().info("we do not close ongoing transaction") 79 | } 80 | } 81 | 82 | /** 83 | * Provider of jdbc connection. 84 | */ 85 | class JDBCProvider( 86 | private val connection: ConnectionWrapper, 87 | private val close: Boolean = false 88 | ) : TxProvider { 89 | constructor(connection: Connection) : this(ConnectionWrapper.DirectConnection(connection)) 90 | 91 | constructor(cfg: JDBCConfig) : this(Class.forName(cfg.driverClassName).let { 92 | val pool = ComboPooledDataSource() 93 | pool.user = cfg.user 94 | pool.password = cfg.password 95 | pool.jdbcUrl = cfg.url 96 | ConnectionWrapper.PooledConnection(pool) 97 | }, true) 98 | 99 | override fun getConnection(): TxConnection = 100 | JDBCConnection(connection.conn(), close) 101 | 102 | override fun setConnectionState(newState: TxConnection): JDBCProvider = 103 | JDBCProvider(ConnectionWrapper.DirectConnection(newState.getResource())) 104 | } 105 | 106 | sealed class ConnectionWrapper { 107 | 108 | abstract fun conn(): Connection 109 | 110 | data class DirectConnection(private val connection: Connection) : ConnectionWrapper() { 111 | override fun conn(): Connection = connection 112 | } 113 | 114 | data class PooledConnection(private val pool: ComboPooledDataSource) : ConnectionWrapper() { 115 | override fun conn(): java.sql.Connection = pool.connection 116 | } 117 | } 118 | 119 | data class JDBCConfig( 120 | val driverClassName: String, 121 | val url: String, 122 | val user: String, 123 | val password: String = "" 124 | ) 125 | -------------------------------------------------------------------------------- /nee-jdbc/src/test/kotlin/dev/neeffect/nee/atomic/AtomicRefTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.atomic 2 | 3 | import dev.neeffect.nee.effects.test.get 4 | import io.kotest.core.spec.style.DescribeSpec 5 | import io.kotest.matchers.shouldBe 6 | import java.util.concurrent.Executor 7 | import java.util.concurrent.ExecutorService 8 | import java.util.concurrent.Executors 9 | import java.util.concurrent.TimeUnit 10 | import kotlin.random.Random 11 | 12 | class AtomicRefTest : DescribeSpec({ 13 | describe("simple atomic operations") { 14 | it("should get initial value") { 15 | val initial = AtomicRef(10) 16 | val result = initial.get().perform(Unit).get() 17 | result shouldBe 10 18 | } 19 | it("should get a set value") { 20 | val initial = AtomicRef(10) 21 | val result = initial.set(7).flatMap { 22 | initial.get() 23 | }.perform(Unit).get() 24 | result shouldBe 7 25 | } 26 | 27 | it("should getAndSet value") { 28 | val initial = AtomicRef(10) 29 | val result = initial.getAndSet(8).flatMap { x -> 30 | initial.get().map { y -> 31 | Pair(x, y) 32 | } 33 | }.perform(Unit).get() 34 | result shouldBe Pair(10, 8) 35 | } 36 | it("should update value") { 37 | val initial = AtomicRef(10) 38 | val result = initial.update { x -> x + 1 } 39 | .flatMap { initial.get() } 40 | .perform(Unit).get() 41 | result shouldBe 11 42 | } 43 | it("should get and update value") { 44 | val initial = AtomicRef(10) 45 | val result = initial.getAndUpdate { x -> x + 1 } 46 | .flatMap { before -> 47 | initial.get().map { after-> 48 | Pair(before, after) 49 | } 50 | }.perform(Unit).get() 51 | result shouldBe Pair(10,11) 52 | } 53 | it("should update and get value") { 54 | val initial = AtomicRef(10) 55 | val result = initial.updateAndGet { x -> x + 1 } 56 | .flatMap { before -> 57 | initial.get().map { after-> 58 | Pair(before, after) 59 | } 60 | }.perform(Unit).get() 61 | result shouldBe Pair(11,11) 62 | } 63 | it("should modify and get value") { 64 | val initial = AtomicRef(10) 65 | val result = initial.modifyGet { x -> Pair(x + 1, x+2) } 66 | .flatMap { before -> 67 | initial.get().map { after-> 68 | Pair(before, after) 69 | } 70 | }.perform(Unit).get() 71 | result shouldBe Pair(Pair(11,12),11) 72 | } 73 | it("should modify value") { 74 | val initial = AtomicRef(10) 75 | val result = initial.modify { x -> Pair(x + 1, x+2) } 76 | .flatMap { before -> 77 | initial.get().map { after-> 78 | Pair(before, after) 79 | } 80 | }.perform(Unit).get() 81 | result shouldBe Pair(12,11) 82 | } 83 | it("try update should work") { 84 | val initial = AtomicRef(10) 85 | val result = initial.tryUpdate { x -> x +1 } 86 | .flatMap { before -> 87 | initial.get().map { after-> 88 | Pair(before, after) 89 | } 90 | }.perform(Unit).get() 91 | result shouldBe Pair(true,11) 92 | } 93 | } 94 | describe("multithreaded atomic operations") { 95 | val random = Random(7) 96 | val badIncrementor = { x: Int -> 97 | if (random.nextInt(4) == 0) { 98 | Thread.sleep(1) 99 | } 100 | Pair(x+1, 0) 101 | } 102 | val executor = Executors.newFixedThreadPool(32) 103 | it ("should update counter to 1000" ) { 104 | val initial = AtomicRef(0) 105 | (0 until 1000).forEach { 106 | executor.submit{initial.modifyGet ( badIncrementor ).perform(Unit).get()} 107 | } 108 | executor.shutdown() 109 | executor.awaitTermination(10, TimeUnit.SECONDS) 110 | initial.get().perform(Unit).get() shouldBe 1000 111 | } 112 | } 113 | }) 114 | -------------------------------------------------------------------------------- /nee-security-jdbc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.config.KotlinCompilerVersion 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.jvm") 5 | } 6 | 7 | dependencies { 8 | api(project(":nee-core")) 9 | api(project(":nee-security")) 10 | implementation(project(":nee-jdbc")) 11 | 12 | implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION)) 13 | implementation(Libs.Kotlin.kotlinStdLib) 14 | 15 | implementation(Libs.Vavr.kotlin) { 16 | exclude("org.jetbrains.kotlin") 17 | } 18 | testImplementation(project(":nee-test:nee-security-jdbc-test")) 19 | testImplementation(Libs.Kotest.runnerJunit5Jvm) 20 | testRuntimeOnly(Libs.H2.h2) 21 | testImplementation(Libs.Liquibase.core) 22 | 23 | } 24 | 25 | 26 | 27 | apply(from = "../publish-mpp.gradle.kts") 28 | -------------------------------------------------------------------------------- /nee-security-jdbc/src/main/kotlin/dev/neeffect/nee/security/DBUserRealm.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security 2 | 3 | import dev.neeffect.nee.effects.jdbc.JDBCProvider 4 | import dev.neeffect.nee.security.DBUserRealm.Companion.uuidByteSize 5 | import io.vavr.collection.List 6 | import io.vavr.control.Option 7 | import java.nio.ByteBuffer 8 | import java.sql.Connection 9 | import java.sql.PreparedStatement 10 | import java.sql.ResultSet 11 | import java.util.UUID 12 | import kotlin.collections.contentEquals 13 | 14 | /** 15 | * Security realm with classic User and Roles tables. 16 | */ 17 | class DBUserRealm(private val dbProvider: JDBCProvider) : 18 | UserRealm { 19 | 20 | override fun loginUser(userLogin: String, password: CharArray): Option = 21 | dbProvider.getConnection().getResource().let { jdbcConnection: Connection -> 22 | jdbcConnection.prepareStatement( 23 | "select id, salt, password from users where login = ?" 24 | ).use { statement -> 25 | findUserInDB(statement, userLogin, password, jdbcConnection) 26 | } 27 | } 28 | 29 | private fun findUserInDB( 30 | statement: PreparedStatement, 31 | userLogin: String, 32 | password: CharArray, 33 | jdbcConnection: Connection 34 | ): Option = 35 | statement.run { 36 | setString(1, userLogin) 37 | val r = executeQuery().use { resultSet -> 38 | if (resultSet.next()) { 39 | checkDBRow(resultSet, password, userLogin, jdbcConnection) 40 | } else { 41 | Option.none() 42 | } 43 | } 44 | r 45 | } 46 | 47 | private fun checkDBRow( 48 | resultSet: ResultSet, 49 | password: CharArray, 50 | userLogin: String, 51 | jdbcConnection: Connection 52 | ): Option = run { 53 | val user = resultSet.getBytes(userIdColumn).toUUID() 54 | val salt = resultSet.getBytes(saltColumn) 55 | val passwordHash = resultSet.getBytes(passHashColumn) 56 | val inputHash = passwordHasher.hashPassword(password, salt) 57 | if (passwordHash.contentEquals(inputHash)) { 58 | Option.some(User(user, userLogin, loadRoles(jdbcConnection, user))) 59 | } else { 60 | Option.none() 61 | } 62 | } 63 | 64 | override fun hasRole(user: User, role: UserRole): Boolean = 65 | user.roles.contains(role) 66 | 67 | private tailrec fun extractUserRoles(rs: ResultSet, prev: List) : List = 68 | if (rs.next()) { 69 | val roleName = rs.getString(1) 70 | extractUserRoles(rs, prev.prepend(UserRole(roleName))) 71 | } else { 72 | prev 73 | } 74 | 75 | private fun loadRoles(jdbcConnection: Connection, userId: UUID): List = 76 | jdbcConnection.prepareStatement("SELECT role_name FROM user_roles WHERE user_id = ?").use { statement -> 77 | statement.setBytes(1, userId.toBytes()) 78 | statement.executeQuery().use { resultSet: ResultSet -> 79 | extractUserRoles(resultSet, List.empty()) 80 | } 81 | } 82 | 83 | companion object { 84 | val passwordHasher = PBKDF2Hasher() 85 | internal const val uuidByteSize = 16 86 | private const val userIdColumn = 1 87 | private const val saltColumn = 2 88 | private const val passHashColumn = 3 89 | } 90 | } 91 | 92 | fun ByteArray.toUUID() = 93 | ByteBuffer.wrap(this).let { 94 | UUID(it.long, it.long) 95 | } 96 | 97 | fun UUID.toBytes(): ByteArray = 98 | ByteBuffer.wrap(ByteArray(uuidByteSize)).let { 99 | it.putLong(this.mostSignificantBits) 100 | it.putLong(this.leastSignificantBits) 101 | it.array() 102 | } 103 | -------------------------------------------------------------------------------- /nee-security-jdbc/src/main/kotlin/dev/neeffect/nee/security/PBKDF2Hasher.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security 2 | 3 | import dev.neeffect.nee.security.PBKDF2Hasher.HashParams.algorithm 4 | import dev.neeffect.nee.security.PBKDF2Hasher.HashParams.iterationCount 5 | import dev.neeffect.nee.security.PBKDF2Hasher.HashParams.keyLength 6 | import javax.crypto.SecretKeyFactory 7 | import javax.crypto.spec.PBEKeySpec 8 | 9 | typealias Salt = ByteArray 10 | 11 | class PBKDF2Hasher { 12 | 13 | fun hashPassword(password: CharArray, salt: Salt): ByteArray = 14 | PBEKeySpec(password, salt, iterationCount, keyLength).let { keySpec -> 15 | algorithm.generateSecret(keySpec).encoded 16 | } 17 | 18 | object HashParams { 19 | const val algorithmName = "PBKDF2WithHmacSHA1" 20 | const val iterationCount = 32761 21 | const val keyLength = 128 22 | val algorithm = SecretKeyFactory.getInstance(algorithmName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /nee-security-jdbc/src/test/kotlin/dev/neeffect/nee/security/DBTestConnection.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security 2 | 3 | //object DBTestConnection { 4 | // val dbUrl = "jdbc:h2:mem:test_mem;DB_CLOSE_DELAY=-1" 5 | // val dbUser = "sa" 6 | // val dbPassword = "" 7 | // fun initializeDb() = 8 | // createDbConnection().use { dbConnection -> 9 | // val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(JdbcConnection(dbConnection)) 10 | // val liquibaseChangeLog = Liquibase("db/db.xml", ClassLoaderResourceAccessor(), database) 11 | // liquibaseChangeLog.update(liquibase.Contexts(), liquibase.LabelExpression()) 12 | // }.let { createDbConnection() } 13 | // 14 | // fun createDbConnection() = DriverManager.getConnection( 15 | // dbUrl, 16 | // dbUser, 17 | // dbPassword 18 | // ) 19 | //} 20 | -------------------------------------------------------------------------------- /nee-security-jdbc/src/test/kotlin/dev/neeffect/nee/security/DBUserRealmTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security 2 | 3 | import dev.neeffect.nee.effects.jdbc.JDBCProvider 4 | import io.kotest.core.spec.style.DescribeSpec 5 | import io.kotest.matchers.shouldBe 6 | import io.vavr.collection.List 7 | 8 | class DBUserRealmTest : DescribeSpec({ 9 | describe("dbuser realm") { 10 | dev.neeffect.nee.security.test.TestDB().initializeDb().use { testDb -> 11 | val jdbcProvider = JDBCProvider(testDb.connection) 12 | val userRealm = DBUserRealm(jdbcProvider) 13 | describe("db with a single user") { 14 | testDb.addUser("test1", "testPass", List.of("test", "user", "admin")) 15 | it("test login succeeds") { 16 | val logged = userRealm.loginUser("test1", "testPass".toCharArray()) 17 | logged.isDefined shouldBe true 18 | } 19 | it("login with bad password fails") { 20 | val logged = userRealm.loginUser("test1", "testPas1s".toCharArray()) 21 | logged.isDefined shouldBe false 22 | } 23 | 24 | } 25 | } 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /nee-security-jdbc/src/test/kotlin/dev/neeffect/nee/security/PBKDF2HasherTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.shouldBe 5 | import java.math.BigInteger 6 | 7 | class PBKDF2HasherTest : DescribeSpec({ 8 | describe("pbk2hasher") { 9 | val hasher = PBKDF2Hasher() 10 | describe("password with some salt") { 11 | val salt = ByteArray(16) { 0xCA.toByte() } 12 | val password = "very secret" 13 | val hash = hasher.hashPassword(password.toCharArray(), salt) 14 | it("hashes to expected value") { 15 | BigInteger(1, hash).toString(16) shouldBe "bb70b13aa04c8e21cbaaa54903cb030b" 16 | } 17 | } 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /nee-security/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.config.KotlinCompilerVersion 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.jvm") 5 | } 6 | 7 | dependencies { 8 | implementation(project(":nee-core")) 9 | implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION)) 10 | implementation(Libs.Kotlin.reflect) 11 | implementation(Libs.Vavr.kotlin) { 12 | exclude("org.jetbrains.kotlin") 13 | } 14 | implementation(Libs.Ktor.clientCore) 15 | implementation(Libs.Ktor.clientJsonJvm) 16 | 17 | implementation(Libs.Ktor.clientJackson) { 18 | exclude("org.jetbrains.kotlin") 19 | } 20 | implementation(Libs.Kotlin.coroutinesJdk8) 21 | api("io.fusionauth:fusionauth-jwt:4.0.1") 22 | 23 | testImplementation(Libs.Kotest.runnerJunit5Jvm) 24 | testImplementation(Libs.Ktor.clientMockJvm) 25 | 26 | implementation(Libs.Hoplite.core) 27 | implementation(Libs.Hoplite.yaml) 28 | } 29 | 30 | apply(from = "../publish-mpp.gradle.kts") 31 | 32 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/User.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security 2 | 3 | import io.vavr.collection.List 4 | import io.vavr.kotlin.toVavrList 5 | import java.util.UUID 6 | 7 | data class User( 8 | val id: UUID, 9 | val login: String, 10 | val roles: List, 11 | val displayName: String = login 12 | ) 13 | 14 | data class UserRole(val roleName: String) { 15 | companion object { 16 | fun roles(vararg names: String): List = names.toVavrList() 17 | .map { UserRole(it) } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/UserRealm.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security 2 | 3 | import io.vavr.collection.HashMap 4 | import io.vavr.collection.HashSet 5 | import io.vavr.collection.Set 6 | import io.vavr.control.Option 7 | 8 | interface UserRealm { 9 | fun loginUser(userLogin: String, password: CharArray): Option 10 | 11 | fun hasRole(user: USERID, role: ROLE): Boolean 12 | } 13 | 14 | data class InMemoryUserRealm( 15 | val rolesMap: HashMap> = HashMap.empty(), 16 | val passwords: HashMap = HashMap.empty(), 17 | val userIdMapper: (USERID) -> String = { it.toString() } 18 | ) : UserRealm { 19 | fun withRole(userid: USERID, role: ROLE) = 20 | this.copy(rolesMap = rolesMap.put(userIdMapper(userid), HashSet.of(role)) { _, preVal -> 21 | preVal.add(role) 22 | }) 23 | 24 | fun withPassword(userid: USERID, password: CharArray) = 25 | this.copy(passwords = passwords.put(userid, password)) 26 | 27 | override fun loginUser(userLogin: String, password: CharArray): Option = 28 | passwords.iterator().find { 29 | userIdMapper(it._1) == userLogin 30 | }.filter { 31 | it._2.contentEquals(password) 32 | }.map { 33 | it._1 34 | } 35 | 36 | override fun hasRole(user: USERID, role: ROLE): Boolean = 37 | rolesMap[userIdMapper(user)].map { roles -> 38 | roles.contains(role) 39 | }.getOrElse(false) 40 | } 41 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/jwt/JwtCoder.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.jwt 2 | 3 | import dev.neeffect.nee.effects.time.HasteTimeProvider 4 | import dev.neeffect.nee.effects.time.TimeProvider 5 | import io.fusionauth.jwt.Signer 6 | import io.fusionauth.jwt.Verifier 7 | import io.fusionauth.jwt.domain.JWT 8 | import io.fusionauth.jwt.hmac.HMACSigner 9 | import io.fusionauth.jwt.hmac.HMACVerifier 10 | import io.vavr.collection.HashMap 11 | import io.vavr.collection.Map 12 | import io.vavr.control.Either 13 | import io.vavr.control.Try 14 | 15 | data class JwtConfig( 16 | val expirationInSeconds: Long = 1000, 17 | val issuer: String, 18 | val signerSecret: String 19 | ) 20 | 21 | open class JwtCoderConfigurationModule( 22 | cfg: JwtConfig, 23 | val timeProvider: TimeProvider = HasteTimeProvider() 24 | ) { 25 | 26 | open val config: JwtConfig = cfg 27 | 28 | open val signer: Signer by lazy { 29 | HMACSigner.newSHA256Signer(config.signerSecret) 30 | } 31 | 32 | open val verifier: Verifier by lazy { 33 | HMACVerifier.newVerifier(config.signerSecret) 34 | } 35 | } 36 | 37 | abstract class JwtConfigurationModule( 38 | cfg: JwtConfig, 39 | timeProvider: TimeProvider 40 | ) : JwtCoderConfigurationModule(cfg, timeProvider) { 41 | open val jwtCoder: JwtCoder by lazy { 42 | JwtCoder(this) 43 | } 44 | open val jwtUsersCoder: JwtUsersCoder by lazy { 45 | JwtUsersCoder(jwtCoder, userCoder) 46 | } 47 | 48 | abstract val userCoder: UserCoder 49 | } 50 | 51 | class JwtCoder(private val jwtBaseConfig: JwtCoderConfigurationModule) { 52 | fun createJwt(subject: String, claims: Map = HashMap.empty()): JWT = 53 | jwtBaseConfig.timeProvider.getTimeSource().now().let { now -> 54 | JWT().setIssuer(jwtBaseConfig.config.issuer) 55 | .setIssuedAt(now) 56 | .setSubject(subject) 57 | .setExpiration(now.plusSeconds(jwtBaseConfig.config.expirationInSeconds)) 58 | .also { jwt -> 59 | claims.forEach { name, value -> jwt.addClaim(name, value) } 60 | } 61 | } 62 | 63 | fun signJwt(jwt: JWT) = JWT.getEncoder().encode(jwt, jwtBaseConfig.signer) 64 | 65 | fun decodeJwt(signed: String): Either = Try.of { 66 | jwtBaseConfig.timeProvider.getTimeSource().now().let { time -> 67 | val decoder = JWT.getTimeMachineDecoder(time) 68 | decoder.decode(signed, jwtBaseConfig.verifier) 69 | } 70 | }.toEither().mapLeft { e -> JWTError.WrongJWT(e) } 71 | } 72 | 73 | sealed class JWTError { 74 | class WrongJWT(val cause: Throwable) : JWTError() 75 | } 76 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/jwt/MultiVerifier.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.jwt 2 | 3 | import io.fusionauth.jwt.InvalidJWTSignatureException 4 | import io.fusionauth.jwt.Verifier 5 | import io.fusionauth.jwt.domain.Algorithm 6 | import io.vavr.collection.Seq 7 | 8 | class MultiVerifier(private val verifiers: Seq) : Verifier { 9 | override fun canVerify(algorithm: Algorithm?): Boolean = verifiers.any { it.canVerify(algorithm) } 10 | 11 | @Suppress("ReturnUnit") 12 | override fun verify(algorithm: Algorithm?, message: ByteArray?, signature: ByteArray?): Unit = 13 | verifiers.filter { it.canVerify(algorithm) }.find { verifier -> 14 | try { 15 | verifier.verify(algorithm, message, signature) 16 | true 17 | } catch (e: InvalidJWTSignatureException) { 18 | false 19 | } 20 | }.map { Unit }.getOrElseThrow { InvalidJWTSignatureException() } 21 | } 22 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/jwt/SimpleUserCoder.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.jwt 2 | 3 | import dev.neeffect.nee.security.User 4 | import dev.neeffect.nee.security.UserRole 5 | import io.vavr.collection.Map 6 | import io.vavr.control.Option 7 | import io.vavr.control.Try 8 | import io.vavr.kotlin.hashMap 9 | import io.vavr.kotlin.toVavrList 10 | import java.util.UUID 11 | 12 | class SimpleUserCoder : UserCoder { 13 | override fun userToIdAndMapAnd(u: User): Pair> = 14 | u.id.toString() to hashMap( 15 | loginKey to u.login, 16 | "id" to u.id.toString(), 17 | displayNameKey to u.displayName, 18 | rolesKey to u.roles.map { it.roleName }.joinToString(",") 19 | ) 20 | 21 | override fun mapToUser(id: String, m: Map): Option = 22 | stringToUUID(id).flatMap { uuid -> 23 | m[loginKey].flatMap { login -> 24 | m[displayNameKey].flatMap { displayName -> 25 | m[rolesKey].map { rolesString -> 26 | val roles = 27 | rolesString.split(",").toVavrList().map(::UserRole) 28 | User(uuid, login, roles, displayName) 29 | } 30 | } 31 | } 32 | } 33 | 34 | override fun hasRole(u: User, r: UserRole): Boolean = u.roles.contains(r) 35 | 36 | fun stringToUUID(id: String) = Try.of { UUID.fromString(id) }.toOption() 37 | 38 | companion object { 39 | const val loginKey = "login" 40 | const val rolesKey = "roles" 41 | const val displayNameKey = "displayName" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/jwt/UserCoder.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.jwt 2 | 3 | import io.fusionauth.jwt.domain.JWT 4 | import io.vavr.collection.Map 5 | import io.vavr.control.Option 6 | import io.vavr.kotlin.toVavrMap 7 | 8 | interface UserCoder { 9 | fun userToIdAndMapAnd(u: USER): Pair> 10 | fun mapToUser(id: String, m: Map): Option 11 | fun hasRole(u: USER, r: ROLE): Boolean 12 | } 13 | 14 | class JwtUsersCoder(val jwtCoder: JwtCoder, val coder: UserCoder) { 15 | fun encodeUser(user: USER): JWT = coder.userToIdAndMapAnd(user).let { (id, mapClaims) -> 16 | jwtCoder.createJwt(id, mapClaims) 17 | } 18 | 19 | @Suppress("MutableCollections") 20 | fun decodeUser(jwt: JWT): Option = 21 | coder.mapToUser(jwt.subject, jwt.allClaims.toVavrMap().mapValues { it.toString() }) 22 | 23 | fun hasRole(u: USER, r: ROLE): Boolean = coder.hasRole(u, r) 24 | } 25 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/oauth/OauthConfig.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.oauth 2 | 3 | import io.vavr.collection.HashMap 4 | import io.vavr.collection.Map 5 | import io.vavr.control.Option 6 | 7 | data class OauthConfig( 8 | val providers: Map = HashMap.empty() 9 | ) { 10 | fun getProviderConfig(ouathProviderName: OauthProviderName): Option = 11 | providers[ouathProviderName.providerName] 12 | 13 | fun getClientId(ouathProviderName: OauthProviderName) = 14 | getProviderConfig(ouathProviderName).map { it.clientId }.getOrElseThrow { 15 | IllegalStateException("unconfigured provider: $ouathProviderName") 16 | } 17 | 18 | fun getClientSecret(ouathProviderName: OauthProviderName) = 19 | getProviderConfig(ouathProviderName).map { it.clientSecret }.getOrElseThrow { 20 | IllegalStateException("unconfigured provider: $ouathProviderName") 21 | } 22 | } 23 | 24 | data class ProviderConfig( 25 | val clientId: String, 26 | val clientSecret: String, 27 | val certificatesFile: Option = Option.none() 28 | ) 29 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/oauth/OauthConfigModule.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.oauth 2 | 3 | import dev.neeffect.nee.effects.time.HasteTimeProvider 4 | import dev.neeffect.nee.effects.time.TimeProvider 5 | import dev.neeffect.nee.security.User 6 | import dev.neeffect.nee.security.UserRole 7 | import dev.neeffect.nee.security.jwt.JwtConfig 8 | import dev.neeffect.nee.security.jwt.JwtConfigurationModule 9 | import dev.neeffect.nee.security.jwt.SimpleUserCoder 10 | import dev.neeffect.nee.security.jwt.UserCoder 11 | import dev.neeffect.nee.security.state.ServerVerifier 12 | import io.ktor.client.HttpClient 13 | import io.ktor.client.features.json.JacksonSerializer 14 | import io.ktor.client.features.json.JsonFeature 15 | import io.vavr.collection.Seq 16 | import io.vavr.kotlin.list 17 | import java.security.KeyPair 18 | import java.security.SecureRandom 19 | import java.util.Random 20 | import java.util.UUID 21 | 22 | abstract class OauthConfigModule( 23 | val config: OauthConfig, 24 | val jwtConfig: JwtConfig 25 | ) { 26 | 27 | open val randomGenerator: Random by lazy { 28 | SecureRandom() 29 | } 30 | open val baseTimeProvider: TimeProvider by lazy { 31 | HasteTimeProvider() 32 | } 33 | 34 | open val keyPair: KeyPair by lazy { 35 | ServerVerifier.generateKeyPair() 36 | } 37 | 38 | open val serverVerifier: ServerVerifier by lazy { 39 | ServerVerifier(rng = this.randomGenerator, keyPair = keyPair) 40 | } 41 | open val httpClient by lazy { 42 | HttpClient() { 43 | install(JsonFeature) { // TODO - move it so that it is tested (it was not) 44 | serializer = JacksonSerializer() 45 | } 46 | } 47 | } 48 | 49 | open val jwtConfigModule: JwtConfigurationModule by lazy { 50 | 51 | object : JwtConfigurationModule(jwtConfig, baseTimeProvider) { 52 | override val userCoder: UserCoder = this@OauthConfigModule.userCoder 53 | } 54 | } 55 | 56 | abstract val userEncoder: (OauthProviderName, OauthResponse) -> USER 57 | 58 | abstract val userRoles: (OauthProviderName, OauthResponse) -> Seq 59 | 60 | abstract val userCoder: UserCoder 61 | } 62 | 63 | open class SimpleOauthConfigModule( 64 | config: OauthConfig, 65 | jwtConfig: JwtConfig 66 | ) : OauthConfigModule(config, jwtConfig) { 67 | override val userCoder: UserCoder = SimpleUserCoder() 68 | override val userEncoder: (OauthProviderName, OauthResponse) -> User = { provider, oauthResponse -> 69 | val uuid = UUID(randomGenerator.nextLong(), randomGenerator.nextLong()) 70 | val roles = userRoles(provider, oauthResponse) 71 | 72 | User( 73 | uuid, 74 | "${provider.providerName}:${oauthResponse.subject}", 75 | roles.toList(), 76 | oauthResponse.displayName.getOrElse(oauthResponse.subject) 77 | ) 78 | } 79 | override val userRoles: (OauthProviderName, OauthResponse) -> Seq = { _, _ -> 80 | list(oauthUser) 81 | } 82 | 83 | val oauthUser = UserRole("oauthUser") 84 | } 85 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/oauth/OauthProviderName.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.oauth 2 | 3 | import io.vavr.control.Option 4 | import io.vavr.kotlin.option 5 | 6 | enum class OauthProviderName(val providerName: String) { 7 | Google("google"), 8 | Github("github"); 9 | 10 | fun getByName(name: String): Option = OauthProviderName.values().find { 11 | name == it.providerName 12 | }.option() 13 | } 14 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/oauth/OauthProviders.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.oauth 2 | 3 | enum class OauthProviders(val providerName: String) { 4 | Google("google") 5 | } 6 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/oauth/OauthService.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.oauth 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.NoEffect 5 | import dev.neeffect.nee.effects.Out 6 | import dev.neeffect.nee.effects.security.SecurityErrorType 7 | import dev.neeffect.nee.security.oauth.config.GithubOAuth 8 | import io.vavr.control.Either 9 | import io.vavr.control.Option 10 | import io.vavr.kotlin.some 11 | import io.vavr.kotlin.toVavrMap 12 | 13 | class OauthService(private val oauthConfig: OauthConfigModule) { 14 | 15 | private val googleOpenId = GoogleOpenId(oauthConfig) 16 | private val githubOAuth = GithubOAuth(oauthConfig) 17 | 18 | fun login( 19 | code: String, 20 | state: String, 21 | redirectUri: String, 22 | oauthProvider: OauthProviderName 23 | ): Nee = 24 | findOauthProvider(oauthProvider).map { provider -> 25 | if (oauthConfig.serverVerifier.verifySignedText(state)) { 26 | provider.verifyOauthToken(code, redirectUri, state).map { oauthResponse -> 27 | println("validate idToken $oauthResponse") // TODO 28 | val user = oauthConfig.userEncoder(oauthProvider, oauthResponse) 29 | val jwt = oauthConfig.jwtConfigModule.jwtUsersCoder.encodeUser(user) 30 | val signedJwt = oauthConfig.jwtConfigModule.jwtCoder.signJwt(jwt) 31 | LoginResult(signedJwt, oauthResponse.displayName, oauthResponse.subject) 32 | } 33 | } else { 34 | Nee.constWithError(NoEffect()) { _ -> 35 | Out.left(SecurityErrorType.MalformedCredentials("state unrecognized: $state")) 36 | } 37 | } 38 | }.getOrElse { 39 | Nee.constWithError(NoEffect()) { _ -> 40 | Out.left(SecurityErrorType.NoSecurityCtx) 41 | } 42 | } 43 | 44 | fun generateApiCall(oauthProvider: OauthProviderName, redirectUrl: String): Option = 45 | findOauthProvider(oauthProvider).map { 46 | it.generateApiCall(redirectUrl) 47 | } 48 | 49 | private fun findOauthProvider(oauthProvider: OauthProviderName): Option = when (oauthProvider) { 50 | OauthProviderName.Google -> some(googleOpenId) 51 | OauthProviderName.Github -> some(githubOAuth) 52 | } 53 | 54 | fun decodeUser(jwtToken: String): Either = 55 | oauthConfig.jwtConfigModule.jwtCoder.decodeJwt(jwtToken).mapLeft { 56 | SecurityErrorType.MalformedCredentials(it.toString()) 57 | }.flatMap { jwt -> 58 | 59 | oauthConfig.userCoder.mapToUser(jwt.subject, jwt.allClaims.toVavrMap().mapValues { it.toString() }) 60 | .toEither { 61 | SecurityErrorType.UnknownUser 62 | } 63 | } 64 | } 65 | 66 | data class LoginResult( 67 | val encodedToken: String, 68 | val displayName: Option, 69 | val subject: String 70 | ) 71 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/oauth/config/GithubOAuth.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.oauth.config 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | import dev.neeffect.nee.Nee 5 | import dev.neeffect.nee.NoEffect 6 | import dev.neeffect.nee.effects.Out 7 | import dev.neeffect.nee.effects.async.InPlaceExecutor 8 | import dev.neeffect.nee.effects.security.SecurityErrorType 9 | import dev.neeffect.nee.effects.utils.Logging 10 | import dev.neeffect.nee.effects.utils.logger 11 | import dev.neeffect.nee.security.oauth.OauthConfigModule 12 | import dev.neeffect.nee.security.oauth.OauthProvider 13 | import dev.neeffect.nee.security.oauth.OauthProviderName 14 | import dev.neeffect.nee.security.oauth.OauthResponse 15 | import dev.neeffect.nee.security.oauth.OauthTokens 16 | import io.ktor.client.request.forms.submitForm 17 | import io.ktor.client.request.get 18 | import io.ktor.client.request.header 19 | import io.ktor.http.ContentType 20 | import io.ktor.http.HttpHeaders 21 | import io.ktor.http.Parameters 22 | import io.vavr.concurrent.Future 23 | import io.vavr.control.Either 24 | import io.vavr.kotlin.option 25 | import kotlinx.coroutines.GlobalScope 26 | import kotlinx.coroutines.future.future 27 | import java.util.concurrent.CompletableFuture 28 | 29 | class GithubOAuth( 30 | private val oauthConfigModule: OauthConfigModule 31 | ) : OauthProvider, Logging { 32 | 33 | override fun generateApiCall(redirect: String) = 34 | apiUrlTemplate( 35 | oauthConfigModule.config.getClientId(OauthProviderName.Github), 36 | redirect, 37 | oauthConfigModule.serverVerifier.generateRandomSignedState() 38 | ) 39 | 40 | override fun verifyOauthToken(code: String, redirectUri: String, state: String) = 41 | Nee.constWithError(NoEffect()) { _ -> 42 | 43 | Out.Companion.fromFuture( 44 | Future.fromCompletableFuture(InPlaceExecutor, getTokens(code, redirectUri, state)).flatMap { result -> 45 | val accessToken = result.accessToken 46 | val userData: Future = 47 | Future.fromCompletableFuture(InPlaceExecutor, getUserData(accessToken)) 48 | userData.map { gitubUser -> 49 | Either.right( 50 | OauthResponse( 51 | result, 52 | gitubUser.id.toString(), 53 | gitubUser.name.option().orElse(gitubUser.login.option()), 54 | gitubUser.email.option() 55 | ) 56 | ) 57 | } 58 | } 59 | ) 60 | } 61 | 62 | private fun getUserData(accessToken: String) = GlobalScope.future { 63 | val result: GithubUserData = oauthConfigModule.httpClient.get( 64 | scheme = "https", 65 | host = "api.github.com", 66 | path = "/user" 67 | ) { 68 | this.header(HttpHeaders.Authorization, "Bearer $accessToken") 69 | } 70 | result 71 | } 72 | 73 | @SuppressWarnings("TooGenericExceptionCaught") 74 | private fun getTokens(code: String, redirectUri: String, state: String): CompletableFuture = 75 | GlobalScope.future { 76 | try { 77 | val result: OauthTokens = oauthConfigModule.httpClient.submitForm( 78 | url = "https://github.com/login/oauth/access_token", 79 | 80 | formParameters = Parameters.build { 81 | append("code", code) 82 | append("client_id", oauthConfigModule.config.getClientId(OauthProviderName.Github)) 83 | append("client_secret", oauthConfigModule.config.getClientSecret(OauthProviderName.Github)) 84 | append("redirect_uri", redirectUri) 85 | append("state", state) 86 | }, 87 | encodeInQuery = false, 88 | 89 | ) { 90 | header(HttpHeaders.Accept, ContentType.Application.Json.toString()) 91 | } 92 | result 93 | } catch (e: Exception) { 94 | logger().warn(e.message, e) 95 | throw e 96 | } 97 | } 98 | 99 | companion object { 100 | fun apiUrlTemplate(clientId: String, redirect: String, state: String) = 101 | """ 102 | https://github.com/login/oauth/authorize? 103 | client_id=$clientId& 104 | redirect_uri=$redirect& 105 | state=$state& 106 | allow_signup=true""".trimIndent().replace("\n", "") 107 | } 108 | } 109 | 110 | @JsonIgnoreProperties(ignoreUnknown = true) 111 | data class GithubUserData( 112 | val login: String, 113 | val id: Long, 114 | val email: String, 115 | val name: String? = null, 116 | val company: String? = null 117 | ) 118 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/oauth/config/OauthConfigLoder.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.oauth.config 2 | 3 | import com.sksamuel.hoplite.ConfigLoader 4 | import com.sksamuel.hoplite.ConfigResult 5 | import com.sksamuel.hoplite.DecoderContext 6 | import com.sksamuel.hoplite.Node 7 | import com.sksamuel.hoplite.PropertySource 8 | import com.sksamuel.hoplite.decoder.Decoder 9 | import com.sksamuel.hoplite.decoder.MapDecoder 10 | import com.sksamuel.hoplite.fp.Validated 11 | import com.sksamuel.hoplite.yaml.YamlParser 12 | import dev.neeffect.nee.security.UserRole 13 | import dev.neeffect.nee.security.jwt.JwtConfig 14 | import dev.neeffect.nee.security.oauth.OauthConfig 15 | import dev.neeffect.nee.security.oauth.OauthProviderName 16 | import dev.neeffect.nee.security.oauth.OauthResponse 17 | import io.vavr.collection.Map 18 | import io.vavr.collection.Seq 19 | import io.vavr.control.Either 20 | import io.vavr.kotlin.toVavrMap 21 | import java.nio.file.Path 22 | import kotlin.reflect.KType 23 | import kotlin.reflect.full.isSubtypeOf 24 | import kotlin.reflect.full.starProjectedType 25 | 26 | typealias RolesMapper = (OauthProviderName, OauthResponse) -> Seq 27 | 28 | class OauthConfigLoder(private val configPath: Path) { 29 | val yamlParser = YamlParser() 30 | fun loadOauthConfig(): Either = ConfigLoader.Builder() 31 | .addFileExtensionMapping("yml", yamlParser) 32 | .addSource(PropertySource.path(configPath.resolve("oauthConfig.yml"))) 33 | .addDecoder(VMapDecoder()) 34 | .build() 35 | .loadConfig() 36 | .foldi({ error -> 37 | Either.left(ConfigError(error.description())) 38 | }, { cfg -> 39 | Either.right(cfg) 40 | }) 41 | 42 | fun loadJwtConfig(): Either = 43 | ConfigLoader.Builder() 44 | .addFileExtensionMapping("yml", yamlParser) 45 | .addSource(PropertySource.path(configPath.resolve("jwtConfig.yml"))) 46 | .build() 47 | .loadConfig() 48 | .foldi({ error -> 49 | Either.left(ConfigError(error.description())) 50 | }, { cfg -> 51 | Either.right(cfg) 52 | }) 53 | 54 | fun loadConfig(rolesMapper: RolesMapper) = loadOauthConfig().flatMap { oauthConf -> 55 | loadJwtConfig().map { jwtConf -> 56 | OauthModule(oauthConf, jwtConf, rolesMapper) 57 | } 58 | } 59 | } 60 | 61 | data class ConfigError(val msg: String) 62 | 63 | // TODO - report as problem 64 | inline fun Validated.foldi(ifInvalid: (E) -> T, ifValid: (A) -> T): T = when (this) { 65 | is Validated.Invalid -> ifInvalid(error) 66 | is Validated.Valid -> ifValid(value) 67 | } 68 | 69 | internal class VMapDecoder : Decoder> { 70 | private val hMapDecoder = MapDecoder() 71 | override fun decode(node: Node, type: KType, context: DecoderContext): ConfigResult> = 72 | hMapDecoder.decode(node, type, context).map { kotlinMap -> 73 | kotlinMap.toVavrMap() 74 | } 75 | 76 | override fun supports(type: KType): Boolean = 77 | type.isSubtypeOf(Map::class.starProjectedType) 78 | } 79 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/oauth/config/OauthModule.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.oauth.config 2 | 3 | import dev.neeffect.nee.security.UserRole 4 | import dev.neeffect.nee.security.jwt.JwtConfig 5 | import dev.neeffect.nee.security.oauth.OauthConfig 6 | import dev.neeffect.nee.security.oauth.OauthProviderName 7 | import dev.neeffect.nee.security.oauth.OauthResponse 8 | import dev.neeffect.nee.security.oauth.OauthService 9 | import dev.neeffect.nee.security.oauth.SimpleOauthConfigModule 10 | import io.vavr.collection.Seq 11 | 12 | class OauthModule( 13 | oathConfig: OauthConfig, 14 | jwtConfig: JwtConfig, 15 | private val rolesMapper: RolesMapper 16 | ) : SimpleOauthConfigModule(oathConfig, jwtConfig) { 17 | 18 | val oauthService by lazy { 19 | OauthService(this) 20 | } 21 | 22 | override val userRoles: (OauthProviderName, OauthResponse) -> Seq = rolesMapper 23 | } 24 | -------------------------------------------------------------------------------- /nee-security/src/main/kotlin/dev/neeffect/nee/security/state/ServerVerifier.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.state 2 | 3 | import io.vavr.control.Option 4 | import io.vavr.control.Try 5 | import java.io.InputStream 6 | import java.io.ObjectInputStream 7 | import java.io.ObjectOutputStream 8 | import java.nio.file.Files 9 | import java.nio.file.Path 10 | import java.nio.file.Paths 11 | import java.security.KeyPair 12 | import java.security.KeyPairGenerator 13 | import java.security.SecureRandom 14 | import java.security.Signature 15 | import java.util.Base64 16 | import java.util.Random 17 | 18 | /** 19 | * Utility to use - for CSRF and similar 20 | */ 21 | class ServerVerifier( 22 | private val rng: Random = SecureRandom(), 23 | private val keyPair: KeyPair = generateKeyPair() 24 | ) { 25 | 26 | fun generateRandomSignedState(): String = 27 | ByteArray(randomStateContentLength).let { rndArray -> 28 | rng.nextBytes(rndArray) 29 | val encoded = Base64.getEncoder().encodeToString(rndArray) 30 | val signature = signArray(rndArray) 31 | encoded + "@" + signature 32 | } 33 | 34 | fun verifySignedText(text: String) = 35 | text.split("@").let { splitted -> 36 | when (splitted.size) { 37 | 2 -> verifyText(splitted[0], splitted[1]).getOrElse(false) 38 | else -> false 39 | } 40 | } 41 | 42 | private fun signArray(data: ByteArray): String = 43 | Signature.getInstance("SHA1WithRSA").let { sig -> 44 | sig.initSign(keyPair.private) 45 | sig.update(data) 46 | Base64.getEncoder().encodeToString(sig.sign()) 47 | } 48 | 49 | private fun verifyText(base64Text: String, signature: String): Try = Try.of { 50 | val sig: Signature = Signature.getInstance("SHA1WithRSA") 51 | sig.initVerify(keyPair.public) 52 | val data = Base64.getDecoder().decode(base64Text) 53 | sig.update(data) 54 | val signatureBytes = Base64.getDecoder().decode(signature) 55 | sig.verify(signatureBytes) 56 | } 57 | 58 | companion object { 59 | const val randomStateContentLength = 16 60 | private const val keySize = 1024 61 | fun generateKeyPair(): KeyPair = with( 62 | KeyPairGenerator.getInstance("RSA").apply { 63 | initialize(keySize) 64 | }) { 65 | genKeyPair() 66 | } 67 | 68 | fun loadKeyPair(path: Path): Option = Try.of { 69 | Files.newInputStream(path).use { 70 | loadKeyPair(it) 71 | } 72 | }.toOption().flatMap { it } 73 | 74 | fun loadKeyPair(inputStream: InputStream): Option = Try.of { 75 | ObjectInputStream(inputStream).use { objectStream -> 76 | objectStream.readObject() as KeyPair 77 | } 78 | }.toOption() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /nee-security/src/test/kotlin/dev/neeffect/nee/effects/security/SecuredRunEffectTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.security 2 | 3 | import dev.neeffect.nee.Nee 4 | import dev.neeffect.nee.effects.flatMap 5 | import dev.neeffect.nee.effects.toFuture 6 | import io.kotest.core.spec.style.BehaviorSpec 7 | import io.kotest.matchers.shouldBe 8 | import io.vavr.collection.List 9 | 10 | internal class SecuredRunEffectTest : BehaviorSpec({ 11 | 12 | Given("secure provider") { 13 | 14 | val secEffect = SecuredRunEffect>("test") 15 | val f = Nee.with(secEffect, businessFunction) 16 | When("function called with test user ") { 17 | val testSecurityProvider = SimpleSecurityProvider("test", List.of("test")) 18 | val result = f.perform(testSecurityProvider) 19 | .flatMap { it } 20 | Then("called with correct user") { 21 | result.toFuture().get().get() shouldBe "called by: test" 22 | } 23 | } 24 | When("function called without roles test user ") { 25 | val testSecurityProvider = SimpleSecurityProvider("test", List.empty()) 26 | val result = f.perform(testSecurityProvider) 27 | .flatMap { it } 28 | Then("function is not called") { 29 | result.toFuture().get().isLeft shouldBe true 30 | } 31 | } 32 | } 33 | }) { 34 | companion object { 35 | val businessFunction = { securityProvider: SecurityProvider -> 36 | securityProvider.getSecurityContext().flatMap { 37 | it.getCurrentUser().map { 38 | "called by: $it" 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /nee-security/src/test/kotlin/dev/neeffect/nee/effects/security/SimpleSecurityProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.security 2 | 3 | import dev.neeffect.nee.effects.Out 4 | import io.vavr.collection.List 5 | 6 | class SimpleSecurityProvider(user: USER, roles: List) : SecurityProvider { 7 | private val ctx = SimpleSecurityContext(user, roles) 8 | override fun getSecurityContext(): Out> = Out.right(ctx) 9 | 10 | internal class SimpleSecurityContext(private val user: USER, private val roles: List) : 11 | SecurityCtx { 12 | override fun getCurrentUser(): Out = Out.right(user) 13 | override fun hasRole(role: ROLE): Boolean = roles.contains(role) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nee-security/src/test/kotlin/dev/neeffect/nee/effects/security/oauth/OauthServiceTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.security.oauth 2 | 3 | import dev.neeffect.nee.effects.security.SecurityErrorType 4 | import dev.neeffect.nee.effects.test.get 5 | import dev.neeffect.nee.effects.test.getLeft 6 | import dev.neeffect.nee.security.oauth.OauthProviderName 7 | import dev.neeffect.nee.security.oauth.OauthService 8 | import io.kotest.core.spec.style.DescribeSpec 9 | import io.kotest.matchers.shouldBe 10 | import io.kotest.matchers.string.shouldHaveMinLength 11 | import io.kotest.matchers.types.shouldBeTypeOf 12 | import io.vavr.kotlin.some 13 | 14 | internal class OauthServiceTest : DescribeSpec({ 15 | 16 | describe("oauth service") { 17 | val testModule = GoogleOpenIdTest.createTestModule() 18 | val service = OauthService(testModule) 19 | describe("login to google") { 20 | val result = service.login( 21 | "acode", 22 | GoogleOpenIdTest.preservedState, 23 | "http://localhost:8080", 24 | OauthProviderName.Google 25 | ) 26 | .perform(Unit) 27 | 28 | it("should be successful") { 29 | result.get().encodedToken shouldHaveMinLength 20 30 | } 31 | //TODO - actually think what is subject here 32 | it("should contain user id in token") { 33 | val jwt = result.get().encodedToken 34 | testModule.jwtConfigModule.jwtCoder.decodeJwt(jwt) 35 | .get().subject shouldBe "ba419d35-0dfe-8af7-aee7-bbe10c45c028" 36 | } 37 | it("should contain user name") { 38 | result.get().displayName shouldBe some("Jarek Ratajski") 39 | } 40 | it("encoded token should contain user name") { 41 | val jwt = result.get().encodedToken 42 | service.decodeUser(jwt).get().displayName shouldBe "Jarek Ratajski" 43 | } 44 | } 45 | it("should create oauth api call url") { 46 | val url = service.generateApiCall(OauthProviderName.Google, "http://globalpost.any/mypath") 47 | url shouldBe some(expectedUrlCall) 48 | } 49 | describe("login with bad state") { 50 | val result = service.login("acode", "really bad state", "http://localhost:8080", OauthProviderName.Google) 51 | .perform(Unit) 52 | it("should fail") { 53 | result.getLeft().shouldBeTypeOf() 54 | } 55 | } 56 | 57 | 58 | } 59 | 60 | }) { 61 | companion object { 62 | //notice - this is volatile and may change if scenario is changed (depends on random sequence) 63 | const val expectedUrlCall = 64 | "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=testId&scope=openid%20profile%20email%20https://www.googleapis.com/auth/user.organization.read&redirect_uri=http://globalpost.any/mypath&state=5DwIT0u7K/GDne5GbYUstQ==@Z0GCZibsGi2m32XO2BO3J/i7GyncfvHwADR7y2lZxm9NoVYGNZ2k7reTDuZ4GLbdE4lXZMA8bG3vHLM9ZDYo3XQZ7nXUZa+/FeO12M2ire7gYCHOLf+V/4Xl/hbetiGHT7z5FnzlrtVlG6wQZaZJBn2NdzuLZ4grQHY8jThP8nI=&login_hint=jsmith@example.com&nonce=0.6655489" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /nee-security/src/test/kotlin/dev/neeffect/nee/effects/security/oauth/config/OauthConfigLoderTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.security.oauth.config 2 | 3 | import dev.neeffect.nee.security.oauth.OauthProviderName 4 | import dev.neeffect.nee.security.oauth.config.OauthConfigLoder 5 | import io.kotest.core.spec.style.DescribeSpec 6 | import io.kotest.matchers.shouldBe 7 | import java.nio.file.Paths 8 | 9 | class OauthConfigLoderTest : DescribeSpec({ 10 | describe("oauth config loader") { 11 | 12 | val confFolder = Paths.get(OauthConfigLoderTest::class.java.getResource("/conf").toURI()) 13 | val configLoader = OauthConfigLoder(confFolder) 14 | it("should load jwtConf") { 15 | val jwt = configLoader.loadJwtConfig() 16 | jwt.get().issuer shouldBe "test" 17 | } 18 | it("should load oauthConf") { 19 | val oauth = configLoader.loadOauthConfig() 20 | oauth.get().getClientSecret(OauthProviderName.Google) shouldBe "googleClientSecret" 21 | } 22 | } 23 | 24 | }) 25 | -------------------------------------------------------------------------------- /nee-security/src/test/kotlin/dev/neeffect/nee/effects/security/state/ServerStateCheckTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.effects.security.state 2 | 3 | import dev.neeffect.nee.security.state.ServerVerifier 4 | import io.kotest.core.spec.style.DescribeSpec 5 | import io.kotest.matchers.shouldBe 6 | import java.util.* 7 | 8 | internal class ServerStateCheckTest : DescribeSpec({ 9 | describe("server state") { 10 | val serverState = ServerVerifier(Random(44L)) 11 | val signedState = serverState.generateRandomSignedState() 12 | it("should verify signed state") { 13 | serverState.verifySignedText(signedState) shouldBe true 14 | } 15 | 16 | it("should fail verification of empty text") { 17 | serverState.verifySignedText("") shouldBe false 18 | } 19 | 20 | it("should fail verification of produced text") { 21 | serverState.verifySignedText("qGMeurZQOqugTVOzHvXA2g==@mJ2Y79oAAG3vifad3R/dFih749AjoWLx/EQ9lWa97RKXAstpXu2lMo9jGLAV8zN2eyY1tVvJ3RMdco/rngw9Cg+7sFp4QkSmAOHGXsQKiWs/nig1TkvpviNLNjJliE/WrmI7IPCL6x39hpgvvpGA/oo+iSyJdNct7CvLctqoetOs=") shouldBe false 22 | } 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /nee-security/src/test/kotlin/dev/neeffect/nee/security/jwt/JwtUsersTest.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.jwt 2 | 3 | import dev.neeffect.nee.effects.time.HasteTimeProvider 4 | import dev.neeffect.nee.security.User 5 | import dev.neeffect.nee.security.UserRole 6 | import io.haste.Haste 7 | import io.kotest.core.spec.style.DescribeSpec 8 | import io.kotest.matchers.collections.shouldContain 9 | import io.kotest.matchers.shouldBe 10 | import io.vavr.kotlin.list 11 | import java.time.Clock 12 | import java.time.Instant 13 | import java.time.ZoneId 14 | import java.util.* 15 | 16 | internal class JwtUsersCoderTest : DescribeSpec({ 17 | describe("jwtuserscoder") { 18 | val jwtUsersCoder = JwtUsersCoder(jwtTestModule.jwtCoder, SimpleUserCoder()) 19 | val uuid = UUID(0, 1) 20 | val user = User( 21 | uuid, 22 | "badmin", 23 | list( 24 | UserRole("editor"), 25 | UserRole("reader") 26 | ), 27 | "mirek" 28 | ) 29 | val jwt = jwtUsersCoder.encodeUser(user) 30 | describe("decoded user") { 31 | val decodedUser = jwtUsersCoder.decodeUser(jwt) 32 | it("has id") { 33 | decodedUser.get().id shouldBe uuid 34 | } 35 | it("has login") { 36 | decodedUser.get().login shouldBe "badmin" 37 | } 38 | it("has role in object") { 39 | decodedUser.get().roles shouldContain UserRole("reader") 40 | } 41 | it("has given role ") { 42 | jwtUsersCoder.hasRole(decodedUser.get(), UserRole("reader")) shouldBe true 43 | } 44 | it("has displayName") { 45 | decodedUser.get().displayName shouldBe "mirek" 46 | } 47 | 48 | } 49 | 50 | } 51 | }) { 52 | companion object { 53 | val testConfig = JwtConfig(1000, "neekt takee", "la secret") 54 | val haste = Haste.TimeSource.withFixedClock( 55 | Clock.fixed(Instant.parse("2020-10-24T22:22:03.00Z"), ZoneId.of("Europe/Berlin")) 56 | ) 57 | val jwtTestModule = object : JwtConfigurationModule( 58 | testConfig, 59 | HasteTimeProvider(haste) 60 | ) { 61 | override val userCoder: UserCoder = SimpleUserCoder() 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /nee-security/src/test/resources/conf/jwtConfig.yml: -------------------------------------------------------------------------------- 1 | issuer: "test" 2 | signerSecret: "notsosecret_secret" 3 | -------------------------------------------------------------------------------- /nee-security/src/test/resources/conf/oauthConfig.yml: -------------------------------------------------------------------------------- 1 | providers: 2 | google: 3 | clientId: "googleClientIdTest" 4 | clientSecret: "googleClientSecret" 5 | -------------------------------------------------------------------------------- /nee-security/src/test/resources/google/keys.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "alg": "RS256", 5 | "kty": "RSA", 6 | "use": "sig", 7 | "n": "syWuIlYmoWSl5rBQGOtYGwO5OCCZnhoWBCyl-x5gby5ofc4HNhBoVVMUggk-f_MH-pyMI5yRYsS_aPQ2bmSox2s4i9cPhxqtSAYMhTPwSwQ2BROC7xxi_N0ovp5Ivut5q8TwAn5kQZa_jR9d7JO20BUB7UqbMkBsqg2J8QTtMJ9YtA5BmUn4Y6vhIjTFtvrA6iM4i1cKoUD5Rirt5CYpcKwsLxBZbVk4E4rqgv7G0UlWt6NAs-z7XDkchlNBVpMUuiUBzxHl4LChc7dsWXRaO5vhu3j_2WnxuWCQZPlGoB51jD_ynZ027hhIcoa_tXg28_qb5Al78ZttiRCQDKueAQ", 8 | "kid": "2e3025f26b595f96eac907cc2b9471422bcaeb93", 9 | "e": "AQAB" 10 | }, 11 | { 12 | "use": "sig", 13 | "e": "AQAB", 14 | "alg": "RS256", 15 | "kty": "RSA", 16 | "n": "s44bQ6JmMh-9YBCyCdpbfslwFQ9mloCTgBiX3mwzrBUkliwBRBt5-jJKTXNz_IKERRf43grdSBb3mUiNwq-I6H6EHU0ueyiliGS38rTOrZSK9LM0qy-I8mSNc7p-5MA4Yu-gkBBfvicQ9GZfwlFZpoXt6UIVXywtvNuQNtRsx5oJ8PtbmMPCcA5aFkFl-8YS-4lM6ZNTc9Q6UgWFap3sM9kfCiuISmJs0_SNOzlbLu4FJEA2ZIEqM-aV7kciE4jTeR0W3ks3SotiwitHTvQF89mADa8qEzh5xA0HagKDWnoT0TdF80hdT2lsvggL2r5tllw3gyCVL0LT_pjb12841w", 17 | "kid": "d4cba25e563660a9009d820a1c02022070574e82" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /nee-security/src/test/resources/keys/testServerKey.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neeffect/nee/f44d60abab751da1d07529b532fc39ae6a95056d/nee-security/src/test/resources/keys/testServerKey.bin -------------------------------------------------------------------------------- /nee-test/nee-core-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api(project(":nee-core")) 3 | 4 | } 5 | 6 | apply(from = "../../publish-mpp.gradle.kts") 7 | -------------------------------------------------------------------------------- /nee-test/nee-ctx-web-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.config.KotlinCompilerVersion 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.jvm") 5 | } 6 | 7 | 8 | 9 | dependencies { 10 | api(project(":nee-ctx-web-ktor")) 11 | implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION)) 12 | implementation(Libs.Ktor.serverCore) 13 | implementation(Libs.Ktor.serverTestHost) 14 | implementation(Libs.Ktor.jackson) 15 | implementation(Libs.Vavr.jackson) 16 | implementation(Libs.Jackson.jacksonModuleKotlin) 17 | } 18 | 19 | 20 | apply(from = "../../publish-mpp.gradle.kts") 21 | -------------------------------------------------------------------------------- /nee-test/nee-ctx-web-test/src/main/kotlin/dev/neeffect/nee/web/test/TestServer.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.web.test 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import dev.neeffect.nee.ctx.web.WebContextProvider 5 | import dev.neeffect.nee.ctx.web.pure.InitialRouting 6 | import dev.neeffect.nee.ctx.web.pure.Routing 7 | import dev.neeffect.nee.ctx.web.pure.RoutingDef 8 | import dev.neeffect.nee.effects.tx.TxProvider 9 | import io.ktor.application.Application 10 | import io.ktor.application.install 11 | import io.ktor.features.ContentNegotiation 12 | import io.ktor.http.ContentType 13 | import io.ktor.jackson.JacksonConverter 14 | import io.ktor.routing.routing 15 | 16 | fun > testApplication( 17 | mapper: ObjectMapper, 18 | webContextProvider: WebContextProvider, 19 | aRouting: (Routing) -> RoutingDef 20 | ): Application.() -> Unit = { 21 | install(ContentNegotiation) { 22 | register(ContentType.Application.Json, JacksonConverter(mapper)) 23 | } 24 | val initialRouting = InitialRouting() 25 | routing { 26 | aRouting(initialRouting).buildRoute(this, webContextProvider) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /nee-test/nee-ctx-web-test/src/main/kotlin/dev/neeffect/nee/web/test/TestWebContextProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.web.test 2 | 3 | import dev.neeffect.nee.ctx.web.JDBCBasedWebContextProvider 4 | import dev.neeffect.nee.effects.jdbc.JDBCConfig 5 | import dev.neeffect.nee.effects.jdbc.JDBCProvider 6 | import io.ktor.application.Application 7 | import io.ktor.server.engine.ApplicationEngineEnvironment 8 | import io.ktor.server.testing.TestApplicationCall 9 | import io.ktor.server.testing.TestApplicationRequest 10 | import io.ktor.server.testing.createTestEnvironment 11 | import kotlin.coroutines.EmptyCoroutineContext 12 | 13 | open class TestWebContextProvider : JDBCBasedWebContextProvider() { 14 | 15 | open val testEnv: ApplicationEngineEnvironment by lazy { createTestEnvironment() } 16 | open val testApplication by lazy { Application(testEnv) } 17 | open val testCallConstrucor 18 | by lazy { { TestApplicationCall(testApplication, false, true, EmptyCoroutineContext) } } 19 | 20 | val jdbcConfig: JDBCConfig by lazy { 21 | JDBCConfig( 22 | driverClassName = "org.h2.Driver", 23 | url = "jdbc:h2:mem:test", 24 | user = "sa", 25 | password = "" 26 | ) 27 | } 28 | override val jdbcProvider: JDBCProvider by lazy { 29 | JDBCProvider(jdbcConfig) 30 | } 31 | 32 | fun testCtx(reqConfig: (TestApplicationRequest) -> Unit = {}) = testCallConstrucor().let { appCall -> 33 | reqConfig(appCall.request) 34 | super.create(appCall) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nee-test/nee-security-jdbc-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.config.KotlinCompilerVersion 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.jvm") 5 | } 6 | 7 | dependencies { 8 | api(project(":nee-jdbc")) 9 | api(project(":nee-security-jdbc")) 10 | implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION)) 11 | implementation(Libs.Kotlin.kotlinStdLib) 12 | implementation(Libs.Vavr.kotlin) { 13 | exclude("org.jetbrains.kotlin") 14 | } 15 | implementation(Libs.Liquibase.core) 16 | runtime(Libs.H2.h2) 17 | } 18 | 19 | apply(from = "../../publish-mpp.gradle.kts") 20 | -------------------------------------------------------------------------------- /nee-test/nee-security-jdbc-test/src/main/kotlin/dev/neeffect/nee/security/test/TestDB.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.security.test 2 | 3 | import dev.neeffect.nee.effects.jdbc.JDBCConfig 4 | import dev.neeffect.nee.security.PBKDF2Hasher 5 | import dev.neeffect.nee.security.Salt 6 | import dev.neeffect.nee.security.User 7 | import dev.neeffect.nee.security.UserRole 8 | import dev.neeffect.nee.security.toBytes 9 | import io.vavr.collection.List 10 | import liquibase.Liquibase 11 | import liquibase.database.DatabaseFactory 12 | import liquibase.database.jvm.JdbcConnection 13 | import liquibase.resource.ClassLoaderResourceAccessor 14 | import java.sql.Connection 15 | import java.sql.DriverManager 16 | import java.util.Random 17 | import java.util.UUID 18 | 19 | /**s 20 | * Small utility for setting sql db for tests. 21 | */ 22 | class TestDB(val jdbcConfig: JDBCConfig = h2InMemDatabase) { 23 | 24 | fun initializeDb() = 25 | createDbConnection().let { dbConnection -> 26 | val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(JdbcConnection(dbConnection)) 27 | val liquibaseChangeLog = Liquibase("db/db.xml", ClassLoaderResourceAccessor(), database) 28 | liquibaseChangeLog.update(liquibase.Contexts(), liquibase.LabelExpression()) 29 | TestDBConnection(dbConnection, jdbcConfig) 30 | } 31 | 32 | fun connection() = 33 | TestDBConnection(createDbConnection(), jdbcConfig) 34 | 35 | private fun createDbConnection() = DriverManager.getConnection( 36 | jdbcConfig.url, 37 | jdbcConfig.user, 38 | jdbcConfig.password 39 | ) 40 | } 41 | 42 | class TestDBConnection(val connection: Connection, val jdbcConfig: JDBCConfig) : AutoCloseable { 43 | 44 | private val hasher = PBKDF2Hasher() 45 | private val randomGeneratorForUUID = Random(42) 46 | private val testSalt = UUID.fromString("699add98-2aa2-49ad-8d09-d35f2a36f36b").toBytes() 47 | 48 | fun addUser(login: String, password: String, roles: List) = 49 | inTransaction(connection) { cn -> 50 | val uuid = UUID(randomGeneratorForUUID.nextLong(), randomGeneratorForUUID.nextLong()) 51 | val newUser = User( 52 | uuid, 53 | login, 54 | roles.map { UserRole(it) } 55 | ) 56 | insertUser(newUser, testSalt, password, cn) 57 | newUser 58 | } 59 | 60 | private fun insertUser(user: User, salt: Salt, initialPassword: String, cn: Connection) = 61 | user.run { 62 | cn.prepareStatement( 63 | "insert into users (id, salt, password, login)" + 64 | "values (?,?,?,?)" 65 | ).use { stmt -> 66 | stmt.setBytes(1, id.toBytes()) 67 | stmt.setBytes(2, salt) 68 | stmt.setBytes( 69 | 3, hasher.hashPassword( 70 | initialPassword.toCharArray(), salt 71 | ) 72 | ) 73 | stmt.setString(4, login) 74 | stmt.execute() 75 | }.also { 76 | insertRoles(user, cn) 77 | } 78 | } 79 | 80 | private fun insertRoles(user: User, cn: Connection) = 81 | user.run { 82 | cn.prepareStatement( 83 | "insert into user_roles" + 84 | " (user_id, role_name)" + 85 | "values (?, ?)" 86 | ).use { stmt -> 87 | roles.forEach { userRole -> 88 | stmt.setBytes(1, id.toBytes()) 89 | stmt.setString(2, userRole.roleName) 90 | stmt.execute() 91 | stmt.clearParameters() 92 | } 93 | } 94 | } 95 | 96 | override fun close() { 97 | this.connection.close() 98 | } 99 | } 100 | 101 | val h2InMemDatabase = JDBCConfig( 102 | driverClassName = "org.h2.Driver", 103 | url = "jdbc:h2:mem:test_mem", 104 | user = "sa", 105 | password = "" 106 | ) 107 | 108 | fun inTransaction(connection: Connection, f: (Connection) -> R) { 109 | val initialACState = connection.autoCommit 110 | connection.autoCommit = false 111 | try { 112 | f(connection) 113 | connection.commit() 114 | } catch (e: Exception) { 115 | connection.rollback() 116 | throw e 117 | } finally { 118 | connection.autoCommit = initialACState 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /nee-test/nee-security-jdbc-test/src/main/resources/db/db.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /publish-mpp.gradle.kts: -------------------------------------------------------------------------------- 1 | apply(plugin = "java") 2 | apply(plugin = "java-library") 3 | apply(plugin = "maven-publish") 4 | apply(plugin = "signing") 5 | apply(plugin = "org.jetbrains.dokka") 6 | apply(plugin = "com.bmuschko.nexus") 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | val ossrhUsername: String by project 12 | val ossrhPassword: String by project 13 | val signingKey: String? by project 14 | val signingPassword: String? by project 15 | 16 | fun Project.publishing(action: PublishingExtension.() -> Unit) = 17 | configure(action) 18 | 19 | fun Project.signing(configure: SigningExtension.() -> Unit): Unit = 20 | configure(configure) 21 | 22 | val dokka = tasks.named("dokka") 23 | val javadoc = tasks.named("javadoc") 24 | //val sources = tasks.named("kotlinSourcesJar") 25 | 26 | val publications: PublicationContainer = (extensions.getByName("publishing") as PublishingExtension).publications 27 | 28 | signing { 29 | setRequired({ 30 | (Ci.isRelease) && gradle.taskGraph.hasTask("publish") 31 | }) 32 | 33 | if (signingKey != null && signingPassword != null) { 34 | @Suppress("UnstableApiUsage") 35 | useInMemoryPgpKeys(signingKey, signingPassword) 36 | } 37 | if (Ci.isRelease) { 38 | useGpgCmd() 39 | sign(publications) 40 | } 41 | } 42 | 43 | val javadocJar2 by tasks.creating(Jar::class) { 44 | group = JavaBasePlugin.DOCUMENTATION_GROUP 45 | description = "Assembles java doc to jar" 46 | archiveClassifier.set("javadoc") 47 | from(javadoc) 48 | } 49 | val dokkaJar by tasks.creating(Jar::class) { 50 | group = JavaBasePlugin.DOCUMENTATION_GROUP 51 | description = "Assembles Kotlin docs with Dokka" 52 | archiveClassifier.set("javadoc") 53 | from(dokka) 54 | } 55 | //val sourcesJar by tasks.creating(Jar::class) { 56 | // group = JavaBasePlugin.DOCUMENTATION_GROUP 57 | // description = "Assembles sources" 58 | // archiveClassifier.set("sources") 59 | // from(the()["main"].allSource) 60 | //} 61 | 62 | 63 | publishing { 64 | repositories { 65 | maven { 66 | val releasesRepoUrl = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") 67 | val snapshotsRepoUrl = uri("https://oss.sonatype.org/content/repositories/snapshots/") 68 | name = "deploy" 69 | url = if (Ci.isRelease) releasesRepoUrl else snapshotsRepoUrl 70 | credentials { 71 | username = System.getenv("OSSRH_USERNAME") ?: ossrhUsername 72 | password = System.getenv("OSSRH_PASSWORD") ?: ossrhPassword 73 | } 74 | } 75 | } 76 | 77 | publications.withType().forEach { 78 | it.apply { 79 | if (Ci.isRelease) { 80 | artifact(tasks["dokkaJar"]) 81 | } 82 | artifact(tasks["kotlinSourcesJar"]) 83 | 84 | pom { 85 | name.set("Neefect") 86 | description.set("Nee") 87 | url.set("http://www.github.com/neeffect/nee/") 88 | 89 | scm { 90 | connection.set("scm:git:http://www.github.com/neeffect/nee/") 91 | developerConnection.set("scm:git:http://github.com/jarekratajski/") 92 | url.set("http://www.github.com/neeffect/nee/") 93 | } 94 | 95 | licenses { 96 | license { 97 | name.set("Apache-2.0") 98 | url.set("https://opensource.org/licenses/Apache-2.0") 99 | } 100 | } 101 | 102 | developers { 103 | developer { 104 | id.set("jarekratajski") 105 | name.set("Jarek Ratajski") 106 | email.set("jratajski@gmail.com") 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /scratchpad/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "org.jetbrains.kotlin.jvm" 3 | } 4 | dependencies { 5 | implementation project(":nee-core") 6 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6" 7 | implementation "io.vavr:vavr-kotlin:0.10.2" 8 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 9 | testImplementation "io.kotest:kotest-runner-junit5-jvm:4.1.1" 10 | } 11 | 12 | -------------------------------------------------------------------------------- /scratchpad/src/main/kotlin/dev/neeffect/nee/scratchpad/Coroutines.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.scratchpad 2 | 3 | import kotlinx.coroutines.Deferred 4 | import kotlinx.coroutines.GlobalScope 5 | import kotlinx.coroutines.async 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.runBlocking 8 | 9 | fun main() { 10 | val deferredResult: Deferred = GlobalScope.async { 11 | delay(1000L) 12 | "World!" 13 | } 14 | println("now I am here") 15 | runBlocking { 16 | println("Hello, ${deferredResult.await()}") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scratchpad/src/main/kotlin/dev/neeffect/nee/scratchpad/RProblem.kt: -------------------------------------------------------------------------------- 1 | package dev.neeffect.nee.scratchpad 2 | 3 | interface A1> { 4 | fun get(): T 5 | fun getVal(): String 6 | fun setVal(s: String): A1 = this.let { parent -> 7 | object : A1 { 8 | override fun get(): T = parent.get() 9 | override fun getVal(): String = s 10 | } 11 | } 12 | } 13 | 14 | interface A2> { 15 | fun get(): T 16 | fun getVal2(): String 17 | fun setVal2(s: String): A2 = this.let { parent -> 18 | object : A2 { 19 | override fun get(): T = parent.get() 20 | override fun getVal2(): String = s 21 | } 22 | } 23 | } 24 | 25 | 26 | fun main() { 27 | data class Test1(val x: String, val y: String) : A1, A2 { 28 | override fun get(): Test1 = this 29 | 30 | override fun getVal(): String = x 31 | 32 | override fun getVal2(): String = y 33 | 34 | } 35 | 36 | val b1 = Test1("hello", "world") 37 | println(b1.getVal()) 38 | println(b1.getVal2()) 39 | val b2 = b1.setVal("uuu").get() 40 | println(b2.getVal()) 41 | println(b2.getVal2()) 42 | val b3 = b2.setVal("aaa").get() 43 | println(b3.getVal()) 44 | println(b3.getVal2()) 45 | } 46 | -------------------------------------------------------------------------------- /scripts/publishLocal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . gojava11 3 | ./gradlew --stop 4 | ./gradlew clean build publishToMavenLocal 5 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "nee" 2 | 3 | include "nee-core", 4 | "nee-jdbc", 5 | "nee-security", 6 | "nee-security-jdbc", 7 | "nee-cache-caffeine", 8 | "nee-ctx-web-ktor", 9 | "nee-test", 10 | "nee-test:nee-core-test", 11 | "nee-test:nee-security-jdbc-test", 12 | "nee-test:nee-ctx-web-test", 13 | "scratchpad" 14 | --------------------------------------------------------------------------------