Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: simplify token audiences config #947

Open
wants to merge 27 commits into
base: incubation
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5547a5a
tmp init cmt
cam-garrison Mar 27, 2024
518f888
add audience switching logic
cam-garrison Apr 1, 2024
941e71d
cleanup unnec comments
cam-garrison Apr 1, 2024
cea9305
restore get_all_mani
cam-garrison Apr 1, 2024
94d8626
Merge branch 'incubation' into automate-audiences
cam-garrison Apr 1, 2024
a5b609c
Update controllers/dscinitialization/servicemesh_setup.go
cam-garrison Apr 2, 2024
0b63d2d
Update main.go
cam-garrison Apr 3, 2024
1374f2a
inline audience checking, check token status
cam-garrison Apr 3, 2024
b156a34
Merge branch 'incubation' into automate-audiences
cam-garrison Apr 3, 2024
c99d6d7
fetch audiences outside of main, move label
cam-garrison Apr 3, 2024
5fd931a
move labels import
cam-garrison Apr 3, 2024
7365c32
add basic unit tests
cam-garrison Apr 4, 2024
0e20b23
lint
cam-garrison Apr 4, 2024
cae6062
do not runSpec twice
cam-garrison Apr 4, 2024
e38a8ad
refactor from review
cam-garrison Apr 8, 2024
bb1289e
Merge branch 'incubation' into automate-audiences
cam-garrison Apr 8, 2024
287b303
Merge branch 'incubation' into automate-audiences
bartoszmajsak Apr 9, 2024
0a32786
Update controllers/dscinitialization/servicemesh_test.go
cam-garrison Apr 9, 2024
e349c3c
move tests + helpers to cluster pkg, rework tests
cam-garrison Apr 9, 2024
9bb75de
remove fakeClient, stub not mock
cam-garrison Apr 10, 2024
adf2672
Merge branch 'incubation' into automate-audiences
cam-garrison Apr 10, 2024
ef204ab
Merge branch 'incubation' into automate-audiences
cam-garrison Apr 10, 2024
98c69c5
Update pkg/cluster/audiences_test.go
cam-garrison Apr 10, 2024
ae69939
fix typo, lint
cam-garrison Apr 10, 2024
fc544c5
Merge branch 'incubation' into automate-audiences
cam-garrison Apr 15, 2024
a27dba9
lint post merge
cam-garrison Apr 15, 2024
65887e4
Merge branch 'incubation' into automate-audiences
bartoszmajsak Apr 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions controllers/dscinitialization/servicemesh_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1"
"github.com/opendatahub-io/opendatahub-operator/v2/controllers/status"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/deploy"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/feature"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/feature/servicemesh"
Expand All @@ -18,7 +19,6 @@ import (
func (r *DSCInitializationReconciler) configureServiceMesh(instance *dsciv1.DSCInitialization) error {
switch instance.Spec.ServiceMesh.ManagementState {
case operatorv1.Managed:

capabilities := []*feature.HandlerWithReporter[*dsciv1.DSCInitialization]{
r.serviceMeshCapability(instance, serviceMeshCondition(status.ConfiguredReason, "Service Mesh configured")),
}
Expand Down Expand Up @@ -46,7 +46,6 @@ func (r *DSCInitializationReconciler) configureServiceMesh(instance *dsciv1.DSCI
return err
}
}

return nil
}

Expand Down Expand Up @@ -149,9 +148,11 @@ func (r *DSCInitializationReconciler) serviceMeshCapabilityFeatures(instance *ds
}
}

audiences := cluster.GetEffectiveClusterAudiences(r.Client, r.Log, handler.ServiceMesh.Auth.Audiences, cluster.GetSAToken)

