Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MODELIX-1064 Bulk sync based MPS plugin (sync-plugin 3) #1396

Merged
merged 37 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b6c3aaa
feat(mps-sync-plugin): re-implementation of the sync plugin for MPS
slisson Feb 5, 2025
6b48f5d
feat(mps-sync-plugin): support for MPS 2020.3
slisson Feb 18, 2025
c18ff22
feat(model-api): changed serialization format of references to modeli…
slisson Feb 19, 2025
8f4832b
build: use absolute path to docker executable
slisson Feb 19, 2025
f53d799
test(authorization): fixed flaky test RSATest.`key file changes are d…
slisson Feb 19, 2025
5143b51
feat(mps-sync-plugin): persist bindings to .mps/modelix.xml and resto…
slisson Feb 20, 2025
bdadaaf
fix(model-datastructure): deserialization failed after addNewChildren…
slisson Feb 20, 2025
2f6cdf8
chore(mps-sync-plugin): move classes to separate files
slisson Feb 20, 2025
b7bc5e4
feat(mps-sync-plugin): handle disabled bindings when loading from mod…
slisson Feb 20, 2025
9e6610e
feat(mps-sync-plugin): MPS 2020.3 support
slisson Feb 21, 2025
16209d3
fix(mps-sync-plugin): binding couldn't be disabled
slisson Feb 21, 2025
ee17b6b
feat(mps-sync-plugin): catch exceptions and continue synchronization
slisson Feb 22, 2025
c1eead7
fix(mps-sync-plugin): descendants of new nodes where not synchronized
slisson Feb 22, 2025
1eb2ad1
fix(mps-sync-plugin): model-synchronizer didn't call ISyncTargetNode.…
slisson Feb 23, 2025
63e6a29
feat(mps-sync-plugin): ignore exception during synchronization
slisson Feb 23, 2025
6509a81
fix(model-datastructure): AddNewChildrenOp.toString()
slisson Feb 23, 2025
8f4d148
feat(mps-sync-plugin): ignore exception during synchronization
slisson Feb 24, 2025
6487904
fix(mps-sync-plugin): descendants of new nodes where not synchronized
slisson Feb 24, 2025
298a71f
fix(mps-sync-plugin): synchronization of used devkits failed
slisson Feb 24, 2025
a0039a2
chore(mps-sync-plugin): duplicate logger
slisson Feb 24, 2025
e3a0303
fix(mps-sync-plugin): exceptions weren't logged (missing appender)
slisson Feb 24, 2025
e910315
build(mps-sync-plugin): fixed installMpsPlugin task
slisson Feb 24, 2025
327bfe8
test(mps-sync-plugin): some cleanup
slisson Feb 24, 2025
48899e2
test(mps-sync-plugin): variation with active binding
slisson Feb 24, 2025
4a7f317
feat(model-server): include oauth endpoints in WWW-Authenticate on 401
slisson Feb 26, 2025
66a17ad
fix(model-server): some versions were missing on the history page
slisson Feb 26, 2025
73cc5c5
fix(model-server): don't require login for the /headers endpoint
slisson Feb 26, 2025
8a07819
fix(mps-sync-plugin): handle exceptions during initial sync
slisson Feb 26, 2025
c4c6787
fix(model-client): OAuth login
slisson Feb 26, 2025
2dbdbbc
fix(model-client): expired access token wasn't refreshed
slisson Feb 26, 2025
cd457fd
fix(model-client): only use OAuth if explicitly enabled
slisson Feb 27, 2025
a252e6b
test(model-client): increase timeout for starting containers
slisson Feb 27, 2025
e5188ab
fix(model-client): OAuth login
slisson Feb 27, 2025
15cfe7f
test(model-server): use keycloak version from gradle dependencies
slisson Feb 27, 2025
9530a0c
test(model-server): increase timeout in AuthorizationTest
slisson Feb 27, 2025
a8b6fcc
fix(model-server): use output of gradle application plugin to run the…
slisson Feb 28, 2025
1187b88
fix(mps-sync-plugin): removed last usages of originalId in ModelSynch…
slisson Feb 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/mps-compatibility.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
timeout-minutes: 60

strategy:
fail-fast: false
matrix:
version:
- "2020.3"
Expand Down Expand Up @@ -47,4 +48,13 @@ jobs:
:metamodel-export:build
:mps-model-adapters:build
:mps-model-adapters-plugin:build
:mps-sync-plugin3:build
-Pmps.version.major=${{ matrix.version }}
- name: Archive test report
uses: actions/upload-artifact@v4
if: always()
with:
name: test-report-${{ matrix.version }}
path: |
*/build/test-results
*/build/reports
1 change: 1 addition & 0 deletions authorization/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ java {
}

dependencies {
implementation(project(":kotlin-utils"))
implementation(libs.kotlin.serialization.json)
implementation(libs.kotlin.serialization.yaml)
implementation(libs.guava)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package org.modelix.authorization

import com.auth0.jwt.interfaces.DecodedJWT
import io.ktor.server.auth.Principal
import com.auth0.jwt.interfaces.Payload

class AccessTokenPrincipal(val jwt: DecodedJWT) : Principal {
class AccessTokenPrincipal(val jwt: Payload) {

Check warning

Code scanning / detekt

AccessTokenPrincipal is missing required documentation. Warning

AccessTokenPrincipal is missing required documentation.

Check warning

Code scanning / detekt

The property jwt is missing documentation. Warning

The property jwt is missing documentation.
fun getUserName(): String? = ModelixJWTUtil.extractUserId(jwt)

override fun equals(other: Any?): Boolean {
if (other !is AccessTokenPrincipal) return false
return other.jwt.token.equals(jwt.token)
return other.jwt.claims == jwt.claims
}

override fun hashCode(): Int {
return jwt.token.hashCode()
return jwt.claims.hashCode()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ class ModelixAuthorizationConfig : IModelixAuthorizationConfig {
*
* The fake token is generated so that we always have a username that can be used in the server logic.
*/
fun shouldGenerateFakeTokens() = generateFakeTokens ?: !jwtUtil.canVerifyTokens()
fun shouldGenerateFakeTokens() = generateFakeTokens ?: !permissionCheckingEnabled()

/**
* Whether permission checking should be enabled based on the configuration values provided.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@
import com.auth0.jwt.interfaces.JWTVerifier
import com.google.common.cache.CacheBuilder
import com.nimbusds.jose.jwk.JWKSet
import com.nimbusds.jwt.proc.BadJWTException
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.auth.HttpAuthHeader
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.ApplicationCallPipeline
import io.ktor.server.application.BaseRouteScopedPlugin
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.application.plugin
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.AuthenticationContext
import io.ktor.server.auth.AuthenticationProvider
import io.ktor.server.auth.UnauthorizedResponse
import io.ktor.server.auth.authenticate
import io.ktor.server.auth.jwt.jwt
import io.ktor.server.auth.parseAuthorizationHeader
import io.ktor.server.auth.principal
import io.ktor.server.plugins.forwardedheaders.XForwardedHeaders
import io.ktor.server.plugins.statuspages.StatusPages
Expand All @@ -40,6 +43,7 @@
import org.modelix.authorization.permissions.SchemaInstance
import org.modelix.authorization.permissions.recordKnownRoles
import org.modelix.authorization.permissions.recordKnownUser
import org.modelix.kotlin.utils.filterNotNullValues
import java.util.concurrent.TimeUnit

private val LOG = mu.KotlinLogging.logger { }
Expand Down Expand Up @@ -79,29 +83,41 @@
} else {
// "Authorization: Bearer ..." header is provided in the header by OAuth proxy
jwt(MODELIX_JWT_AUTH) {
realm = "modelix"
authHeader { call ->
call.request.parseAuthorizationHeader()
?: call.request.headers["X-Forwarded-Access-Token"]
?.let { HttpAuthHeader.Single("Bearer", it) }
}

verifier(config.getVerifier())
challenge { _, _ ->
call.respond(status = HttpStatusCode.Unauthorized, "No or invalid JWT token provided")
challenge { scheme, realm ->
call.respond(
UnauthorizedResponse(
HttpAuthHeader.Parameterized(
scheme,
mapOf(
HttpAuthHeader.Parameters.Realm to realm,
"error" to "invalid_token",
"authorization_uri" to System.getenv("MODELIX_AUTHORIZATION_URI")?.takeIf { it.isNotBlank() },
"token_uri" to System.getenv("MODELIX_TOKEN_URI")?.takeIf { it.isNotBlank() },
).filterNotNullValues(),
),
),
)

// login and token generation is done by OAuth proxy. Only validation is required here.
}
validate {
try {
validate { credential ->
val jwt = credential.payload
application.launch(Dispatchers.IO) {

Check warning

Code scanning / detekt

Dispatcher IO is used without dependency injection. Warning

Dispatcher IO is used without dependency injection.
val authPlugin = application.plugin(ModelixAuthorization)
val authConfig = authPlugin.config
jwtFromHeaders()
?.let { authConfig.nullIfInvalid(it) }
?.also { jwt ->
application.launch(Dispatchers.IO) {
val accessControlPersistence = authConfig.accessControlPersistence
accessControlPersistence.recordKnownUser(authConfig.jwtUtil.extractUserId(jwt))
accessControlPersistence.recordKnownRoles(authConfig.jwtUtil.extractUserRoles(jwt))
}
}
?.let(::AccessTokenPrincipal)
} catch (e: Exception) {
LOG.warn(e) { "Failed to read JWT token" }
null
val accessControlPersistence = authConfig.accessControlPersistence
accessControlPersistence.recordKnownUser(authConfig.jwtUtil.extractUserId(jwt))
accessControlPersistence.recordKnownRoles(authConfig.jwtUtil.extractUserRoles(jwt))
}
AccessTokenPrincipal(jwt)
}
}
}
Expand Down Expand Up @@ -135,7 +151,7 @@
(installedIntoRoute ?: this).apply {
if (config.debugEndpointsEnabled) {
get("/user") {
val jwt = call.principal<AccessTokenPrincipal>()?.jwt ?: call.jwtFromHeaders()
val jwt = call.jwtFromHeaders()
if (jwt == null) {
call.respondText("No JWT token available")
} else {
Expand Down Expand Up @@ -226,9 +242,13 @@
}

override fun verify(jwt: DecodedJWT?): DecodedJWT {
if (jwt == null) {
throw JWTVerificationException("No JWT provided.")
try {
if (jwt == null) {
throw JWTVerificationException("No JWT provided.")
}
return [email protected](jwt)?.also { println("Valid token: ${jwt.token}") } ?: throw JWTVerificationException("JWT invalid.")
} catch (ex: BadJWTException) {
throw JWTVerificationException("Invalid token: ${jwt?.token}", ex)
}
return [email protected](jwt) ?: throw JWTVerificationException("JWT invalid.")
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.modelix.authorization

import com.auth0.jwt.interfaces.DecodedJWT
import com.auth0.jwt.interfaces.Payload
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.JWSHeader
import com.nimbusds.jose.JWSObject
Expand Down Expand Up @@ -244,12 +245,12 @@
return PermissionEvaluator(schema).also { loadGrantedPermissions(token, it) }
}

fun extractPermissions(token: DecodedJWT): List<String>? {
fun extractPermissions(token: Payload): List<String>? {

Check warning

Code scanning / detekt

The function extractPermissions is missing documentation. Warning

The function extractPermissions is missing documentation.
return token.claims[ModelixTokenConstants.PERMISSIONS]?.asList(String::class.java)
}

@Synchronized
fun loadGrantedPermissions(token: DecodedJWT, evaluator: PermissionEvaluator) {
fun loadGrantedPermissions(token: Payload, evaluator: PermissionEvaluator) {

Check warning

Code scanning / detekt

The function loadGrantedPermissions is missing documentation. Warning

The function loadGrantedPermissions is missing documentation.
val permissions = extractPermissions(token)

// There is a difference between access tokens and identity tokens.
Expand All @@ -271,11 +272,11 @@
}
}

fun extractUserId(jwt: DecodedJWT): String? {
fun extractUserId(jwt: Payload): String? {

Check warning

Code scanning / detekt

The function extractUserId is missing documentation. Warning

The function extractUserId is missing documentation.
return Companion.extractUserId(jwt)
}

fun extractUserRoles(jwt: DecodedJWT): List<String> {
fun extractUserRoles(jwt: Payload): List<String> {

Check warning

Code scanning / detekt

The function extractUserRoles is missing documentation. Warning

The function extractUserRoles is missing documentation.
val keycloakRoles = jwt
.getClaim(KeycloakTokenConstants.REALM_ACCESS)?.asMap()
?.get(KeycloakTokenConstants.REALM_ACCESS_ROLES)
Expand Down Expand Up @@ -372,7 +373,7 @@
}

companion object {
fun extractUserId(jwt: DecodedJWT): String? {
fun extractUserId(jwt: Payload): String? {

Check warning

Code scanning / detekt

The function extractUserId is missing documentation. Warning

The function extractUserId is missing documentation.
return jwt.getClaim(KeycloakTokenConstants.EMAIL)?.asString()
?: jwt.getClaim(KeycloakTokenConstants.PREFERRED_USERNAME)?.asString()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ class RSATest {
privateKeyFile.writeText(privateKeyPem1)

val verifyingUtil = ModelixJWTUtil()
verifyingUtil.fileRefreshTime = 50.milliseconds
verifyingUtil.fileRefreshTime = 500.milliseconds
verifyingUtil.loadKeysFromFiles(publicKeyFile)
run {
val signingUtil = ModelixJWTUtil()
Expand All @@ -314,7 +314,7 @@ class RSATest {
assertFailsWith<BadJOSEException> {
verifyingUtil.verifyToken(tokenString)
}
Thread.sleep(50)
Thread.sleep(500)
verifyingUtil.verifyToken(tokenString)
}
}
Expand Down
Loading
Loading