Skip to content

Commit 224579d

Browse files
committed
feat: connector config entitlement for rejected records storage (#16799)
1 parent b316f44 commit 224579d

File tree

21 files changed

+791
-72
lines changed

21 files changed

+791
-72
lines changed

airbyte-commons-entitlements/src/main/kotlin/io/airbyte/commons/entitlements/EntitlementProvider.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import io.airbyte.config.ActorType
1010
import io.airbyte.featureflag.AllowConfigTemplateEndpoints
1111
import io.airbyte.featureflag.DestinationDefinition
1212
import io.airbyte.featureflag.FeatureFlagClient
13+
import io.airbyte.featureflag.LicenseAllowDestinationObjectStorageConfig
1314
import io.airbyte.featureflag.LicenseAllowEnterpriseConnector
1415
import io.airbyte.featureflag.Multi
1516
import io.airbyte.featureflag.Organization
@@ -28,6 +29,8 @@ interface EntitlementProvider {
2829
): Map<UUID, Boolean>
2930

3031
fun hasConfigTemplateEntitlements(organizationId: UUID): Boolean
32+
33+
fun hasDestinationObjectStorageEntitlement(organizationId: UUID): Boolean
3134
}
3235

3336
/**
@@ -42,6 +45,8 @@ class DefaultEntitlementProvider : EntitlementProvider {
4245
): Map<UUID, Boolean> = actorDefinitionIds.associateWith { _ -> false }
4346

4447
override fun hasConfigTemplateEntitlements(organizationId: UUID): Boolean = false
48+
49+
override fun hasDestinationObjectStorageEntitlement(organizationId: UUID): Boolean = false
4550
}
4651

4752
/**
@@ -68,6 +73,8 @@ class EnterpriseEntitlementProvider(
6873
}
6974

7075
override fun hasConfigTemplateEntitlements(organizationId: UUID): Boolean = activeLicense.license?.isEmbedded ?: false
76+
77+
override fun hasDestinationObjectStorageEntitlement(organizationId: UUID): Boolean = true
7178
}
7279

7380
/**
@@ -104,4 +111,7 @@ class CloudEntitlementProvider(
104111

105112
override fun hasConfigTemplateEntitlements(organizationId: UUID): Boolean =
106113
featureFlagClient.boolVariation(AllowConfigTemplateEndpoints, Organization(organizationId))
114+
115+
override fun hasDestinationObjectStorageEntitlement(organizationId: UUID): Boolean =
116+
featureFlagClient.boolVariation(flag = LicenseAllowDestinationObjectStorageConfig, Organization(organizationId))
107117
}

airbyte-commons-entitlements/src/main/kotlin/io/airbyte/commons/entitlements/EntitlementService.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
package io.airbyte.commons.entitlements
66

7+
import io.airbyte.api.problems.model.generated.ProblemLicenseEntitlementData
8+
import io.airbyte.api.problems.throwable.generated.LicenseEntitlementProblem
79
import io.airbyte.commons.entitlements.models.ConnectorEntitlement
10+
import io.airbyte.commons.entitlements.models.DestinationObjectStorageEntitlement
811
import io.airbyte.commons.entitlements.models.Entitlement
912
import io.airbyte.commons.entitlements.models.EntitlementResult
1013
import io.airbyte.config.ActorType
@@ -19,7 +22,24 @@ class EntitlementService(
1922
fun checkEntitlement(
2023
organizationId: UUID,
2124
entitlement: Entitlement,
22-
): EntitlementResult = entitlementClient.checkEntitlement(organizationId, entitlement)
25+
): EntitlementResult =
26+
when (entitlement) {
27+
// TODO: Remove once we've migrated the entitlement to Stigg
28+
DestinationObjectStorageEntitlement -> hasDestinationObjectStorageEntitlement(organizationId)
29+
else -> entitlementClient.checkEntitlement(organizationId, entitlement)
30+
}
31+
32+
fun ensureEntitled(
33+
organizationId: UUID,
34+
entitlement: Entitlement,
35+
) {
36+
if (!checkEntitlement(organizationId, entitlement).isEntitled) {
37+
throw LicenseEntitlementProblem(
38+
ProblemLicenseEntitlementData()
39+
.entitlement(entitlement.featureId),
40+
)
41+
}
42+
}
2343

2444
fun getEntitlements(organizationId: UUID): List<EntitlementResult> = entitlementClient.getEntitlements(organizationId)
2545

@@ -42,5 +62,11 @@ class EntitlementService(
4262
return clientResults.toMutableMap().apply { putAll(providerResults) }
4363
}
4464

65+
private fun hasDestinationObjectStorageEntitlement(organizationId: UUID): EntitlementResult =
66+
EntitlementResult(
67+
isEntitled = entitlementProvider.hasDestinationObjectStorageEntitlement(organizationId),
68+
featureId = DestinationObjectStorageEntitlement.featureId,
69+
)
70+
4571
internal fun hasConfigTemplateEntitlements(organizationId: UUID): Boolean = entitlementProvider.hasConfigTemplateEntitlements(organizationId)
4672
}

airbyte-commons-entitlements/src/main/kotlin/io/airbyte/commons/entitlements/models/EntitlementDefinitions.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@ object PlatformSubOneHourSyncFrequency : FeatureEntitlement(
1212
featureId = "feature-platform-sub-one-hour-sync-frequency",
1313
)
1414

15+
object DestinationObjectStorageEntitlement : FeatureEntitlement(
16+
featureId = "feature-destination-object-storage",
17+
)
18+
1519
object Entitlements {
1620
private val ALL =
1721
listOf(
1822
PlatformLlmSyncJobFailureExplanation,
1923
PlatformSubOneHourSyncFrequency,
24+
DestinationObjectStorageEntitlement,
2025
)
2126

2227
private val byId = ALL.associateBy { it.featureId }

airbyte-commons-entitlements/src/test/kotlin/io/airbyte/commons/entitlements/EntitlementProviderTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import io.airbyte.commons.license.AirbyteLicense
99
import io.airbyte.config.ActorType
1010
import io.airbyte.featureflag.AllowConfigTemplateEndpoints
1111
import io.airbyte.featureflag.DestinationDefinition
12+
import io.airbyte.featureflag.LicenseAllowDestinationObjectStorageConfig
1213
import io.airbyte.featureflag.LicenseAllowEnterpriseConnector
1314
import io.airbyte.featureflag.Multi
1415
import io.airbyte.featureflag.Organization
@@ -51,6 +52,13 @@ class EntitlementProviderTest {
5152
val res = entitlementProvider.hasConfigTemplateEntitlements(organizationId)
5253
assertFalse(res)
5354
}
55+
56+
@Test
57+
fun `test hasDestinationObjectStorageEntitlement`() {
58+
val organizationId = UUID.randomUUID()
59+
val res = entitlementProvider.hasDestinationObjectStorageEntitlement(organizationId)
60+
assertFalse(res)
61+
}
5462
}
5563

5664
@Nested
@@ -93,6 +101,13 @@ class EntitlementProviderTest {
93101
val res = entitlementProvider.hasConfigTemplateEntitlements(organizationId)
94102
assertEquals(res, license.isEmbedded)
95103
}
104+
105+
@Test
106+
fun `test hasDestinationObjectStorageEntitlement always returns true`() {
107+
val organizationId = UUID.randomUUID()
108+
val res = entitlementProvider.hasDestinationObjectStorageEntitlement(organizationId)
109+
assertEquals(true, res)
110+
}
96111
}
97112

98113
@Nested
@@ -132,6 +147,16 @@ class EntitlementProviderTest {
132147
assertEquals(res, isEntitled)
133148
}
134149

150+
@ParameterizedTest
151+
@ValueSource(booleans = [true, false])
152+
fun `test hasDestinationObjectStorageEntitlement returns value from feature flag`(isEntitled: Boolean) {
153+
val organizationId = UUID.randomUUID()
154+
every { featureFlagClient.boolVariation(LicenseAllowDestinationObjectStorageConfig, Organization(organizationId)) } returns isEntitled
155+
156+
val res = entitlementProvider.hasDestinationObjectStorageEntitlement(organizationId)
157+
assertEquals(res, isEntitled)
158+
}
159+
135160
private fun mockEntitledEnterpriseConnector(
136161
actorType: ActorType,
137162
organizationId: UUID,

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

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
import io.airbyte.data.services.DestinationService;
3030
import io.airbyte.data.services.OAuthService;
3131
import io.airbyte.data.services.SourceService;
32+
import io.airbyte.domain.models.EntitledConnectorSpec;
33+
import io.airbyte.domain.services.entitlements.ConnectorConfigEntitlementService;
34+
import io.airbyte.persistence.job.WorkspaceHelper;
3235
import io.airbyte.validation.json.JsonValidationException;
3336
import jakarta.inject.Singleton;
3437
import java.io.IOException;
@@ -50,17 +53,23 @@ public class ConnectorDefinitionSpecificationHandler {
5053
private final SourceService sourceService;
5154
private final DestinationService destinationService;
5255
private final OAuthService oAuthService;
56+
private final ConnectorConfigEntitlementService connectorConfigEntitlementService;
57+
private final WorkspaceHelper workspaceHelper;
5358

5459
public ConnectorDefinitionSpecificationHandler(final ActorDefinitionVersionHelper actorDefinitionVersionHelper,
5560
final JobConverter jobConverter,
5661
final SourceService sourceService,
5762
final DestinationService destinationService,
58-
final OAuthService oauthService) {
63+
final WorkspaceHelper workspaceHelper,
64+
final OAuthService oauthService,
65+
final ConnectorConfigEntitlementService connectorConfigEntitlementService) {
5966
this.actorDefinitionVersionHelper = actorDefinitionVersionHelper;
6067
this.jobConverter = jobConverter;
6168
this.sourceService = sourceService;
6269
this.destinationService = destinationService;
6370
this.oAuthService = oauthService;
71+
this.connectorConfigEntitlementService = connectorConfigEntitlementService;
72+
this.workspaceHelper = workspaceHelper;
6473
}
6574

6675
/**
@@ -78,9 +87,9 @@ public SourceDefinitionSpecificationRead getSpecificationForSourceId(final Sourc
7887
final StandardSourceDefinition sourceDefinition = sourceService.getStandardSourceDefinition(source.getSourceDefinitionId());
7988
final ActorDefinitionVersion sourceVersion =
8089
actorDefinitionVersionHelper.getSourceVersion(sourceDefinition, source.getWorkspaceId(), sourceIdRequestBody.getSourceId());
81-
final io.airbyte.protocol.models.v0.ConnectorSpecification spec = sourceVersion.getSpec();
82-
83-
return getSourceSpecificationRead(sourceDefinition, spec, source.getWorkspaceId());
90+
final EntitledConnectorSpec entitledConnectorSpec =
91+
connectorConfigEntitlementService.getEntitledConnectorSpec(source.getWorkspaceId(), sourceVersion);
92+
return getSourceSpecificationRead(sourceDefinition, entitledConnectorSpec, source.getWorkspaceId());
8493
}
8594

8695
/**
@@ -98,9 +107,10 @@ public SourceDefinitionSpecificationRead getSourceDefinitionSpecification(final
98107
final StandardSourceDefinition source = sourceService.getStandardSourceDefinition(sourceDefinitionId);
99108
final ActorDefinitionVersion sourceVersion =
100109
actorDefinitionVersionHelper.getSourceVersion(source, sourceDefinitionIdWithWorkspaceId.getWorkspaceId());
101-
final io.airbyte.protocol.models.v0.ConnectorSpecification spec = sourceVersion.getSpec();
102-
103-
return getSourceSpecificationRead(source, spec, sourceDefinitionIdWithWorkspaceId.getWorkspaceId());
110+
final UUID organizationId = workspaceHelper.getOrganizationForWorkspace(sourceDefinitionIdWithWorkspaceId.getWorkspaceId());
111+
final EntitledConnectorSpec entitledConnectorSpec =
112+
connectorConfigEntitlementService.getEntitledConnectorSpec(organizationId, sourceVersion);
113+
return getSourceSpecificationRead(source, entitledConnectorSpec, sourceDefinitionIdWithWorkspaceId.getWorkspaceId());
104114
}
105115

106116
/**
@@ -120,8 +130,11 @@ public DestinationDefinitionSpecificationRead getSpecificationForDestinationId(f
120130
final ActorDefinitionVersion destinationVersion =
121131
actorDefinitionVersionHelper.getDestinationVersion(destinationDefinition, destination.getWorkspaceId(),
122132
destinationIdRequestBody.getDestinationId());
123-
final io.airbyte.protocol.models.v0.ConnectorSpecification spec = destinationVersion.getSpec();
124-
return getDestinationSpecificationRead(destinationDefinition, spec, destinationVersion.getSupportsRefreshes(), destination.getWorkspaceId());
133+
final UUID organizationId = workspaceHelper.getOrganizationForWorkspace(destination.getWorkspaceId());
134+
final EntitledConnectorSpec entitledConnectorSpec =
135+
connectorConfigEntitlementService.getEntitledConnectorSpec(organizationId, destinationVersion);
136+
return getDestinationSpecificationRead(destinationDefinition, entitledConnectorSpec, destinationVersion.getSupportsRefreshes(),
137+
destination.getWorkspaceId());
125138
}
126139

127140
/**
@@ -141,17 +154,19 @@ public DestinationDefinitionSpecificationRead getDestinationSpecification(final
141154
final StandardDestinationDefinition destination = destinationService.getStandardDestinationDefinition(destinationDefinitionId);
142155
final ActorDefinitionVersion destinationVersion =
143156
actorDefinitionVersionHelper.getDestinationVersion(destination, destinationDefinitionIdWithWorkspaceId.getWorkspaceId());
144-
final io.airbyte.protocol.models.v0.ConnectorSpecification spec = destinationVersion.getSpec();
145-
146-
return getDestinationSpecificationRead(destination, spec, destinationVersion.getSupportsRefreshes(),
157+
final UUID organizationId = workspaceHelper.getOrganizationForWorkspace(destinationDefinitionIdWithWorkspaceId.getWorkspaceId());
158+
final EntitledConnectorSpec entitledConnectorSpec =
159+
connectorConfigEntitlementService.getEntitledConnectorSpec(organizationId, destinationVersion);
160+
return getDestinationSpecificationRead(destination, entitledConnectorSpec, destinationVersion.getSupportsRefreshes(),
147161
destinationDefinitionIdWithWorkspaceId.getWorkspaceId());
148162
}
149163

150164
@VisibleForTesting
151165
SourceDefinitionSpecificationRead getSourceSpecificationRead(final StandardSourceDefinition sourceDefinition,
152-
final io.airbyte.protocol.models.v0.ConnectorSpecification spec,
166+
final EntitledConnectorSpec entitledConnectorSpec,
153167
final UUID workspaceId)
154168
throws IOException {
169+
final io.airbyte.protocol.models.v0.ConnectorSpecification spec = entitledConnectorSpec.getSpec();
155170
final SourceDefinitionSpecificationRead specRead = new SourceDefinitionSpecificationRead()
156171
.jobInfo(jobConverter.getSynchronousJobRead(SynchronousJobMetadata.mock(JobConfig.ConfigType.GET_SPEC)))
157172
.connectionSpecification(spec.getConnectionSpecification())
@@ -174,10 +189,11 @@ SourceDefinitionSpecificationRead getSourceSpecificationRead(final StandardSourc
174189

175190
@VisibleForTesting
176191
DestinationDefinitionSpecificationRead getDestinationSpecificationRead(final StandardDestinationDefinition destinationDefinition,
177-
final io.airbyte.protocol.models.v0.ConnectorSpecification spec,
192+
final EntitledConnectorSpec entitledConnectorSpec,
178193
final boolean supportsRefreshes,
179194
final UUID workspaceId)
180195
throws IOException {
196+
final io.airbyte.protocol.models.v0.ConnectorSpecification spec = entitledConnectorSpec.getSpec();
181197
final DestinationDefinitionSpecificationRead specRead = new DestinationDefinitionSpecificationRead()
182198
.jobInfo(jobConverter.getSynchronousJobRead(SynchronousJobMetadata.mock(JobConfig.ConfigType.GET_SPEC)))
183199
.supportedDestinationSyncModes(getFinalDestinationSyncModes(spec.getSupportedDestinationSyncModes(), supportsRefreshes))

0 commit comments

Comments
 (0)