Skip to content

Commit b912665

Browse files
committed
feat: community auth updates and finalization (#13333)
1 parent cc663f1 commit b912665

File tree

15 files changed

+109
-112
lines changed

15 files changed

+109
-112
lines changed

airbyte-api/server-api/src/main/openapi/config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11778,6 +11778,7 @@ components:
1177811778
- airbyteUrl
1177911779
- auth
1178011780
- defaultOrganizationId
11781+
- defaultOrganizationEmail
1178111782
- defaultUserId
1178211783
- defaultWorkspaceId
1178311784
- edition
@@ -11811,6 +11812,9 @@ components:
1181111812
defaultOrganizationId:
1181211813
type: string
1181311814
format: uuid
11815+
defaultOrganizationEmail:
11816+
type: string
11817+
format: email
1181411818
defaultWorkspaceId:
1181511819
type: string
1181611820
format: uuid

airbyte-bootloader/src/main/kotlin/io/airbyte/bootloader/AuthKubernetesSecretInitializer.kt

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import java.util.UUID
1414

1515
private val logger = KotlinLogging.logger {}
1616

17-
const val SECRET_LENGTH = 16
18-
private const val DEFAULT_USERNAME_VALUE = "airbyte"
17+
const val SECRET_LENGTH = 32
1918

2019
@Singleton
2120
@Requires(env = [Environment.KUBERNETES])
@@ -48,12 +47,6 @@ class AuthKubernetesSecretInitializer(
4847
}
4948

5049
private fun getSecretDataMap(): Map<String, String> {
51-
val usernameValue =
52-
getOrCreateSecretEncodedValue(
53-
secretKeysConfig.instanceAdminUsernameSecretKey,
54-
providedSecretValuesConfig.instanceAdminUsername,
55-
DEFAULT_USERNAME_VALUE,
56-
)
5750
val passwordValue =
5851
getOrCreateSecretEncodedValue(
5952
secretKeysConfig.instanceAdminPasswordSecretKey,
@@ -76,10 +69,9 @@ class AuthKubernetesSecretInitializer(
7669
getOrCreateSecretEncodedValue(
7770
secretKeysConfig.jwtSignatureSecretKey,
7871
providedSecretValuesConfig.jwtSignatureSecret,
79-
UUID.randomUUID().toString(),
72+
RandomStringUtils.randomAlphanumeric(SECRET_LENGTH),
8073
)
8174
return mapOf(
82-
secretKeysConfig.instanceAdminUsernameSecretKey!! to usernameValue,
8375
secretKeysConfig.instanceAdminPasswordSecretKey!! to passwordValue,
8476
secretKeysConfig.instanceAdminClientIdSecretKey!! to clientIdValue,
8577
secretKeysConfig.instanceAdminClientSecretSecretKey!! to clientSecretValue,
@@ -115,7 +107,6 @@ class AuthKubernetesSecretInitializer(
115107

116108
@ConfigurationProperties("airbyte.auth.kubernetes-secret.keys")
117109
open class AuthKubernetesSecretKeysConfig {
118-
var instanceAdminUsernameSecretKey: String? = null
119110
var instanceAdminPasswordSecretKey: String? = null
120111
var instanceAdminClientIdSecretKey: String? = null
121112
var instanceAdminClientSecretSecretKey: String? = null
@@ -124,7 +115,6 @@ open class AuthKubernetesSecretKeysConfig {
124115

125116
@ConfigurationProperties("airbyte.auth.kubernetes-secret.values")
126117
open class AuthKubernetesSecretValuesConfig {
127-
var instanceAdminUsername: String? = null
128118
var instanceAdminPassword: String? = null
129119
var instanceAdminClientId: String? = null
130120
var instanceAdminClientSecret: String? = null

airbyte-bootloader/src/main/resources/application.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,11 @@ airbyte:
3535
creation-enabled: ${AB_AUTH_SECRET_CREATION_ENABLED:false}
3636
name: ${AB_KUBERNETES_SECRET_NAME:airbyte-auth-secrets}
3737
keys:
38-
instance-admin-username-secret-key: ${AB_INSTANCE_ADMIN_USERNAME_SECRET_KEY:instance-admin-username}
3938
instance-admin-password-secret-key: ${AB_INSTANCE_ADMIN_PASSWORD_SECRET_KEY:instance-admin-password}
4039
instance-admin-client-id-secret-key: ${AB_INSTANCE_ADMIN_CLIENT_ID_SECRET_KEY:instance-admin-client-id}
4140
instance-admin-client-secret-secret-key: ${AB_INSTANCE_ADMIN_CLIENT_SECRET_SECRET_KEY:instance-admin-client-secret}
4241
jwt-signature-secret-key: ${AB_JWT_SIGNATURE_SECRET_KEY:jwt-signature-secret}
4342
values:
44-
instance-admin-username: ${AB_INSTANCE_ADMIN_USERNAME:}
4543
instance-admin-password: ${AB_INSTANCE_ADMIN_PASSWORD:}
4644
instance-admin-client-id: ${AB_INSTANCE_ADMIN_CLIENT_ID:}
4745
instance-admin-client-secret: ${AB_INSTANCE_ADMIN_CLIENT_SECRET:}

airbyte-bootloader/src/test-integration/kotlin/io/airbyte/bootloader/AuthKubernetesSecretInitializerTest.kt

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,10 @@ import org.junit.jupiter.api.Test
2222
import java.util.Base64
2323

2424
private const val SECRET_NAME = "test-secret"
25-
private const val USERNAME_KEY = "username"
2625
private const val PASSWORD_KEY = "password"
2726
private const val CLIENT_ID_KEY = "clientId"
2827
private const val CLIENT_SECRET_KEY = "clientSecret"
2928
private const val JWT_SIGNATURE_KEY = "jwtSignature"
30-
private const val PROVIDED_USERNAME_VALUE = "admin"
3129
private const val PROVIDED_PASSWORD_VALUE = "hunter2"
3230
private const val PROVIDED_CLIENT_ID_VALUE = "myClientId"
3331
private const val PROVIDED_CLIENT_SECRET_VALUE = "myClientSecret"
@@ -86,8 +84,7 @@ class AuthKubernetesSecretInitializerTest {
8684
verify { mockKubernetesClient.resource(capture(secretSlot)) }
8785
val capturedSecret = secretSlot.captured
8886
assertEquals(SECRET_NAME, capturedSecret.metadata.name)
89-
assertEquals(5, capturedSecret.data.size)
90-
assertEquals(Base64.getEncoder().encodeToString(PROVIDED_USERNAME_VALUE.toByteArray()), capturedSecret.data[USERNAME_KEY])
87+
assertEquals(4, capturedSecret.data.size)
9188
assertEquals(Base64.getEncoder().encodeToString(PROVIDED_PASSWORD_VALUE.toByteArray()), capturedSecret.data[PASSWORD_KEY])
9289
assertEquals(Base64.getEncoder().encodeToString(PROVIDED_CLIENT_ID_VALUE.toByteArray()), capturedSecret.data[CLIENT_ID_KEY])
9390
assertEquals(Base64.getEncoder().encodeToString(PROVIDED_CLIENT_SECRET_VALUE.toByteArray()), capturedSecret.data[CLIENT_SECRET_KEY])
@@ -115,8 +112,7 @@ class AuthKubernetesSecretInitializerTest {
115112
verify { mockKubernetesClient.resource(capture(secretSlot)) }
116113
val capturedSecret = secretSlot.captured
117114
assertEquals(SECRET_NAME, capturedSecret.metadata.name)
118-
assertEquals(5, capturedSecret.data.size)
119-
assertEquals(Base64.getEncoder().encodeToString(PROVIDED_USERNAME_VALUE.toByteArray()), capturedSecret.data[USERNAME_KEY])
115+
assertEquals(4, capturedSecret.data.size)
120116
assertEquals(Base64.getEncoder().encodeToString(PROVIDED_PASSWORD_VALUE.toByteArray()), capturedSecret.data[PASSWORD_KEY])
121117
assertEquals(Base64.getEncoder().encodeToString(PROVIDED_CLIENT_ID_VALUE.toByteArray()), capturedSecret.data[CLIENT_ID_KEY])
122118
assertEquals(Base64.getEncoder().encodeToString(PROVIDED_CLIENT_SECRET_VALUE.toByteArray()), capturedSecret.data[CLIENT_SECRET_KEY])
@@ -135,11 +131,10 @@ class AuthKubernetesSecretInitializerTest {
135131
.withNewMetadata()
136132
.withName(SECRET_NAME)
137133
.endMetadata()
138-
// username is already set in the secret, so it should persist through the update as it
139-
// was not provided.
140-
.addToData(USERNAME_KEY, Base64.getEncoder().encodeToString("preExistingUsername".toByteArray()))
141134
// clientId is already set in the secret, but a new value was provided, so it should be updated.
142135
.addToData(CLIENT_ID_KEY, Base64.getEncoder().encodeToString("preExistingClientId".toByteArray()))
136+
// clientSecret is already set in the secret, and no new value was provided, so it should remain the same.
137+
.addToData(CLIENT_SECRET_KEY, Base64.getEncoder().encodeToString(PROVIDED_CLIENT_SECRET_VALUE.toByteArray()))
143138
.build()
144139
every { mockKubernetesClient.secrets().withName(any()).get() } returns existingSecret
145140
every { mockKubernetesClient.resource(any<Secret>()) } returns mockResource
@@ -154,8 +149,7 @@ class AuthKubernetesSecretInitializerTest {
154149
verify { mockKubernetesClient.resource(capture(secretSlot)) }
155150
val capturedSecret = secretSlot.captured
156151
assertEquals(SECRET_NAME, capturedSecret.metadata.name)
157-
assertEquals(5, capturedSecret.data.size)
158-
assertEquals(Base64.getEncoder().encodeToString("preExistingUsername".toByteArray()), capturedSecret.data[USERNAME_KEY])
152+
assertEquals(4, capturedSecret.data.size)
159153
assertEquals(Base64.getEncoder().encodeToString(randomPassword.toByteArray()), capturedSecret.data[PASSWORD_KEY])
160154
assertEquals(Base64.getEncoder().encodeToString(PROVIDED_CLIENT_ID_VALUE.toByteArray()), capturedSecret.data[CLIENT_ID_KEY])
161155
assertEquals(Base64.getEncoder().encodeToString(PROVIDED_CLIENT_SECRET_VALUE.toByteArray()), capturedSecret.data[CLIENT_SECRET_KEY])
@@ -166,23 +160,20 @@ class AuthKubernetesSecretInitializerTest {
166160
}
167161

168162
private fun setupProvidedSecretValuesConfigWithoutPassword() {
169-
every { mockProvidedSecretValuesConfig.instanceAdminUsername } returns null
170163
every { mockProvidedSecretValuesConfig.instanceAdminPassword } returns null
171164
every { mockProvidedSecretValuesConfig.instanceAdminClientId } returns PROVIDED_CLIENT_ID_VALUE
172165
every { mockProvidedSecretValuesConfig.instanceAdminClientSecret } returns PROVIDED_CLIENT_SECRET_VALUE
173166
every { mockProvidedSecretValuesConfig.jwtSignatureSecret } returns PROVIDED_JWT_SIGNATURE_VALUE
174167
}
175168

176169
private fun setupSecretKeysConfig() {
177-
every { mockSecretKeysConfig.instanceAdminUsernameSecretKey } returns USERNAME_KEY
178170
every { mockSecretKeysConfig.instanceAdminPasswordSecretKey } returns PASSWORD_KEY
179171
every { mockSecretKeysConfig.instanceAdminClientIdSecretKey } returns CLIENT_ID_KEY
180172
every { mockSecretKeysConfig.instanceAdminClientSecretSecretKey } returns CLIENT_SECRET_KEY
181173
every { mockSecretKeysConfig.jwtSignatureSecretKey } returns JWT_SIGNATURE_KEY
182174
}
183175

184176
private fun setupProvidedSecretValuesConfig() {
185-
every { mockProvidedSecretValuesConfig.instanceAdminUsername } returns PROVIDED_USERNAME_VALUE
186177
every { mockProvidedSecretValuesConfig.instanceAdminPassword } returns PROVIDED_PASSWORD_VALUE
187178
every { mockProvidedSecretValuesConfig.instanceAdminClientId } returns PROVIDED_CLIENT_ID_VALUE
188179
every { mockProvidedSecretValuesConfig.instanceAdminClientSecret } returns PROVIDED_CLIENT_SECRET_VALUE

airbyte-commons-server/src/main/java/io/airbyte/commons/server/handlers/InstanceConfigurationHandler.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public InstanceConfigurationHandler(@Named("airbyteUrl") final Optional<String>
7676
}
7777

7878
public InstanceConfigurationResponse getInstanceConfiguration() throws IOException {
79-
final UUID defaultOrganizationId = getDefaultOrganizationId();
79+
final Organization defaultOrganization = getDefaultOrganization();
8080
final Boolean initialSetupComplete = workspacePersistence.getInitialSetupComplete();
8181

8282
return new InstanceConfigurationResponse()
@@ -87,18 +87,23 @@ public InstanceConfigurationResponse getInstanceConfiguration() throws IOExcepti
8787
.auth(getAuthConfiguration())
8888
.initialSetupComplete(initialSetupComplete)
8989
.defaultUserId(getDefaultUserId())
90-
.defaultOrganizationId(defaultOrganizationId)
90+
.defaultOrganizationId(defaultOrganization.getOrganizationId())
91+
.defaultOrganizationEmail(defaultOrganization.getEmail())
9192
.trackingStrategy("segment".equalsIgnoreCase(trackingStrategy) ? TrackingStrategyEnum.SEGMENT : TrackingStrategyEnum.LOGGING);
9293
}
9394

9495
public InstanceConfigurationResponse setupInstanceConfiguration(final InstanceConfigurationSetupRequestBody requestBody)
9596
throws IOException, JsonValidationException, ConfigNotFoundException {
9697

97-
final UUID defaultOrganizationId = getDefaultOrganizationId();
98-
final StandardWorkspace defaultWorkspace = getDefaultWorkspace(defaultOrganizationId);
98+
final Organization defaultOrganization = getDefaultOrganization();
99+
final StandardWorkspace defaultWorkspace = getDefaultWorkspace(defaultOrganization.getOrganizationId());
99100

100-
// Update the default organization and user with the provided information
101+
// Update the default organization and user with the provided information.
102+
// note that this is important especially for Community edition w/ Auth enabled,
103+
// because the login email must match the default organization's saved email in
104+
// order to login successfully.
101105
updateDefaultOrganization(requestBody);
106+
102107
updateDefaultUser(requestBody);
103108

104109
// Update the underlying workspace to mark the initial setup as complete
@@ -154,10 +159,9 @@ private void updateDefaultUser(final InstanceConfigurationSetupRequestBody reque
154159
userPersistence.writeUser(defaultUser);
155160
}
156161

157-
private UUID getDefaultOrganizationId() throws IOException {
162+
private Organization getDefaultOrganization() throws IOException {
158163
return organizationPersistence.getDefaultOrganization()
159-
.orElseThrow(() -> new IllegalStateException("Default organization does not exist."))
160-
.getOrganizationId();
164+
.orElseThrow(() -> new IllegalStateException("Default organization does not exist."));
161165
}
162166

163167
private void updateDefaultOrganization(final InstanceConfigurationSetupRequestBody requestBody) throws IOException {

airbyte-commons-server/src/test/java/io/airbyte/commons/server/handlers/InstanceConfigurationHandlerTest.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class InstanceConfigurationHandlerTest {
5555
private static final UUID WORKSPACE_ID = UUID.randomUUID();
5656
private static final UUID USER_ID = UUID.randomUUID();
5757
private static final UUID ORGANIZATION_ID = UUID.randomUUID();
58+
private static final String EMAIL = "[email protected]";
5859
private static final String DEFAULT_ORG_NAME = "Default Org Name";
5960
private static final String DEFAULT_USER_NAME = "Default User Name";
6061

@@ -114,6 +115,7 @@ void testGetInstanceConfiguration(final boolean isEnterprise, final boolean isIn
114115
.initialSetupComplete(isInitialSetupComplete)
115116
.defaultUserId(USER_ID)
116117
.defaultOrganizationId(ORGANIZATION_ID)
118+
.defaultOrganizationEmail(EMAIL)
117119
.trackingStrategy(TrackingStrategyEnum.LOGGING);
118120

119121
final InstanceConfigurationResponse actual = instanceConfigurationHandler.getInstanceConfiguration();
@@ -199,8 +201,6 @@ void testSetupInstanceConfiguration(final boolean userNamePresent, final boolean
199201

200202
instanceConfigurationHandler = getInstanceConfigurationHandler(true);
201203

202-
final String email = "[email protected]";
203-
204204
final InstanceConfigurationResponse expected = new InstanceConfigurationResponse()
205205
.edition(EditionEnum.PRO)
206206
.version("0.50.1")
@@ -213,10 +213,11 @@ void testSetupInstanceConfiguration(final boolean userNamePresent, final boolean
213213
.initialSetupComplete(true)
214214
.defaultUserId(USER_ID)
215215
.defaultOrganizationId(ORGANIZATION_ID)
216+
.defaultOrganizationEmail(EMAIL)
216217
.trackingStrategy(TrackingStrategyEnum.LOGGING);
217218

218219
final InstanceConfigurationSetupRequestBody requestBody = new InstanceConfigurationSetupRequestBody()
219-
.email(email)
220+
.email(EMAIL)
220221
.displaySetupWizard(true)
221222
.anonymousDataCollection(true)
222223
.initialSetupComplete(true);
@@ -240,19 +241,19 @@ void testSetupInstanceConfiguration(final boolean userNamePresent, final boolean
240241
// verify the user was updated with the email and name from the request
241242
verify(mUserPersistence).writeUser(eq(new User()
242243
.withUserId(USER_ID)
243-
.withEmail(email)
244+
.withEmail(EMAIL)
244245
.withName(expectedUserName)));
245246

246247
// verify the organization was updated with the name from the request
247248
verify(mOrganizationPersistence).updateOrganization(eq(new Organization()
248249
.withOrganizationId(ORGANIZATION_ID)
249250
.withName(expectedOrgName)
250-
.withEmail(email)
251+
.withEmail(EMAIL)
251252
.withUserId(USER_ID)));
252253

253254
verify(mWorkspacesHandler).updateWorkspace(eq(new WorkspaceUpdate()
254255
.workspaceId(WORKSPACE_ID)
255-
.email(email)
256+
.email(EMAIL)
256257
.displaySetupWizard(true)
257258
.anonymousDataCollection(true)
258259
.initialSetupComplete(true)));
@@ -270,7 +271,8 @@ private void stubGetDefaultOrganization() throws IOException {
270271
Optional.of(new Organization()
271272
.withOrganizationId(ORGANIZATION_ID)
272273
.withName(DEFAULT_ORG_NAME)
273-
.withUserId(USER_ID)));
274+
.withUserId(USER_ID)
275+
.withEmail(EMAIL)));
274276
}
275277

276278
private void stubDefaultAuthConfigs() {

airbyte-server/src/main/kotlin/io/airbyte/server/config/community/auth/CommunityAuthProvider.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
*/
44
package io.airbyte.server.config.community.auth
55

6+
import io.airbyte.api.problems.model.generated.ProblemMessageData
7+
import io.airbyte.api.problems.throwable.generated.ForbiddenProblem
68
import io.airbyte.commons.auth.RequiresAuthMode
79
import io.airbyte.commons.auth.config.AuthMode
810
import io.airbyte.commons.server.support.RbacRoleHelper
11+
import io.airbyte.config.persistence.OrganizationPersistence
912
import io.airbyte.config.persistence.UserPersistence
1013
import io.airbyte.data.config.InstanceAdminConfig
1114
import io.micronaut.http.HttpRequest
@@ -24,12 +27,21 @@ const val SESSION_ID = "sessionId"
2427
@RequiresAuthMode(AuthMode.SIMPLE)
2528
class CommunityAuthProvider<B>(
2629
private val instanceAdminConfig: InstanceAdminConfig,
30+
private val organizationPersistence: OrganizationPersistence,
2731
) : HttpRequestAuthenticationProvider<B> {
2832
override fun authenticate(
2933
requestContext: HttpRequest<B>?,
3034
authRequest: AuthenticationRequest<String, String>,
3135
): AuthenticationResponse? {
32-
if (authRequest.identity == instanceAdminConfig.username && authRequest.secret == instanceAdminConfig.password) {
36+
// The authRequest identity must match the default organization's email address that
37+
// was collected during the instanceConfiguration step.
38+
val defaultOrgEmail =
39+
organizationPersistence.defaultOrganization
40+
.orElseThrow {
41+
ForbiddenProblem(ProblemMessageData().message("Default organization not found. Cannot authenticate."))
42+
}.email
43+
44+
if (authRequest.identity == defaultOrgEmail && authRequest.secret == instanceAdminConfig.password) {
3345
val sessionId = UUID.randomUUID()
3446
val authenticationResponse =
3547
AuthenticationResponse.success(

airbyte-server/src/main/resources/application-edition-community.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ micronaut:
1919
cookie:
2020
enabled: true
2121
cookie-same-site: ${AB_COOKIE_SAME_SITE:Strict}
22-
cookie-secure: true
22+
cookie-secure: ${AB_COOKIE_SECURE:true}
2323
refresh:
2424
cookie:
2525
enabled: true
26-
cookie-same-site: None
27-
cookie-secure: true
26+
cookie-same-site: ${AB_COOKIE_SAME_SITE:Strict}
27+
cookie-secure: ${AB_COOKIE_SECURE:true}
2828
cookie-max-age: PT5M
2929
generator:
3030
access-token:
@@ -45,7 +45,6 @@ airbyte:
4545
edition: community
4646
auth:
4747
instanceAdmin:
48-
username: ${AB_INSTANCE_ADMIN_USERNAME:}
4948
password: ${AB_INSTANCE_ADMIN_PASSWORD:}
5049
clientId: ${AB_INSTANCE_ADMIN_CLIENT_ID:}
5150
clientSecret: ${AB_INSTANCE_ADMIN_CLIENT_SECRET:}

airbyte-server/src/test/kotlin/io/airbyte/server/CommunityAuthProviderTest.kt

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)