cfgMapErr := feature.CreateFeature("mesh-shared-configmap").
For(handler).
WithResources(servicemesh.MeshRefs, servicemesh.AuthRefs).
WithResources(servicemesh.MeshRefs, servicemesh.AuthRefs(audiences)).
Load()
if cfgMapErr != nil {
return cfgMapErr
Expand Down
57 changes: 57 additions & 0 deletions pkg/cluster/audiences.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cluster

import (
"context"
"fmt"
"reflect"

"github.com/go-logr/logr"
authentication "k8s.io/api/authentication/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var (
// Default value of audiences for DSCI.SM.auth.
defaultAudiences = []string{"https://kubernetes.default.svc"}
)

func isDefaultAudiences(specAudiences *[]string) bool {
return specAudiences == nil || reflect.DeepEqual(*specAudiences, defaultAudiences)
}

type TokenSupplier func() (string, error)

// GetEffectiveClusterAudiences returns the audiences defined for the cluster
// falling back to the default audiences in case of errors.
func GetEffectiveClusterAudiences(cli client.Client, log logr.Logger, specAudiences *[]string, getTokenFunc TokenSupplier) []string {
if isDefaultAudiences(specAudiences) {
return fetchClusterAudiences(cli, log, getTokenFunc)
}
return *specAudiences
}

func fetchClusterAudiences(cli client.Client, log logr.Logger, getTokenFunc TokenSupplier) []string {
token, err := getTokenFunc()
if err != nil {
log.Error(err, "Error getting token, using default audiences")
return defaultAudiences
}

tokenReview := &authentication.TokenReview{
Spec: authentication.TokenReviewSpec{
Token: token,
},
}

if err = cli.Create(context.Background(), tokenReview, &client.CreateOptions{}); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to use the ServiceAccount mounted by the cluster, as a pivot to find the audientes.

I wonder if it isn't enough to parse the ServiceAccount token?
Just by parsing the one from CRC, I see the audiences claim in the payload:

  "aud": [
    "https://kubernetes.default.svc"
  ],

I'm not sure how it would be for ROSA.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, originally we were using opaque token for the bash "workaround" and this solution is based on this, so perhaps, using SA (jwt) token aud is simpler and more efficient. We could replace this impl and use https://github.com/golang-jwt/jwt instead to fetch standard claims without verification (so passing nil as the key func ). @israel-hdez @aslakknutsen do you think it's better to rely on the fact that SA token is JWT?

I just checked in ROSA and we can also parse JWT instead. For opaque (openshift oauth) tokens it's the same audience. We can get it by using the exact call to TokenReview API.

This behavior is only invoked when you do not specify anything in DSCI.ServiceMesh.Auth part. If you later do, it will update underlying config map which is now used by odh-model-controller

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@israel-hdez @aslakknutsen do you think it's better to rely on the fact that SA token is JWT?

AFAIK, core Kubernetes only supports JWT (read 1st sentence of this). So, I'd expect any SA token to be JWT. I understand we have control about the mounted/projected token so, as long as the setup does not change, we should be safe to assume it is a JWT.

About the opaque tokens, my understanding is that in core Kubernetes these are only supported when a 3rd party issues the token and the API Server would validate the token via a Webhook to the 3rd party (i.e. Kubernetes does not validate by its own). OpenShift's OAuth server may fall in this category.

log.Error(err, "Error creating TokenReview, using default audiences")
return defaultAudiences
}

if tokenReview.Status.Error != "" || !tokenReview.Status.Authenticated {
log.Error(fmt.Errorf(tokenReview.Status.Error), "Error with token review authentication status, using default audiences")
return defaultAudiences
}

return tokenReview.Status.Audiences
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading these docs: https://kubernetes.io/docs/reference/access-authn-authz/authentication/.

There is a note that the audiences field is optional and that, if not present, it may still mean the token is valid for the cluster. So, I'm not sure if this is safe.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Underlying AuthConfig will be defaulted if not configured specifically.

// If omitted, Authorino will review tokens expecting the host name of the requested protected service amongst the audiences.

Current implementation defaults it to "https://kubernetes.default.svc" if not specified.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the docs that I linked, there is this text over status.audiences of the TokenReview:

# If this is omitted, the token is considered to be valid to authenticate to the Kubernetes API server.

Underlying AuthConfig will be defaulted if not configured specifically.
Current implementation defaults it to "https://kubernetes.default.svc" if not specified.

IIRC, this default is on odh-model-controller side. This is OK.

My concern is that my understanding of the kubernetes docs is that the cluster may choose to omit the status.audiences field as an indication that the token is valid for the API Server. Yet, in that case, I see no statement that the audience would be https://kubernetes.default.svc in case the audiences field is omitted. Thus, we may wrongly assume it is https://kubernetes.default.svc.

The writing is somewhat confusing. So, I may be understanding things badly.

}
75 changes: 75 additions & 0 deletions pkg/cluster/audiences_test.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should stick to the correct terminology. We are providing canned responses, so this is stubbing, not mocking.

A bit more about it in this great blog post https://martinfowler.com/articles/mocksArentStubs.html

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package cluster_test

