diff --git a/airbyte-commons-auth/src/main/kotlin/io/airbyte/commons/auth/config/AuthConfigs.kt b/airbyte-commons-auth/src/main/kotlin/io/airbyte/commons/auth/config/AuthConfigs.kt index 8905de32bc8..4edf5c920bb 100644 --- a/airbyte-commons-auth/src/main/kotlin/io/airbyte/commons/auth/config/AuthConfigs.kt +++ b/airbyte-commons-auth/src/main/kotlin/io/airbyte/commons/auth/config/AuthConfigs.kt @@ -36,29 +36,26 @@ class AuthModeFactory( val airbyteEdition: Configs.AirbyteEdition, ) { /** - * When the Micronaut environment is set to `community-auth`, the `SIMPLE` auth mode is used - * regardless of the deployment mode or other configurations. This bean replaces the - * [defaultAuthMode] when the `community-auth` environment is active. + * When the Airbyte edition is set to `community` and Micronaut Security is enabled, the + * `SIMPLE` auth mode is used regardless of the deployment mode or other configurations. */ @Singleton - @Requires(env = ["community-auth"]) + @Requires(property = "airbyte.edition", value = "community") + @Requires(property = "micronaut.security.enabled", value = "true") @Primary - fun communityAuthMode(): AuthMode { - return AuthMode.SIMPLE - } + fun communityAuthMode(): AuthMode = AuthMode.SIMPLE /** * The default auth mode is determined by the deployment mode and edition. */ @Singleton - fun defaultAuthMode(): AuthMode { - return when { + fun defaultAuthMode(): AuthMode = + when { deploymentMode == DeploymentMode.CLOUD -> AuthMode.OIDC airbyteEdition == Configs.AirbyteEdition.PRO -> AuthMode.OIDC deploymentMode == DeploymentMode.OSS -> AuthMode.NONE else -> throw IllegalStateException("Unknown or unspecified deployment mode: $deploymentMode") } - } } /** @@ -75,7 +72,5 @@ class AuthConfigFactory( val initialUserConfig: InitialUserConfig? = null, ) { @Singleton - fun authConfig(): AuthConfigs { - return AuthConfigs(authMode, keycloakConfig, oidcConfig, initialUserConfig) - } + fun authConfig(): AuthConfigs = AuthConfigs(authMode, keycloakConfig, oidcConfig, initialUserConfig) } diff --git a/airbyte-commons-auth/src/test/kotlin/io/airbyte/commons/auth/config/AuthConfigsTest.kt b/airbyte-commons-auth/src/test/kotlin/io/airbyte/commons/auth/config/AuthConfigsTest.kt index 9f2a1e6bb0b..2f3eb18dc6d 100644 --- a/airbyte-commons-auth/src/test/kotlin/io/airbyte/commons/auth/config/AuthConfigsTest.kt +++ b/airbyte-commons-auth/src/test/kotlin/io/airbyte/commons/auth/config/AuthConfigsTest.kt @@ -86,13 +86,15 @@ class AuthConfigsForCloudTest { } } -@MicronautTest(environments = ["community-auth"]) +@MicronautTest class AuthConfigsForCommunityAuthTest { @Inject lateinit var authConfigs: AuthConfigs @Test @Property(name = "airbyte.deployment-mode", value = "OSS") + @Property(name = "airbyte.edition", value = "community") + @Property(name = "micronaut.security.enabled", value = "true") fun `test community-auth environment sets mode to SIMPLE`() { Assertions.assertTrue(authConfigs.authMode == AuthMode.SIMPLE) } diff --git a/airbyte-commons-micronaut/build.gradle.kts b/airbyte-commons-micronaut/build.gradle.kts index 10cf70aafc0..fa43b193524 100644 --- a/airbyte-commons-micronaut/build.gradle.kts +++ b/airbyte-commons-micronaut/build.gradle.kts @@ -5,7 +5,7 @@ plugins { dependencies { compileOnly(libs.lombok) - annotationProcessor(libs.lombok) // Lombok must be added BEFORE Micronaut + annotationProcessor(libs.lombok) // Lombok must be added BEFORE Micronaut annotationProcessor(platform(libs.micronaut.platform)) annotationProcessor(libs.bundles.micronaut.annotation.processor) @@ -25,6 +25,7 @@ dependencies { testImplementation(libs.bundles.micronaut.test) testImplementation(libs.mockito.inline) + testImplementation(libs.mockk) } tasks.named("test") { diff --git a/airbyte-commons-micronaut/src/main/kotlin/io/airbyte/micronaut/config/EditionPropertySourceLoader.kt b/airbyte-commons-micronaut/src/main/kotlin/io/airbyte/micronaut/config/EditionPropertySourceLoader.kt new file mode 100644 index 00000000000..6bb15fb33d7 --- /dev/null +++ b/airbyte-commons-micronaut/src/main/kotlin/io/airbyte/micronaut/config/EditionPropertySourceLoader.kt @@ -0,0 +1,36 @@ +package io.airbyte.micronaut.config + +import io.micronaut.context.env.AbstractPropertySourceLoader +import io.micronaut.context.env.PropertySource +import io.micronaut.context.env.yaml.YamlPropertySourceLoader +import io.micronaut.core.io.ResourceLoader +import java.util.Optional + +/** + * Loads properties based on the edition of Airbyte. + * Looks for a file named `application-edition-.yml` where `` is the value of the + * `AIRBYTE_EDITION` environment variable. + * This loader is registered in META-INF/services/io.micronaut.context.env.PropertySourceLoader + * so that it is automatically picked up by Micronaut. + */ +class EditionPropertySourceLoader( + private val airbyteEdition: String? = System.getenv(AIRBYTE_EDITION_ENV_VAR), +) : YamlPropertySourceLoader() { + companion object { + const val AIRBYTE_EDITION_ENV_VAR = "AIRBYTE_EDITION" + } + + override fun getOrder(): Int = AbstractPropertySourceLoader.DEFAULT_POSITION + 10 + + override fun load( + name: String?, + resourceLoader: ResourceLoader?, + ): Optional { + if (airbyteEdition.isNullOrEmpty()) { + return Optional.empty() + } + + val fileName = "application-edition-" + airbyteEdition.lowercase() + return super.load(fileName, resourceLoader) + } +} diff --git a/airbyte-commons-micronaut/src/main/resources/META-INF/services/io.micronaut.context.env.PropertySourceLoader b/airbyte-commons-micronaut/src/main/resources/META-INF/services/io.micronaut.context.env.PropertySourceLoader index 247e11541b5..f84dec9274e 100644 --- a/airbyte-commons-micronaut/src/main/resources/META-INF/services/io.micronaut.context.env.PropertySourceLoader +++ b/airbyte-commons-micronaut/src/main/resources/META-INF/services/io.micronaut.context.env.PropertySourceLoader @@ -1 +1,2 @@ -io.airbyte.micronaut.config.AirbytePropertySourceLoader \ No newline at end of file +io.airbyte.micronaut.config.AirbytePropertySourceLoader +io.airbyte.micronaut.config.EditionPropertySourceLoader \ No newline at end of file diff --git a/airbyte-commons-micronaut/src/test/kotlin/io/airbyte/micronaut/config/EditionPropertySourceLoaderTest.kt b/airbyte-commons-micronaut/src/test/kotlin/io/airbyte/micronaut/config/EditionPropertySourceLoaderTest.kt new file mode 100644 index 00000000000..69b9b6ec658 --- /dev/null +++ b/airbyte-commons-micronaut/src/test/kotlin/io/airbyte/micronaut/config/EditionPropertySourceLoaderTest.kt @@ -0,0 +1,43 @@ +package io.airbyte.micronaut.config + +import io.micronaut.core.io.ResourceLoader +import io.mockk.Called +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class EditionPropertySourceLoaderTest { + private val resourceLoader = mockk(relaxed = true) + + @Test + fun `should return empty optional when AIRBYTE_EDITION is not set`() { + val loader = EditionPropertySourceLoader(null) + + val result = loader.load("test", resourceLoader) + + assertTrue(result.isEmpty) + verify { resourceLoader wasNot Called } + } + + @Test + fun `should return empty optional when AIRBYTE_EDITION is empty`() { + val loader = EditionPropertySourceLoader("") + + val result = loader.load("test", resourceLoader) + + assertTrue(result.isEmpty) + verify { resourceLoader wasNot Called } + } + + @Test + fun `should attempt to load correct file when AIRBYTE_EDITION is set`() { + val loader = EditionPropertySourceLoader("community") + + loader.load("test", resourceLoader) + + // ensures underlying loader is called with correct file name and extensions + verify { resourceLoader.getResourceAsStream("application-edition-community.yml") } + verify { resourceLoader.getResourceAsStream("application-edition-community.yaml") } + } +} diff --git a/airbyte-commons-server/build.gradle.kts b/airbyte-commons-server/build.gradle.kts index 1bb3878e155..38e0e9319b6 100644 --- a/airbyte-commons-server/build.gradle.kts +++ b/airbyte-commons-server/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(libs.micronaut.inject) implementation(libs.micronaut.jaxrs.server) implementation(libs.micronaut.security) + implementation(libs.micronaut.security.jwt) implementation(libs.bundles.micronaut.data.jdbc) implementation(libs.bundles.micronaut.kotlin) implementation(libs.bundles.flyway) diff --git a/airbyte-server/build.gradle.kts b/airbyte-server/build.gradle.kts index c5d6fd5a89e..53c9bee8c3c 100644 --- a/airbyte-server/build.gradle.kts +++ b/airbyte-server/build.gradle.kts @@ -9,7 +9,7 @@ plugins { dependencies { compileOnly(libs.lombok) - annotationProcessor(libs.lombok) // Lombok must be added BEFORE Micronaut + annotationProcessor(libs.lombok) // Lombok must be added BEFORE Micronaut annotationProcessor(platform(libs.micronaut.platform)) annotationProcessor(libs.bundles.micronaut.annotation.processor) annotationProcessor(libs.micronaut.jaxrs.processor) @@ -26,6 +26,7 @@ dependencies { implementation(libs.micronaut.jaxrs.server) implementation(libs.micronaut.http) implementation(libs.micronaut.security) + implementation(libs.micronaut.security.jwt) implementation(libs.bundles.flyway) implementation(libs.s3) implementation(libs.sts) @@ -78,7 +79,7 @@ dependencies { runtimeOnly(libs.h2.database) testCompileOnly(libs.lombok) - testAnnotationProcessor(libs.lombok) // Lombok must be added BEFORE Micronaut + testAnnotationProcessor(libs.lombok) // Lombok must be added BEFORE Micronaut testAnnotationProcessor(platform(libs.micronaut.platform)) testAnnotationProcessor(libs.bundles.micronaut.annotation.processor) testAnnotationProcessor(libs.micronaut.jaxrs.processor) @@ -116,9 +117,10 @@ tasks.named("assemble") { dependsOn(copySeed) } -val env = Properties().apply { - load(rootProject.file(".env.dev").inputStream()) -} +val env = + Properties().apply { + load(rootProject.file(".env.dev").inputStream()) + } airbyte { application { @@ -143,7 +145,7 @@ airbyte { "TRACKING_STRATEGY" to env["TRACKING_STRATEGY"].toString(), "TEMPORAL_HOST" to "localhost:7233", "MICRONAUT_ENVIRONMENTS" to "control-plane", - ) + ), ) } @@ -157,11 +159,14 @@ airbyte { } spotbugs { - excludes = listOf(" \n" + - " \n" + - " \n" + - " \n" + - " ") + excludes = + listOf( + " \n" + + " \n" + + " \n" + + " \n" + + " ", + ) } } @@ -171,7 +176,7 @@ tasks.named("test") { "AIRBYTE_VERSION" to env["VERSION"], "MICRONAUT_ENVIRONMENTS" to "test", "SERVICE_NAME" to project.name, - ) + ), ) } diff --git a/airbyte-server/src/main/java/io/airbyte/server/pro/AirbyteAuthInternalTokenReader.java b/airbyte-server/src/main/java/io/airbyte/server/pro/AirbyteAuthInternalTokenReader.java index f43f7342e8c..9dbca1bd50e 100644 --- a/airbyte-server/src/main/java/io/airbyte/server/pro/AirbyteAuthInternalTokenReader.java +++ b/airbyte-server/src/main/java/io/airbyte/server/pro/AirbyteAuthInternalTokenReader.java @@ -5,7 +5,7 @@ package io.airbyte.server.pro; import io.airbyte.commons.auth.AirbyteAuthConstants; -import io.airbyte.commons.license.annotation.RequiresAirbyteProEnabled; +import io.micronaut.context.annotation.Requires; import io.micronaut.security.token.reader.HttpHeaderTokenReader; import jakarta.inject.Singleton; @@ -17,7 +17,10 @@ * will only be present on internal requests. */ @Singleton -@RequiresAirbyteProEnabled +@Requires(property = "micronaut.security.enabled", + value = "true") +@Requires(property = "airbyte.deployment-mode", + value = "OSS") public class AirbyteAuthInternalTokenReader extends HttpHeaderTokenReader { // This is set higher than other token readers so that it is checked last. diff --git a/airbyte-server/src/main/java/io/airbyte/server/pro/AirbyteAuthInternalTokenValidator.java b/airbyte-server/src/main/java/io/airbyte/server/pro/AirbyteAuthInternalTokenValidator.java index ee4b7016275..c2f92bf2af9 100644 --- a/airbyte-server/src/main/java/io/airbyte/server/pro/AirbyteAuthInternalTokenValidator.java +++ b/airbyte-server/src/main/java/io/airbyte/server/pro/AirbyteAuthInternalTokenValidator.java @@ -7,8 +7,8 @@ import static io.airbyte.config.persistence.UserPersistence.DEFAULT_USER_ID; import io.airbyte.commons.auth.AirbyteAuthConstants; -import io.airbyte.commons.license.annotation.RequiresAirbyteProEnabled; import io.airbyte.commons.server.support.RbacRoleHelper; +import io.micronaut.context.annotation.Requires; import io.micronaut.http.HttpRequest; import io.micronaut.security.authentication.Authentication; import io.micronaut.security.token.validator.TokenValidator; @@ -26,7 +26,10 @@ **/ @Slf4j @Singleton -@RequiresAirbyteProEnabled +@Requires(property = "micronaut.security.enabled", + value = "true") +@Requires(property = "airbyte.deployment-mode", + value = "OSS") public class AirbyteAuthInternalTokenValidator implements TokenValidator> { @Override diff --git a/airbyte-server/src/main/kotlin/io/airbyte/server/config/community/auth/CommunityAuthLogoutEventListener.kt b/airbyte-server/src/main/kotlin/io/airbyte/server/config/community/auth/CommunityAuthLogoutEventListener.kt new file mode 100644 index 00000000000..e6014c68812 --- /dev/null +++ b/airbyte-server/src/main/kotlin/io/airbyte/server/config/community/auth/CommunityAuthLogoutEventListener.kt @@ -0,0 +1,25 @@ +package io.airbyte.server.config.community.auth + +import io.airbyte.api.problems.model.generated.ProblemMessageData +import io.airbyte.api.problems.throwable.generated.UnprocessableEntityProblem +import io.airbyte.data.services.AuthRefreshTokenService +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.security.authentication.Authentication +import io.micronaut.security.event.LogoutEvent +import jakarta.inject.Singleton + +@Singleton +class CommunityAuthLogoutEventListener( + private val refreshTokenService: AuthRefreshTokenService, +) : ApplicationEventListener { + override fun onApplicationEvent(event: LogoutEvent) { + val sessionId = + (event.source as? Authentication) + ?.attributes + ?.get(SESSION_ID) + ?.toString() + ?: throw UnprocessableEntityProblem(ProblemMessageData().message("Could not retrieve session ID from authentication context")) + + refreshTokenService.revokeAuthRefreshToken(sessionId) + } +} diff --git a/airbyte-server/src/main/kotlin/io/airbyte/server/SimpleAuthProvider.kt b/airbyte-server/src/main/kotlin/io/airbyte/server/config/community/auth/CommunityAuthProvider.kt similarity index 66% rename from airbyte-server/src/main/kotlin/io/airbyte/server/SimpleAuthProvider.kt rename to airbyte-server/src/main/kotlin/io/airbyte/server/config/community/auth/CommunityAuthProvider.kt index 8b279771eda..f913e0923d0 100644 --- a/airbyte-server/src/main/kotlin/io/airbyte/server/SimpleAuthProvider.kt +++ b/airbyte-server/src/main/kotlin/io/airbyte/server/config/community/auth/CommunityAuthProvider.kt @@ -1,7 +1,7 @@ /* * Copyright (c) 2020-2024 Airbyte, Inc., all rights reserved. */ -package io.airbyte.server +package io.airbyte.server.config.community.auth import io.airbyte.commons.auth.RequiresAuthMode import io.airbyte.commons.auth.config.AuthMode @@ -13,22 +13,31 @@ import io.micronaut.security.authentication.AuthenticationRequest import io.micronaut.security.authentication.AuthenticationResponse import io.micronaut.security.authentication.provider.HttpRequestAuthenticationProvider import jakarta.inject.Singleton +import java.util.UUID + +const val SESSION_ID = "sessionId" /** * This class is responsible for authenticating the user against the community provider. */ @Singleton @RequiresAuthMode(AuthMode.SIMPLE) -class SimpleAuthProvider(private val instanceAdminConfig: InstanceAdminConfig) : HttpRequestAuthenticationProvider { +class CommunityAuthProvider( + private val instanceAdminConfig: InstanceAdminConfig, +) : HttpRequestAuthenticationProvider { override fun authenticate( requestContext: HttpRequest?, authRequest: AuthenticationRequest, ): AuthenticationResponse? { if (authRequest.identity == instanceAdminConfig.username && authRequest.secret == instanceAdminConfig.password) { - return AuthenticationResponse.success( - UserPersistence.DEFAULT_USER_ID.toString(), - RbacRoleHelper.getInstanceAdminRoles(), - ) + val sessionId = UUID.randomUUID() + val authenticationResponse = + AuthenticationResponse.success( + UserPersistence.DEFAULT_USER_ID.toString(), + RbacRoleHelper.getInstanceAdminRoles(), + mapOf(SESSION_ID to sessionId.toString()), + ) + return authenticationResponse } return AuthenticationResponse.failure("Invalid credentials") } diff --git a/airbyte-server/src/main/kotlin/io/airbyte/server/config/community/auth/CommunityAuthRefreshTokenPersistence.kt b/airbyte-server/src/main/kotlin/io/airbyte/server/config/community/auth/CommunityAuthRefreshTokenPersistence.kt new file mode 100644 index 00000000000..9779ed30dd9 --- /dev/null +++ b/airbyte-server/src/main/kotlin/io/airbyte/server/config/community/auth/CommunityAuthRefreshTokenPersistence.kt @@ -0,0 +1,60 @@ +package io.airbyte.server.config.community.auth + +import io.airbyte.api.problems.model.generated.ProblemMessageData +import io.airbyte.api.problems.throwable.generated.ForbiddenProblem +import io.airbyte.api.problems.throwable.generated.UnprocessableEntityProblem +import io.airbyte.commons.server.support.RbacRoleHelper +import io.airbyte.config.persistence.UserPersistence +import io.airbyte.data.services.AuthRefreshTokenService +import io.micronaut.security.authentication.Authentication +import io.micronaut.security.token.event.RefreshTokenGeneratedEvent +import io.micronaut.security.token.refresh.RefreshTokenPersistence +import jakarta.inject.Singleton +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux + +@Singleton +class CommunityAuthRefreshTokenPersistence( + val authRefreshTokenService: AuthRefreshTokenService, +) : RefreshTokenPersistence { + override fun persistToken(event: RefreshTokenGeneratedEvent?) { + val sessionId = + event + ?.authentication + ?.attributes + ?.get(SESSION_ID) + ?.toString() + ?: throw UnprocessableEntityProblem(ProblemMessageData().message("Session ID not found in authentication")) + + val refreshToken = + event + .refreshToken + ?: throw UnprocessableEntityProblem(ProblemMessageData().message("Refresh token not found")) + + event.let { + authRefreshTokenService.saveAuthRefreshToken( + sessionId = sessionId, + tokenValue = refreshToken, + ) + } + } + + override fun getAuthentication(refreshToken: String): Publisher = + Flux.create { + val token = authRefreshTokenService.getAuthRefreshToken(refreshToken) + if (token == null) { + it.error(ForbiddenProblem(ProblemMessageData().message("Refresh token not found: $refreshToken"))) + } else if (token.revoked) { + it.error(ForbiddenProblem(ProblemMessageData().message("Refresh token revoked: $refreshToken"))) + } else { + it + .next( + Authentication.build( + UserPersistence.DEFAULT_USER_ID.toString(), + RbacRoleHelper.getInstanceAdminRoles(), + mapOf(SESSION_ID to token.sessionId), + ), + ).complete() + } + } +} diff --git a/airbyte-server/src/main/resources/application-community-auth.yml b/airbyte-server/src/main/resources/application-community-auth.yml deleted file mode 100644 index d22de0fd588..00000000000 --- a/airbyte-server/src/main/resources/application-community-auth.yml +++ /dev/null @@ -1,30 +0,0 @@ -micronaut: - security: - enabled: true - endpoints: - login: - enabled: true - path: /api/login - logout: - enabled: true - path: /api/logout - token: - jwt: - enabled: true - signatures: - secret: - generator: - secret: ${AB_JWT_SIGNATURE_SECRET:51c5739d-1145-4076-b190-8904d2c5de06} - generator: - refresh-token: - enabled: true - secret: ${AB_JWT_REFRESH_TOKEN_SECRET:d9f2755b-bd7c-49af-89d3-757de41f045a} - authentication: bearer - -airbyte: - auth: - instanceAdmin: - username: ${AB_INSTANCE_ADMIN:airbyte} - password: ${AB_INSTANCE_ADMIN_PASSWORD:password} - clientId: ${AB_INSTANCE_ADMIN_CLIENT_ID:00000000-00000000-00000000-00000000} - clientSecret: ${AB_INSTANCE_ADMIN_CLIENT_SECRET:5ba6a164-4a0b-4efa-8317-83b82648b16b} diff --git a/airbyte-server/src/main/resources/application-edition-community.yml b/airbyte-server/src/main/resources/application-edition-community.yml new file mode 100644 index 00000000000..c58d8044656 --- /dev/null +++ b/airbyte-server/src/main/resources/application-edition-community.yml @@ -0,0 +1,54 @@ +## This file configures authentication for the Community edition of Airbyte. It is loaded by the +# EditionPropertySourceLoader when AIRBYTE_EDITION is set to "community". +micronaut: + security: + enabled: ${API_AUTHORIZATION_ENABLED:false} + endpoints: + login: + enabled: true + path: /api/login + logout: + enabled: true + path: /api/logout + oauth: + path: /api/oauth/access_token + authentication: cookie + redirect: + enabled: false + token: + cookie: + enabled: true + ## TODO: don't do same-site: None on actual deploys (only local dev). We also might need to turn off cookie-secure for http only installs + ## solution 1: additional application.yml for cookie-same-site and cookie-secure for local dev? + ## solution 2: use a env variable and set it to None for local dev + cookie-same-site: None + cookie-secure: true + refresh: + cookie: + enabled: true + cookie-same-site: None + cookie-secure: true + cookie-max-age: PT5M + generator: + access-token: + ## Warning: if this is ever exposed to users to customize, the frontend will need to be updated + expiration: 180 # 3 mins + jwt: + enabled: true + signatures: + secret: + generator: + secret: ${AB_JWT_SIGNATURE_SECRET:51c5739d-1145-4076-b190-8904d2c5de06} + generator: + refresh-token: + enabled: true + secret: ${AB_JWT_SIGNATURE_SECRET:51c5739d-1145-4076-b190-8904d2c5de06} + +airbyte: + edition: community + auth: + instanceAdmin: + username: ${AB_INSTANCE_ADMIN:airbyte} + password: ${AB_INSTANCE_ADMIN_PASSWORD:password} + clientId: ${AB_INSTANCE_ADMIN_CLIENT_ID:00000000-00000000-00000000-00000000} + clientSecret: ${AB_INSTANCE_ADMIN_CLIENT_SECRET:5ba6a164-4a0b-4efa-8317-83b82648b16b} diff --git a/airbyte-server/src/main/resources/application.yml b/airbyte-server/src/main/resources/application.yml index 48d62005836..3438dcd8eb9 100644 --- a/airbyte-server/src/main/resources/application.yml +++ b/airbyte-server/src/main/resources/application.yml @@ -54,8 +54,15 @@ micronaut: enabled: true configurations: web: - allowed-origins-regex: - - ^.*$ + allowed-origins-regex: ${CORS_ALLOWED_ORIGINS_REGEX:} + allowed-methods: + - GET + - POST + - OPTIONS + allowed-headers: + - authorization + - content-type + - X-Airbyte-Analytic-Source idle-timeout: ${HTTP_IDLE_TIMEOUT:5m} netty: access-logger: diff --git a/airbyte-server/src/test/kotlin/io/airbyte/server/SimpleAuthProviderTest.kt b/airbyte-server/src/test/kotlin/io/airbyte/server/CommunityAuthProviderTest.kt similarity index 64% rename from airbyte-server/src/test/kotlin/io/airbyte/server/SimpleAuthProviderTest.kt rename to airbyte-server/src/test/kotlin/io/airbyte/server/CommunityAuthProviderTest.kt index 991f18aaa75..05aacf613a2 100644 --- a/airbyte-server/src/test/kotlin/io/airbyte/server/SimpleAuthProviderTest.kt +++ b/airbyte-server/src/test/kotlin/io/airbyte/server/CommunityAuthProviderTest.kt @@ -1,6 +1,7 @@ package io.airbyte.server import io.airbyte.commons.server.support.RbacRoleHelper +import io.airbyte.server.config.community.auth.CommunityAuthProvider import io.micronaut.context.annotation.Property import io.micronaut.security.authentication.UsernamePasswordCredentials import io.micronaut.test.extensions.junit5.annotation.MicronautTest @@ -10,16 +11,18 @@ import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -@MicronautTest(environments = ["community-auth"]) +@MicronautTest +@Property(name = "airbyte.edition", value = "community") +@Property(name = "micronaut.security.enabled", value = "true") @Property(name = "airbyte.auth.instanceAdmin.username", value = "test-username") @Property(name = "airbyte.auth.instanceAdmin.password", value = "test-password") -class SimpleAuthProviderTest { +class CommunityAuthProviderTest { @Inject - private lateinit var simpleAuthProvider: SimpleAuthProvider + private lateinit var communityAuthProvider: CommunityAuthProvider @Test fun `test authenticate with valid credentials`() { - val result = simpleAuthProvider.authenticate(null, UsernamePasswordCredentials("test-username", "test-password")) + val result = communityAuthProvider.authenticate(null, UsernamePasswordCredentials("test-username", "test-password")) assertTrue(result!!.isAuthenticated) val roles = result.authentication.get().roles @@ -28,7 +31,7 @@ class SimpleAuthProviderTest { @Test fun `test authenticate with invalid credentials`() { - val result = simpleAuthProvider.authenticate(null, UsernamePasswordCredentials("test-username", "invalid-password")) + val result = communityAuthProvider.authenticate(null, UsernamePasswordCredentials("test-username", "invalid-password")) assertFalse(result!!.isAuthenticated) } } diff --git a/airbyte-server/src/test/kotlin/io/airbyte/server/config/community/auth/CommunityAuthLogoutEventListenerTest.kt b/airbyte-server/src/test/kotlin/io/airbyte/server/config/community/auth/CommunityAuthLogoutEventListenerTest.kt new file mode 100644 index 00000000000..2d07acc1d22 --- /dev/null +++ b/airbyte-server/src/test/kotlin/io/airbyte/server/config/community/auth/CommunityAuthLogoutEventListenerTest.kt @@ -0,0 +1,42 @@ +package io.airbyte.server.config.community.auth + +import io.airbyte.api.problems.throwable.generated.UnprocessableEntityProblem +import io.airbyte.data.services.AuthRefreshTokenService +import io.micronaut.security.authentication.Authentication +import io.micronaut.security.event.LogoutEvent +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import java.util.Locale +import java.util.UUID + +@MicronautTest +class CommunityAuthLogoutEventListenerTest { + private val refreshTokenService = mockk(relaxed = true) + private val listener = CommunityAuthLogoutEventListener(refreshTokenService) + + @Test + fun `should revoke token on logout`() { + val sessionId = UUID.randomUUID().toString() + val authentication = mockk() + + every { authentication.attributes } returns mapOf(SESSION_ID to sessionId) + + listener.onApplicationEvent(LogoutEvent(authentication, null, Locale.getDefault())) + + verify { refreshTokenService.revokeAuthRefreshToken(sessionId) } + } + + @Test + fun `should throw exception if session ID is missing`() { + val authentication = mockk() + every { authentication.attributes } returns emptyMap() + + assertThrows(UnprocessableEntityProblem::class.java) { + listener.onApplicationEvent(LogoutEvent(authentication, null, Locale.getDefault())) + } + } +} diff --git a/airbyte-server/src/test/kotlin/io/airbyte/server/config/community/auth/CommunityAuthProviderTest.kt b/airbyte-server/src/test/kotlin/io/airbyte/server/config/community/auth/CommunityAuthProviderTest.kt new file mode 100644 index 00000000000..c0815f13027 --- /dev/null +++ b/airbyte-server/src/test/kotlin/io/airbyte/server/config/community/auth/CommunityAuthProviderTest.kt @@ -0,0 +1,51 @@ +package io.airbyte.server.config.community.auth + +import io.airbyte.commons.server.support.RbacRoleHelper +import io.airbyte.config.persistence.UserPersistence +import io.airbyte.data.config.InstanceAdminConfig +import io.micronaut.http.HttpRequest +import io.micronaut.security.authentication.UsernamePasswordCredentials +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +@MicronautTest +class CommunityAuthProviderTest { + private val instanceAdminConfig = mockk() + private val authProvider = CommunityAuthProvider(instanceAdminConfig) + + @Test + fun `should authenticate successfully with valid credentials`() { + val username = "admin" + val password = "password" + every { instanceAdminConfig.username } returns username + every { instanceAdminConfig.password } returns password + + val authRequest = UsernamePasswordCredentials(username, password) + val response = authProvider.authenticate(HttpRequest.GET("/"), authRequest)!! + + assertTrue(response.isAuthenticated) + assertEquals(UserPersistence.DEFAULT_USER_ID.toString(), response.authentication.get().name) + assertTrue( + response.authentication + .get() + .roles + .containsAll(RbacRoleHelper.getInstanceAdminRoles()), + ) + } + + @Test + fun `should fail authentication with invalid credentials`() { + every { instanceAdminConfig.username } returns "admin" + every { instanceAdminConfig.password } returns "password" + + val authRequest = UsernamePasswordCredentials("wrong", "credentials") + val response = authProvider.authenticate(HttpRequest.GET("/"), authRequest)!! + + assertFalse(response.isAuthenticated) + } +} diff --git a/airbyte-server/src/test/kotlin/io/airbyte/server/config/community/auth/CommunityAuthRefreshTokenPersistenceTest.kt b/airbyte-server/src/test/kotlin/io/airbyte/server/config/community/auth/CommunityAuthRefreshTokenPersistenceTest.kt new file mode 100644 index 00000000000..f09c9feabdb --- /dev/null +++ b/airbyte-server/src/test/kotlin/io/airbyte/server/config/community/auth/CommunityAuthRefreshTokenPersistenceTest.kt @@ -0,0 +1,69 @@ +package io.airbyte.server.config.community.auth + +import io.airbyte.api.problems.throwable.generated.ForbiddenProblem +import io.airbyte.commons.server.support.RbacRoleHelper +import io.airbyte.config.AuthRefreshToken +import io.airbyte.config.persistence.UserPersistence +import io.airbyte.data.services.AuthRefreshTokenService +import io.micronaut.security.authentication.Authentication +import io.micronaut.security.token.event.RefreshTokenGeneratedEvent +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.reactivestreams.Publisher +import reactor.test.StepVerifier + +@MicronautTest +class CommunityAuthRefreshTokenPersistenceTest { + private val authRefreshTokenService = mockk(relaxed = true) + private val refreshTokenPersistence = CommunityAuthRefreshTokenPersistence(authRefreshTokenService) + + @Test + fun `should persist token successfully`() { + val sessionId = "testSessionId" + val refreshToken = "testRefreshToken" + val event = mockk() + every { event.authentication.attributes } returns mapOf(SESSION_ID to sessionId) + every { event.refreshToken } returns refreshToken + + refreshTokenPersistence.persistToken(event) + + verify { authRefreshTokenService.saveAuthRefreshToken(sessionId, refreshToken) } + } + + @Test + fun `should retrieve authentication for valid token`() { + val refreshToken = "validToken" + val sessionId = "testSessionId" + every { authRefreshTokenService.getAuthRefreshToken(refreshToken) } returns + AuthRefreshToken().withSessionId(sessionId).withValue(refreshToken).withRevoked(false) + + val result = refreshTokenPersistence.getAuthentication(refreshToken) + + StepVerifier + .create(result) + .assertNext { authentication -> + assertEquals(UserPersistence.DEFAULT_USER_ID.toString(), authentication.name) + assertTrue(authentication.roles.containsAll(RbacRoleHelper.getInstanceAdminRoles())) + assertEquals(sessionId, authentication.attributes[SESSION_ID]) + }.verifyComplete() + } + + @Test + fun `should throw ForbiddenProblem for revoked token`() { + val refreshToken = "revokedToken" + every { authRefreshTokenService.getAuthRefreshToken(refreshToken) } returns + AuthRefreshToken().withSessionId("testSessionId").withValue(refreshToken).withRevoked(true) + + val result: Publisher = refreshTokenPersistence.getAuthentication(refreshToken) + + StepVerifier + .create(result) + .expectError(ForbiddenProblem::class.java) + .verify() + } +} diff --git a/airbyte-webapp/package.json b/airbyte-webapp/package.json index d40098604d0..461d91b9ffa 100644 --- a/airbyte-webapp/package.json +++ b/airbyte-webapp/package.json @@ -89,7 +89,6 @@ "framer-motion": "^6.3.11", "js-yaml": "^4.1.0", "json-schema": "^0.4.0", - "jwt-decode": "^4.0.0", "keycloak-js": "^23.0.7", "launchdarkly-js-client-sdk": "^3.1.0", "lodash": "^4.17.21", diff --git a/airbyte-webapp/pnpm-lock.yaml b/airbyte-webapp/pnpm-lock.yaml index 4e122673af6..2b0364fdb8f 100644 --- a/airbyte-webapp/pnpm-lock.yaml +++ b/airbyte-webapp/pnpm-lock.yaml @@ -115,9 +115,6 @@ dependencies: json-schema: specifier: ^0.4.0 version: 0.4.0 - jwt-decode: - specifier: ^4.0.0 - version: 4.0.0 keycloak-js: specifier: ^23.0.7 version: 23.0.7 diff --git a/airbyte-webapp/src/core/api/apiCall.ts b/airbyte-webapp/src/core/api/apiCall.ts index ebdd192db6a..c43adee281c 100644 --- a/airbyte-webapp/src/core/api/apiCall.ts +++ b/airbyte-webapp/src/core/api/apiCall.ts @@ -7,6 +7,7 @@ import { KnownApiProblem } from "./errors/problems"; export interface ApiCallOptions { getAccessToken: () => Promise; signal?: RequestInit["signal"]; + includeCredentials?: boolean; } export interface RequestOptions { @@ -58,6 +59,7 @@ export const fetchApiCall = async ( ...(data ? { body: getRequestBody(data) } : {}), headers: requestHeaders, signal: options.signal, + ...(options.includeCredentials ? { credentials: "include" } : {}), }); return parseResponse(response, request, requestUrl, responseType); diff --git a/airbyte-webapp/src/core/api/hooks/auth.ts b/airbyte-webapp/src/core/api/hooks/auth.ts index 603a40fbe6a..fd0bdb69b7b 100644 --- a/airbyte-webapp/src/core/api/hooks/auth.ts +++ b/airbyte-webapp/src/core/api/hooks/auth.ts @@ -1,4 +1,4 @@ -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { apiCall } from "../apis"; @@ -15,8 +15,8 @@ interface LoginResponseBody { expires_in: number; } -// Defined in code here because this endpoint is not currently part of the open api spec -export const login = (loginRequestBody: LoginRequestBody, options: Parameters[1]) => { +// These API calls are defined in code here because this endpoint is not currently part of the open api spec +export const simpleAuthLogin = (loginRequestBody: LoginRequestBody, options: Parameters[1]) => { return apiCall( { url: `/login`, @@ -28,13 +28,45 @@ export const login = (loginRequestBody: LoginRequestBody, options: Parameters => { - return login({ username: email, password }, { getAccessToken: () => Promise.resolve(null) }); +export const simpleAuthLogout = (options: Parameters[1]) => { + return apiCall( + { + url: `/logout`, + method: "post", + }, + options + ); +}; + +export const simpleAuthRefreshToken = (options: Parameters[1]) => { + return apiCall( + { + url: `/oauth/access_token`, + method: "post", + headers: { "Content-Type": "application/json" }, + }, + options + ); +}; + +const simpleAuthRequestOptions = { + getAccessToken: () => Promise.resolve(null), + includeCredentials: true, }; export const useSimpleAuthLogin = () => { - return useMutation( - async (loginRequestBody: LoginRequestBody) => - await simpleAuthLogin(loginRequestBody.username, loginRequestBody.password) + return useMutation(async (loginRequestBody: LoginRequestBody) => + simpleAuthLogin(loginRequestBody, simpleAuthRequestOptions) ); }; + +export const useSimpleAuthLogout = () => { + return useMutation(async () => simpleAuthLogout(simpleAuthRequestOptions)); +}; +export const useSimpleAuthTokenRefresh = () => { + return useQuery(["simpleAuthTokenRefresh"], async () => await simpleAuthRefreshToken(simpleAuthRequestOptions), { + refetchInterval: 60_000, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + }); +}; diff --git a/airbyte-webapp/src/core/api/hooks/users.ts b/airbyte-webapp/src/core/api/hooks/users.ts index 90a96dfa860..ce97a9f4f77 100644 --- a/airbyte-webapp/src/core/api/hooks/users.ts +++ b/airbyte-webapp/src/core/api/hooks/users.ts @@ -11,7 +11,7 @@ import { getUtmFromStorage } from "core/utils/utmStorage"; import { useGetInstanceConfiguration } from "./instanceConfiguration"; import { getOrCreateUserByAuthId, getUser, updateUser } from "../generated/AirbyteClient"; import { UserUpdate } from "../types/AirbyteClient"; -import { useRequestOptions } from "../useRequestOptions"; +import { emptyGetAccessToken, useRequestOptions } from "../useRequestOptions"; import { useSuspenseQuery } from "../useSuspenseQuery"; const userKeys = { @@ -20,13 +20,27 @@ const userKeys = { }; // In Community, we do not need to pass an access token to get the current user. This function can be passed in place of an actual getAccessToken callback. -const emptyGetAccesToken = () => Promise.resolve(null); - -export const useGetDefaultUser = ({ getAccessToken }: { getAccessToken?: AuthGetAccessToken }) => { +export const useGetDefaultUser = () => { const { defaultUserId: userId } = useGetInstanceConfiguration(); - return useSuspenseQuery(userKeys.detail(userId), () => - getUser({ userId }, { getAccessToken: getAccessToken ?? emptyGetAccesToken }) + return useSuspenseQuery(userKeys.detail(userId), () => getUser({ userId }, { getAccessToken: emptyGetAccessToken })); +}; + +export const useGetDefaultUserAsync = () => { + const { defaultUserId: userId } = useGetInstanceConfiguration(); + return useMutation( + () => + getUser( + { userId }, + { + getAccessToken: emptyGetAccessToken, + // Currently this is only used in the Simple Auth flow, so we need to include cookies + includeCredentials: true, + } + ), + { + retry: false, + } ); }; diff --git a/airbyte-webapp/src/core/api/useRequestOptions.ts b/airbyte-webapp/src/core/api/useRequestOptions.ts index 0ddeed1d9ff..787bc405d40 100644 --- a/airbyte-webapp/src/core/api/useRequestOptions.ts +++ b/airbyte-webapp/src/core/api/useRequestOptions.ts @@ -4,13 +4,16 @@ import { useAuthService } from "core/services/auth"; import { ApiCallOptions } from "./apiCall"; +export const emptyGetAccessToken = () => Promise.resolve(null); + export const useRequestOptions = (): ApiCallOptions => { - const { getAccessToken } = useAuthService(); + const { getAccessToken, authType } = useAuthService(); return useMemo( () => ({ - getAccessToken: getAccessToken ?? (() => Promise.resolve(null)), + getAccessToken: getAccessToken ?? emptyGetAccessToken, + includeCredentials: authType === "simple", }), - [getAccessToken] + [getAccessToken, authType] ); }; diff --git a/airbyte-webapp/src/core/services/auth/NoAuthService.tsx b/airbyte-webapp/src/core/services/auth/NoAuthService.tsx index 9b1f98dd403..99e3a45936b 100644 --- a/airbyte-webapp/src/core/services/auth/NoAuthService.tsx +++ b/airbyte-webapp/src/core/services/auth/NoAuthService.tsx @@ -6,8 +6,7 @@ import { AuthContext } from "./AuthContext"; // This is a static auth service in case the auth mode of the Airbyte instance is set to "none" export const NoAuthService: React.FC> = ({ children }) => { - // When auth is set to "none", the getUser endpoint does not require an access token - const defaultUser = useGetDefaultUser({ getAccessToken: () => Promise.resolve(null) }); + const defaultUser = useGetDefaultUser(); return ( { - if (jwt.length === 0) { - return false; - } - const decoded = jwtDecode(jwt); - return !!decoded.exp && decoded.exp < Date.now() / 1000; -}; +import { SimpleAuthTokenRefresher } from "./SimpleAuthTokenRefresher"; type AuthState = Pick; @@ -43,7 +34,7 @@ interface LoggedOutState extends AuthState { type SimpleAuthServiceAuthState = InitializingState | LoggedInState | LoggedOutState; -type AuthAction = { type: "login"; user: UserRead; accessToken: string } | { type: "logout" }; +type AuthAction = { type: "login"; user: UserRead } | { type: "logout" }; const simpleAuthStateReducer = (state: SimpleAuthServiceAuthState, action: AuthAction): SimpleAuthServiceAuthState => { switch (action.type) { @@ -75,13 +66,12 @@ const initialAuthState: InitializingState = { // This is a static auth service in case the auth mode of the Airbyte instance is set to "none" export const SimpleAuthService: React.FC = ({ children }) => { const [authState, dispatch] = useReducer(simpleAuthStateReducer, initialAuthState); - // Stored in a ref so we can update the access token without re-rendering the whole context - const accessTokenRef = useRef(null); const { mutateAsync: login } = useSimpleAuthLogin(); - const { mutateAsync: getAirbyteUser } = useGetOrCreateUser(); - const { defaultUserId } = useGetInstanceConfiguration(); + const { mutateAsync: logout } = useSimpleAuthLogout(); + const { mutateAsync: getDefaultUser } = useGetDefaultUserAsync(); + const notificationService = useNotificationService(); const initializingRef = useRef(false); - const navigate = useNavigate(); + const { formatMessage } = useIntl(); // This effect is explicitly run once to initialize the auth state useEffect(() => { @@ -90,42 +80,23 @@ export const SimpleAuthService: React.FC = ({ children }) => } async function initializeSimpleAuthService() { initializingRef.current = true; - const token = localStorage.getItem(SIMPLE_AUTH_LOCAL_STORAGE_KEY); - if (!token) { - dispatch({ type: "logout" }); - return; - } - if (isJwtExpired(token)) { - localStorage.removeItem(SIMPLE_AUTH_LOCAL_STORAGE_KEY); - dispatch({ type: "logout" }); - return; - } try { - accessTokenRef.current = token; - const user = await getAirbyteUser({ - authUserId: defaultUserId, - getAccessToken: () => Promise.resolve(token), - }); - dispatch({ type: "login", user, accessToken: token }); + const user = await getDefaultUser(); + dispatch({ type: "login", user }); } catch { dispatch({ type: "logout" }); } } initializeSimpleAuthService(); - }, [defaultUserId, getAirbyteUser]); + }, [getDefaultUser]); const loginCallback = useCallback( async (values: SimpleAuthLoginFormValues) => { - const loginResponse = await login({ username: values.username, password: values.password }); - accessTokenRef.current = loginResponse.access_token; - localStorage.setItem(SIMPLE_AUTH_LOCAL_STORAGE_KEY, loginResponse.access_token); - const user = await getAirbyteUser({ - authUserId: defaultUserId, - getAccessToken: () => Promise.resolve(loginResponse.access_token), - }); - dispatch({ type: "login", user, accessToken: loginResponse.access_token }); + await login({ username: values.username, password: values.password }); + const user = await getDefaultUser(); + dispatch({ type: "login", user }); }, - [defaultUserId, getAirbyteUser, login] + [getDefaultUser, login] ); const contextValue = useMemo(() => { @@ -134,18 +105,30 @@ export const SimpleAuthService: React.FC = ({ children }) => provider: null, emailVerified: false, ...authState, - getAccessToken: () => Promise.resolve(accessTokenRef.current), + getAccessToken: undefined, // With simple auth, the JWT is stored in a cookie that is set server-side login: authState.loggedOut ? loginCallback : undefined, logout: authState.loggedOut ? undefined : async () => { - localStorage.removeItem(SIMPLE_AUTH_LOCAL_STORAGE_KEY); - accessTokenRef.current = null; - navigate("/"); - dispatch({ type: "logout" }); + try { + await logout(); + dispatch({ type: "logout" }); + } catch (e) { + notificationService.registerNotification({ + type: "error", + id: "", + text: formatMessage({ id: "sidebar.logout.failed" }), + }); + console.error("Error logging out", e); + } }, } as const; - }, [loginCallback, authState, navigate]); + }, [loginCallback, authState, logout, notificationService, formatMessage]); - return {children}; + return ( + + {authState.inited && !authState.loggedOut && } + {children} + + ); }; diff --git a/airbyte-webapp/src/core/services/auth/SimpleAuthTokenRefresher.tsx b/airbyte-webapp/src/core/services/auth/SimpleAuthTokenRefresher.tsx new file mode 100644 index 00000000000..c3faca34565 --- /dev/null +++ b/airbyte-webapp/src/core/services/auth/SimpleAuthTokenRefresher.tsx @@ -0,0 +1,22 @@ +import { useEffect } from "react"; + +import { useSimpleAuthTokenRefresh } from "core/api"; + +import { useAuthService } from "./AuthContext"; + +/** + * When rendered, this component will refresh the auth token every 60 seconds + */ +export const SimpleAuthTokenRefresher: React.FC = () => { + const { logout } = useAuthService(); + const { error } = useSimpleAuthTokenRefresh(); + + useEffect(() => { + if (error) { + console.debug("🔑 Failed to refresh simple auth token. User will be logged out."); + logout?.(); + } + }, [error, logout]); + + return null; +}; diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index a73a0ca81ba..d25bcc4e569 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -59,6 +59,7 @@ "sidebar.upcomingFeatures": "Upcoming features", "sidebar.userSettings": "User settings", "sidebar.logout": "Sign out", + "sidebar.logout.failed": "An error occurred while signing out. Please try again.", "sidebar.lightMode": "Light mode", "sidebar.darkMode": "Dark mode", "sidebar.beta": "Beta", diff --git a/airbyte-webapp/src/packages/cloud/services/auth/CloudAuthService.tsx b/airbyte-webapp/src/packages/cloud/services/auth/CloudAuthService.tsx index 343f2827157..1a5a0f357f8 100644 --- a/airbyte-webapp/src/packages/cloud/services/auth/CloudAuthService.tsx +++ b/airbyte-webapp/src/packages/cloud/services/auth/CloudAuthService.tsx @@ -222,7 +222,6 @@ export const CloudAuthService: React.FC = ({ children }) => { // Handle login/logoff that happened in another tab useEffect(() => { broadcastChannel.onmessage = (event) => { - console.log("broadcastChannel.onmessage", event); if (event.type === "userUnloaded") { console.debug("🔑 Received userUnloaded event from other tab."); dispatch({ type: "userUnloaded" }); diff --git a/airbyte-workload-api-server/src/main/resources/application.yml b/airbyte-workload-api-server/src/main/resources/application.yml index bfdc3a1747c..258f6f00724 100644 --- a/airbyte-workload-api-server/src/main/resources/application.yml +++ b/airbyte-workload-api-server/src/main/resources/application.yml @@ -42,12 +42,6 @@ micronaut: secret: ${WORKLOAD_API_BEARER_TOKEN:} server: port: 8007 - cors: - enabled: true - configurations: - web: - allowed-origins-regex: - - ^.*$ idle-timeout: ${HTTP_IDLE_TIMEOUT:5m} netty: access-logger: diff --git a/charts/airbyte-server/templates/deployment.yaml b/charts/airbyte-server/templates/deployment.yaml index 32846ec7013..8aea1597c42 100644 --- a/charts/airbyte-server/templates/deployment.yaml +++ b/charts/airbyte-server/templates/deployment.yaml @@ -63,6 +63,12 @@ spec: value: "-Xdebug -agentlib:jdwp=transport=dt_socket,address=0.0.0.0:{{ .Values.debug.remoteDebugPort }},server=y,suspend=n" {{- end}} {{- if or (eq .Values.global.edition "pro") (eq .Values.global.edition "enterprise") }} + - name: API_AUTHORIZATION_ENABLED + value: "true" + {{- else if and (eq .Values.global.deploymentMode "oss") (eq .Values.global.auth.enabled true) }} + # Self-Managed Enterprise should always have API_AUTHORIZATION_ENABLED set to true, even + # if global.auth.enabled is not set to true. This can be simplified in the future, once + # globa.auth.enabled is changed to always default to true across all editions of Airbyte. - name: API_AUTHORIZATION_ENABLED value: "true" {{- end }} @@ -86,7 +92,7 @@ spec: valueFrom: configMapKeyRef: name: {{ .Values.global.configMapName | default (printf "%s-airbyte-env" .Release.Name) }} - key: AIRBYTE_URL + key: AIRBYTE_URL - name: AUTO_DETECT_SCHEMA valueFrom: configMapKeyRef: diff --git a/charts/airbyte-worker/templates/deployment.yaml b/charts/airbyte-worker/templates/deployment.yaml index b54dd1fe0c1..6b58f0ae18d 100644 --- a/charts/airbyte-worker/templates/deployment.yaml +++ b/charts/airbyte-worker/templates/deployment.yaml @@ -304,6 +304,13 @@ spec: name: {{ .Release.Name }}-airbyte-env key: WORKLOAD_API_SERVER_ENABLED {{- if or (eq .Values.global.edition "pro") (eq .Values.global.edition "enterprise") }} + - name: AIRBYTE_API_AUTH_HEADER_NAME + value: "X-Airbyte-Auth" + - name: AIRBYTE_API_AUTH_HEADER_VALUE + value: "Internal worker" + {{- else if and (eq .Values.global.deploymentMode "oss") (eq .Values.global.auth.enabled true) }} + # Self-Managed Enterprise and Community w/ auth enabled use the same auth header, just + # splitting into two separate blocks for readability. - name: AIRBYTE_API_AUTH_HEADER_NAME value: "X-Airbyte-Auth" - name: AIRBYTE_API_AUTH_HEADER_VALUE diff --git a/charts/airbyte-workload-launcher/templates/deployment.yaml b/charts/airbyte-workload-launcher/templates/deployment.yaml index 79ffb050322..cb7e07f2775 100644 --- a/charts/airbyte-workload-launcher/templates/deployment.yaml +++ b/charts/airbyte-workload-launcher/templates/deployment.yaml @@ -401,6 +401,13 @@ spec: name: {{ .Release.Name }}-airbyte-env key: WORKLOAD_LAUNCHER_PARALLELISM {{- if or (eq .Values.global.edition "pro") (eq .Values.global.edition "enterprise") }} + - name: AIRBYTE_API_AUTH_HEADER_NAME + value: "X-Airbyte-Auth" + - name: AIRBYTE_API_AUTH_HEADER_VALUE + value: "Internal worker" + {{- else if and (eq .Values.global.deploymentMode "oss") (eq .Values.global.auth.enabled true) }} + # Self-Managed Enterprise and Community w/ auth enabled use the same auth header, just + # splitting into two separate blocks for readability. - name: AIRBYTE_API_AUTH_HEADER_NAME value: "X-Airbyte-Auth" - name: AIRBYTE_API_AUTH_HEADER_VALUE diff --git a/charts/airbyte/values.yaml b/charts/airbyte/values.yaml index 647e844bd1d..2bef3084c2a 100644 --- a/charts/airbyte/values.yaml +++ b/charts/airbyte/values.yaml @@ -23,6 +23,8 @@ global: # -- Auth configuration auth: + # -- Whether auth is enabled + enabled: false # -- Admin user configuration instanceAdmin: # -- Secret name where the instanceAdmin configuration is stored