Skip to content

Commit ecd0d38

Browse files
authored
fix: Initialize auth managers in standalone lineage server (#6873)
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent fcddb9c commit ecd0d38

4 files changed

Lines changed: 245 additions & 53 deletions

File tree

‎.secrets.baseline‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎infra/feast-operator/internal/controller/services/repo_config.go‎

Lines changed: 86 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ func (feast *FeastServices) getServiceRepoConfig() (RepoConfig, error) {
5252
}
5353

5454
func (feast *FeastServices) getLineageFeatureStoreYamlBase64() (string, error) {
55-
repoConfig, err := feast.getLineageRepoConfig()
55+
odhCaBundleExists := feast.GetCustomCertificatesBundle().IsDefined
56+
repoConfig, err := feast.getLineageRepoConfig(odhCaBundleExists)
5657
if err != nil {
5758
return "", err
5859
}
@@ -63,7 +64,7 @@ func (feast *FeastServices) getLineageFeatureStoreYamlBase64() (string, error) {
6364
return base64.StdEncoding.EncodeToString(yamlBytes), nil
6465
}
6566

66-
func (feast *FeastServices) getLineageRepoConfig() (RepoConfig, error) {
67+
func (feast *FeastServices) getLineageRepoConfig(odhCaBundleExists bool) (RepoConfig, error) {
6768
cr := feast.Handler.FeatureStore
6869
applied := cr.Status.Applied
6970

@@ -108,6 +109,28 @@ func (feast *FeastServices) getLineageRepoConfig() (RepoConfig, error) {
108109
}
109110
}
110111

112+
// Apply auth configuration so the lineage server enforces the same
113+
// authentication as the other Feast servers. Without this the
114+
// standalone lineage endpoints would be unauthenticated even when
115+
// Kubernetes or OIDC auth is configured on the FeatureStore CR.
116+
if applied.AuthzConfig != nil && applied.AuthzConfig.OidcAuthz != nil {
117+
authzConfig, err := resolveOidcServerAuthzConfig(applied.AuthzConfig.OidcAuthz, feast.extractConfigFromSecret, odhCaBundleExists)
118+
if err != nil {
119+
return repoConfig, err
120+
}
121+
repoConfig.AuthzConfig = authzConfig
122+
} else if applied.AuthzConfig != nil {
123+
if applied.AuthzConfig.NoAuth != nil && *applied.AuthzConfig.NoAuth {
124+
repoConfig.AuthzConfig = AuthzConfig{Type: NoAuthAuthType}
125+
} else if applied.AuthzConfig.KubernetesAuthz != nil {
126+
repoConfig.AuthzConfig = AuthzConfig{Type: KubernetesAuthType}
127+
} else {
128+
return repoConfig, fmt.Errorf("authzConfig must specify OIDC, Kubernetes, or no-auth")
129+
}
130+
} else {
131+
repoConfig.AuthzConfig = defaultAuthzConfig
132+
}
133+
111134
return repoConfig, nil
112135
}
113136

@@ -190,61 +213,77 @@ func getBaseServiceRepoConfig(
190213
}
191214
appliedSpec := featureStore.Status.Applied
192215
if appliedSpec.AuthzConfig != nil && appliedSpec.AuthzConfig.OidcAuthz != nil {
193-
repoConfig.AuthzConfig = AuthzConfig{Type: OidcAuthType}
194-
oidcAuthz := appliedSpec.AuthzConfig.OidcAuthz
195-
oidcParameters := map[string]interface{}{}
196-
197-
var secretProperties map[string]interface{}
198-
if oidcAuthz.SecretRef != nil {
199-
var err error
200-
secretProperties, err = secretExtractionFunc("", oidcAuthz.SecretRef.Name, oidcAuthz.SecretKeyName)
201-
if err != nil {
202-
return repoConfig, err
203-
}
204-
for _, prop := range OidcOptionalSecretProperties {
205-
if val, exists := secretProperties[string(prop)]; exists {
206-
// Secret values are YAML-parsed on extraction, so an
207-
// all-digits audience or issuer arrives as an int and
208-
// would render unquoted, which the SDK's OidcAuthConfig
209-
// rejects (Optional[str]). Coerce the claim keys back to
210-
// strings; the five original keys keep their historical
211-
// typing.
212-
if prop == OidcAudience || prop == OidcIssuer {
213-
if _, isString := val.(string); !isString {
214-
val = fmt.Sprintf("%v", val)
215-
}
216-
}
217-
oidcParameters[string(prop)] = val
218-
}
219-
}
220-
}
221-
222-
discoveryUrl, err := resolveAuthDiscoveryUrl(oidcAuthz, secretProperties)
216+
authzConfig, err := resolveOidcServerAuthzConfig(appliedSpec.AuthzConfig.OidcAuthz, secretExtractionFunc, odhCaBundleExists)
223217
if err != nil {
224218
return repoConfig, err
225219
}
226-
oidcParameters[string(OidcAuthDiscoveryUrl)] = discoveryUrl
227-
228-
if oidcAuthz.VerifySSL != nil {
229-
oidcParameters[string(OidcVerifySsl)] = *oidcAuthz.VerifySSL
230-
}
231-
if oidcAuthz.JwksCacheLifespanSeconds != nil {
232-
oidcParameters[string(OidcJwksCacheLifespanSeconds)] = *oidcAuthz.JwksCacheLifespanSeconds
233-
}
234-
if oidcAuthz.JwksRequestTimeoutSeconds != nil {
235-
oidcParameters[string(OidcJwksRequestTimeoutSeconds)] = *oidcAuthz.JwksRequestTimeoutSeconds
236-
}
237-
if caCertPath := resolveOidcCACertPath(oidcAuthz, odhCaBundleExists); caCertPath != "" {
238-
oidcParameters[string(OidcCaCertPath)] = caCertPath
239-
}
240-
repoConfig.AuthzConfig.OidcParameters = oidcParameters
220+
repoConfig.AuthzConfig = authzConfig
241221
} else {
242222
repoConfig.AuthzConfig = clientRepoConfig.AuthzConfig
243223
}
244224

245225
return repoConfig, nil
246226
}
247227

228+
// resolveOidcServerAuthzConfig builds the server-side OIDC AuthzConfig from the
229+
// CR's OidcAuthz spec. Used by both getBaseServiceRepoConfig and
230+
// getLineageRepoConfig to avoid duplicating the secret extraction, discovery
231+
// URL resolution, and TLS/CA logic.
232+
func resolveOidcServerAuthzConfig(
233+
oidcAuthz *feastdevv1.OidcAuthz,
234+
secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error),
235+
odhCaBundleExists bool,
236+
) (AuthzConfig, error) {
237+
authzConfig := AuthzConfig{Type: OidcAuthType}
238+
oidcParameters := map[string]interface{}{}
239+
240+
var secretProperties map[string]interface{}
241+
if oidcAuthz.SecretRef != nil {
242+
var err error
243+
secretProperties, err = secretExtractionFunc("", oidcAuthz.SecretRef.Name, oidcAuthz.SecretKeyName)
244+
if err != nil {
245+
return authzConfig, err
246+
}
247+
for _, prop := range OidcOptionalSecretProperties {
248+
if val, exists := secretProperties[string(prop)]; exists {
249+
// Secret values are YAML-parsed on extraction, so an
250+
// all-digits audience or issuer arrives as an int and
251+
// would render unquoted, which the SDK's OidcAuthConfig
252+
// rejects (Optional[str]). Coerce the claim keys back to
253+
// strings; the five original keys keep their historical
254+
// typing.
255+
if prop == OidcAudience || prop == OidcIssuer {
256+
if _, isString := val.(string); !isString {
257+
val = fmt.Sprintf("%v", val)
258+
}
259+
}
260+
oidcParameters[string(prop)] = val
261+
}
262+
}
263+
}
264+
265+
discoveryUrl, err := resolveAuthDiscoveryUrl(oidcAuthz, secretProperties)
266+
if err != nil {
267+
return authzConfig, err
268+
}
269+
oidcParameters[string(OidcAuthDiscoveryUrl)] = discoveryUrl
270+
271+
if oidcAuthz.VerifySSL != nil {
272+
oidcParameters[string(OidcVerifySsl)] = *oidcAuthz.VerifySSL
273+
}
274+
if oidcAuthz.JwksCacheLifespanSeconds != nil {
275+
oidcParameters[string(OidcJwksCacheLifespanSeconds)] = *oidcAuthz.JwksCacheLifespanSeconds
276+
}
277+
if oidcAuthz.JwksRequestTimeoutSeconds != nil {
278+
oidcParameters[string(OidcJwksRequestTimeoutSeconds)] = *oidcAuthz.JwksRequestTimeoutSeconds
279+
}
280+
if caCertPath := resolveOidcCACertPath(oidcAuthz, odhCaBundleExists); caCertPath != "" {
281+
oidcParameters[string(OidcCaCertPath)] = caCertPath
282+
}
283+
authzConfig.OidcParameters = oidcParameters
284+
return authzConfig, nil
285+
}
286+
248287
// resolveAuthDiscoveryUrl determines the OIDC discovery URL from the first available source.
249288
// Priority: CR issuerUrl > Secret auth_discovery_url > OIDC_ISSUER_URL env var.
250289
func resolveAuthDiscoveryUrl(oidcAuthz *feastdevv1.OidcAuthz, secretProperties map[string]interface{}) (string, error) {

‎infra/feast-operator/internal/controller/services/repo_config_test.go‎

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,7 @@ var _ = Describe("Repo Config", func() {
845845
feast := FeastServices{
846846
Handler: handler.FeastHandler{FeatureStore: featureStore},
847847
}
848-
repoConfig, err := feast.getLineageRepoConfig()
848+
repoConfig, err := feast.getLineageRepoConfig(false)
849849
Expect(err).NotTo(HaveOccurred())
850850
Expect(repoConfig.Registry.RegistryType).To(Equal(RegistryRemoteConfigType))
851851
Expect(repoConfig.Registry.Path).To(Equal(remoteHost))
@@ -882,7 +882,7 @@ var _ = Describe("Repo Config", func() {
882882
feast := FeastServices{
883883
Handler: handler.FeastHandler{FeatureStore: featureStore},
884884
}
885-
repoConfig, err := feast.getLineageRepoConfig()
885+
repoConfig, err := feast.getLineageRepoConfig(false)
886886
Expect(err).NotTo(HaveOccurred())
887887
Expect(repoConfig.Registry.RegistryType).To(Equal(RegistryRemoteConfigType))
888888
Expect(repoConfig.Registry.Path).To(Equal(resolvedHost))
@@ -922,6 +922,125 @@ var _ = Describe("Repo Config", func() {
922922
Expect(err).NotTo(HaveOccurred())
923923
})
924924

925+
It("should include kubernetes auth config in lineage repo config", func() {
926+
featureStore := minimalFeatureStore()
927+
remoteHost := "feast-banking-registry.feast.svc.cluster.local:443"
928+
featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{
929+
Registry: &feastdevv1.Registry{
930+
Remote: &feastdevv1.RemoteRegistryConfig{
931+
Hostname: &remoteHost,
932+
},
933+
},
934+
}
935+
featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{
936+
KubernetesAuthz: &feastdevv1.KubernetesAuthz{},
937+
}
938+
featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{
939+
Enabled: true,
940+
Consumer: &feastdevv1.OpenLineageConsumerConfig{
941+
Enabled: true,
942+
LineageServer: &feastdevv1.LineageServerConfig{
943+
Replicas: ptr.To[int32](1),
944+
},
945+
},
946+
}
947+
ApplyDefaultsToStatus(featureStore)
948+
featureStore.Status.ServiceHostnames.Registry = remoteHost
949+
950+
feast := FeastServices{
951+
Handler: handler.FeastHandler{FeatureStore: featureStore},
952+
}
953+
repoConfig, err := feast.getLineageRepoConfig(false)
954+
Expect(err).NotTo(HaveOccurred())
955+
Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType))
956+
Expect(repoConfig.Registry.RegistryType).To(Equal(RegistryRemoteConfigType))
957+
Expect(repoConfig.Registry.Path).To(Equal(remoteHost))
958+
})
959+
960+
It("should default to kubernetes auth in lineage repo config when authzConfig is nil", func() {
961+
featureStore := minimalFeatureStore()
962+
featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{
963+
Enabled: true,
964+
Consumer: &feastdevv1.OpenLineageConsumerConfig{
965+
Enabled: true,
966+
LineageServer: &feastdevv1.LineageServerConfig{
967+
Replicas: ptr.To[int32](1),
968+
},
969+
},
970+
}
971+
ApplyDefaultsToStatus(featureStore)
972+
973+
feast := FeastServices{
974+
Handler: handler.FeastHandler{FeatureStore: featureStore},
975+
}
976+
repoConfig, err := feast.getLineageRepoConfig(false)
977+
Expect(err).NotTo(HaveOccurred())
978+
Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType))
979+
})
980+
981+
It("should include no_auth config in lineage repo config when NoAuth is set", func() {
982+
featureStore := minimalFeatureStore()
983+
featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{
984+
NoAuth: boolPtr(true),
985+
}
986+
featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{
987+
Enabled: true,
988+
Consumer: &feastdevv1.OpenLineageConsumerConfig{
989+
Enabled: true,
990+
LineageServer: &feastdevv1.LineageServerConfig{
991+
Replicas: ptr.To[int32](1),
992+
},
993+
},
994+
}
995+
ApplyDefaultsToStatus(featureStore)
996+
997+
feast := FeastServices{
998+
Handler: handler.FeastHandler{FeatureStore: featureStore},
999+
}
1000+
repoConfig, err := feast.getLineageRepoConfig(false)
1001+
Expect(err).NotTo(HaveOccurred())
1002+
Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType))
1003+
})
1004+
1005+
It("should include OIDC auth config in lineage repo config with issuerUrl", func() {
1006+
featureStore := minimalFeatureStore()
1007+
remoteHost := "feast-banking-registry.feast.svc.cluster.local:443"
1008+
featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{
1009+
Registry: &feastdevv1.Registry{
1010+
Remote: &feastdevv1.RemoteRegistryConfig{
1011+
Hostname: &remoteHost,
1012+
},
1013+
},
1014+
}
1015+
featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{
1016+
OidcAuthz: &feastdevv1.OidcAuthz{
1017+
IssuerUrl: "https://keycloak.example.com/realms/test",
1018+
},
1019+
}
1020+
featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{
1021+
Enabled: true,
1022+
Consumer: &feastdevv1.OpenLineageConsumerConfig{
1023+
Enabled: true,
1024+
LineageServer: &feastdevv1.LineageServerConfig{
1025+
Replicas: ptr.To[int32](1),
1026+
},
1027+
},
1028+
}
1029+
ApplyDefaultsToStatus(featureStore)
1030+
featureStore.Status.ServiceHostnames.Registry = remoteHost
1031+
1032+
feast := FeastServices{
1033+
Handler: handler.FeastHandler{FeatureStore: featureStore},
1034+
}
1035+
repoConfig, err := feast.getLineageRepoConfig(false)
1036+
Expect(err).NotTo(HaveOccurred())
1037+
Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType))
1038+
Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(
1039+
string(OidcAuthDiscoveryUrl),
1040+
"https://keycloak.example.com/realms/test/.well-known/openid-configuration",
1041+
))
1042+
})
1043+
9251044
It("should reject lineageServer without any registry when authz is set", func() {
9261045
featureStore := minimalFeatureStore()
9271046
featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{

0 commit comments

Comments
 (0)