import (
"context"
"fmt"

"github.com/go-logr/logr"
authentication "k8s.io/api/authentication/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var defaultAudiences = []string{"https://kubernetes.default.svc"}

var _ = Describe("Handling Token Audiences", func() {
var fakeClient client.Client
var logger logr.Logger

BeforeEach(func() {
fakeClient = fake.NewClientBuilder().WithScheme(testScheme).Build()
logger = logr.Logger{}
})

Context("Determining the effective cluster audiences", func() {
When("non-default audiences are provided", func() {
It("should return the provided audiences", func() {
specAudiences := []string{"https://example.com"}
Expect(cluster.GetEffectiveClusterAudiences(fakeClient, logger, &specAudiences, cluster.GetSAToken)).To(Equal(specAudiences))
})
})

When("non-default audiences are fetched successfully", func() {
It("should return the fetched audiences", func() {
mockAudiences := []string{"https://mock.audience.com"}
mockGetSAToken := func() (string, error) {
return "mock_token", nil
}

baseClient := fake.NewClientBuilder().WithScheme(testScheme).Build()
fakeClient := &mockAudiencesClient{Client: baseClient, mockAudiences: mockAudiences}

Expect(cluster.GetEffectiveClusterAudiences(fakeClient, logger, nil, mockGetSAToken)).To(Equal(mockAudiences))
})
})

When("an error occurs while fetching the audiences", func() {
It("should return the default audiences", func() {
mockFailGettingToken := func() (string, error) {
return "", fmt.Errorf("we failed getting token")
}

Expect(cluster.GetEffectiveClusterAudiences(fakeClient, logger, nil, mockFailGettingToken)).To(Equal(defaultAudiences))
})
})
})
})

type mockAudiencesClient struct {
client.Client
mockAudiences []string
}

func (mac *mockAudiencesClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
if tokenReview, isTokenReview := obj.(*authentication.TokenReview); isTokenReview {
tokenReview.Status.Audiences = mac.mockAudiences
tokenReview.Status.Authenticated = true
return nil
}
return mac.Client.Create(ctx, obj, opts...)
}
bartoszmajsak marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions pkg/cluster/cluster_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ func GetOperatorNamespace() (string, error) {
return string(data), err
}

func GetSAToken() (string, error) {
data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
return string(data), err
}

// GetClusterServiceVersion retries the clusterserviceversions available in the operator namespace.
func GetClusterServiceVersion(ctx context.Context, c client.Client, watchNameSpace string) (*ofapi.ClusterServiceVersion, error) {
clusterServiceVersionList := &ofapi.ClusterServiceVersionList{}
Expand Down
45 changes: 24 additions & 21 deletions pkg/feature/servicemesh/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/feature"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels"
)

// MeshRefs stores service mesh configuration in the config map, so it can
Expand All @@ -31,26 +32,28 @@ func MeshRefs(f *feature.Feature) error {

// AuthRefs stores authorization configuration in the config map, so it can
// be easily accessed by other components which rely on this information.
func AuthRefs(f *feature.Feature) error {
audiences := f.Spec.Auth.Audiences
namespace := f.Spec.AppNamespace
audiencesList := ""
if audiences != nil && len(*audiences) > 0 {
audiencesList = strings.Join(*audiences, ",")
}
data := map[string]string{
"AUTH_AUDIENCE": audiencesList,
"AUTH_PROVIDER": namespace + "-auth-provider",
"AUTHORINO_LABEL": "security.opendatahub.io/authorization-group=default",
func AuthRefs(audiences []string) feature.Action {
return func(f *feature.Feature) error {
namespace := f.Spec.AppNamespace

audiencesList := ""
if len(audiences) > 0 {
audiencesList = strings.Join(audiences, ",")
}
data := map[string]string{
"AUTH_AUDIENCE": audiencesList,
"AUTH_PROVIDER": namespace + "-auth-provider",
"AUTHORINO_LABEL": labels.ODH.AuthorizationGroup("default"),
}

_, err := cluster.CreateOrUpdateConfigMap(
f.Client,
"auth-refs",
namespace,
data,
feature.OwnedBy(f),
)

return err
}

_, err := cluster.CreateOrUpdateConfigMap(
f.Client,
"auth-refs",
namespace,
data,
feature.OwnedBy(f),
)

return err
}
7 changes: 5 additions & 2 deletions pkg/metadata/labels/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package labels

const ODHAppPrefix = "app.opendatahub.io"
const ODHSecurityPrefix = "security.opendatahub.io"

// K8SCommon keeps common kubernetes labels [1]
// used across the project.
Expand All @@ -13,11 +14,13 @@ var K8SCommon = struct {

// ODH holds Open Data Hub specific labels grouped by types.
var ODH = struct {
OwnedNamespace string
Component func(string) string
OwnedNamespace string
Component func(string) string
AuthorizationGroup func(string) string
}{
OwnedNamespace: "opendatahub.io/generated-namespace",
Component: func(name string) string {
return ODHAppPrefix + "/" + name
},
AuthorizationGroup: func(group string) string { return ODHSecurityPrefix + "/authorization-group=" + group },
}