Skip to content

Commit

Permalink
Merge pull request #3611 from weaveworks/add-oidc-prefix-support-for-…
Browse files Browse the repository at this point in the history
…impersonation

Add OIDC prefix support for impersonation
  • Loading branch information
sarataha authored Apr 19, 2023
2 parents f7d8257 + 728f260 commit 5cd78d7
Show file tree
Hide file tree
Showing 15 changed files with 141 additions and 53 deletions.
21 changes: 20 additions & 1 deletion cmd/gitops-server/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ func NewCommand() *cobra.Command {
cmd.Flags().StringVar(&options.OIDC.ClaimsConfig.Username, "oidc-username-claim", auth.ClaimUsername, "JWT claim to use as the user name. By default email, which is expected to be a unique identifier of the end user. Admins can choose other claims, such as sub or name, depending on their provider")
cmd.Flags().StringVar(&options.OIDC.ClaimsConfig.Groups, "oidc-groups-claim", auth.ClaimGroups, "JWT claim to use as the user's group. If the claim is present it must be an array of strings")
cmd.Flags().StringSliceVar(&options.OIDC.Scopes, "custom-oidc-scopes", auth.DefaultScopes, "Customise the requested scopes for then OIDC authentication flow - openid will always be requested")
// OIDC prefixes
cmd.Flags().StringVar(&options.OIDC.UsernamePrefix, "oidc-username-prefix", "", "Prefix to add to the username when impersonating")
cmd.Flags().StringVar(&options.OIDC.GroupsPrefix, "oidc-group-prefix", "", "Prefix to add to the groups when impersonating")

// Metrics
cmd.Flags().BoolVar(&options.EnableMetrics, "enable-metrics", false, "Starts the metrics listener")
cmd.Flags().StringVar(&options.MetricsAddress, "metrics-address", ":2112", "If the metrics listener is enabled, bind to this address")
Expand Down Expand Up @@ -187,7 +191,22 @@ func runCmd(cmd *cobra.Command, args []string) error {

ctx := context.Background()

cl, err := cluster.NewSingleCluster(cluster.DefaultCluster, rest, scheme, cluster.DefaultKubeConfigOptions...)
oidcPrefixes := kube.UserPrefixes{
UsernamePrefix: options.OIDC.UsernamePrefix,
GroupsPrefix: options.OIDC.GroupsPrefix,
}

// Incorporate values from authServer.AuthConfig.OIDCConfig
if authServer.AuthConfig.OIDCConfig.UsernamePrefix != "" {
log.V(logger.LogLevelWarn).Info("OIDC username prefix configured by both CLI and secret. Secret values will take precedence.")
oidcPrefixes.UsernamePrefix = authServer.AuthConfig.OIDCConfig.UsernamePrefix
}
if authServer.AuthConfig.OIDCConfig.GroupsPrefix != "" {
log.V(logger.LogLevelWarn).Info("OIDC groups prefix configured by both CLI and secret. Secret values will take precedence.")
oidcPrefixes.GroupsPrefix = authServer.AuthConfig.OIDCConfig.GroupsPrefix
}

cl, err := cluster.NewSingleCluster(cluster.DefaultCluster, rest, scheme, oidcPrefixes, cluster.DefaultKubeConfigOptions...)
if err != nil {
return fmt.Errorf("failed to create cluster client; %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion core/clustersmngr/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ func createClusterClientsPool(g *GomegaWithT, clusterName string) clustersmngr.C
})
g.Expect(err).To(BeNil())

cluster, err := cluster.NewSingleCluster(clusterName, k8sEnv.Rest, scheme)
cluster, err := cluster.NewSingleCluster(clusterName, k8sEnv.Rest, scheme, kube.UserPrefixes{})
g.Expect(err).To(BeNil())
err = clientsPool.Add(
client,
Expand Down
24 changes: 13 additions & 11 deletions core/clustersmngr/cluster/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import (
)

type singleCluster struct {
name string
restConfig *rest.Config
scheme *apiruntime.Scheme
name string
restConfig *rest.Config
scheme *apiruntime.Scheme
userPrefixes kube.UserPrefixes
}

func NewSingleCluster(name string, config *rest.Config, scheme *apiruntime.Scheme, kubeConfigOptions ...KubeConfigOption) (Cluster, error) {
func NewSingleCluster(name string, config *rest.Config, scheme *apiruntime.Scheme, userPrefixes kube.UserPrefixes, kubeConfigOptions ...KubeConfigOption) (Cluster, error) {
// TODO: why does the cluster care about options?
config.Timeout = kubeClientTimeout
config.Dial = (&net.Dialer{
Expand All @@ -38,9 +39,10 @@ func NewSingleCluster(name string, config *rest.Config, scheme *apiruntime.Schem
}

return &singleCluster{
name: name,
restConfig: config,
scheme: scheme,
name: name,
restConfig: config,
scheme: scheme,
userPrefixes: userPrefixes,
}, nil
}

Expand Down Expand Up @@ -69,16 +71,16 @@ func getClientFromConfig(config *rest.Config, scheme *apiruntime.Scheme) (client
return client, nil
}

func getImpersonatedConfig(config *rest.Config, user *auth.UserPrincipal) (*rest.Config, error) {
func getImpersonatedConfig(config *rest.Config, user *auth.UserPrincipal, userPrefixes kube.UserPrefixes) (*rest.Config, error) {
if !user.Valid() {
return nil, fmt.Errorf("no user ID or Token found in UserPrincipal")
}

return kube.ConfigWithPrincipal(user, config), nil
return kube.ConfigWithPrincipal(user, config, userPrefixes), nil
}

func (c *singleCluster) GetUserClient(user *auth.UserPrincipal) (client.Client, error) {
cfg, err := getImpersonatedConfig(c.restConfig, user)
cfg, err := getImpersonatedConfig(c.restConfig, user, c.userPrefixes)
if err != nil {
return nil, err
}
Expand All @@ -101,7 +103,7 @@ func (c *singleCluster) GetServerClient() (client.Client, error) {
}

func (c *singleCluster) GetUserClientset(user *auth.UserPrincipal) (kubernetes.Interface, error) {
cfg, err := getImpersonatedConfig(c.restConfig, user)
cfg, err := getImpersonatedConfig(c.restConfig, user, c.userPrefixes)
if err != nil {
return nil, err
}
Expand Down
7 changes: 4 additions & 3 deletions core/clustersmngr/cluster/single_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

. "github.com/onsi/gomega"
"github.com/weaveworks/weave-gitops/pkg/kube"
"github.com/weaveworks/weave-gitops/pkg/server/auth"
"github.com/weaveworks/weave-gitops/pkg/testutils"
"k8s.io/apimachinery/pkg/util/rand"
Expand All @@ -19,7 +20,7 @@ func TestSingleCluster(t *testing.T) {

g := NewGomegaWithT(t)

cluster, err := NewSingleCluster("Default", config, nil)
cluster, err := NewSingleCluster("Default", config, nil, kube.UserPrefixes{})
g.Expect(err).To(BeNil())

g.Expect(cluster.GetName()).To(Equal("Default"))
Expand Down Expand Up @@ -77,9 +78,9 @@ func TestClientConfigWithUser(t *testing.T) {
// Set up
clusterName := fmt.Sprintf("clustersmngr-test-%d-%s", idx, rand.String(5))

cluster, err := NewSingleCluster(clusterName, k8sEnv.Rest, nil)
cluster, err := NewSingleCluster(clusterName, k8sEnv.Rest, nil, kube.UserPrefixes{})
g.Expect(err).NotTo(HaveOccurred())
res, err := getImpersonatedConfig(cluster.(*singleCluster).restConfig, tt.principal)
res, err := getImpersonatedConfig(cluster.(*singleCluster).restConfig, tt.principal, kube.UserPrefixes{})

// Test
if tt.expectedErr != nil {
Expand Down
9 changes: 5 additions & 4 deletions core/clustersmngr/factory_caches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
. "github.com/onsi/gomega"
"github.com/weaveworks/weave-gitops/core/clustersmngr"
"github.com/weaveworks/weave-gitops/core/clustersmngr/cluster"
"github.com/weaveworks/weave-gitops/pkg/kube"
"github.com/weaveworks/weave-gitops/pkg/server/auth"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/rest"
Expand All @@ -36,7 +37,7 @@ func TestUsersNamespaces(t *testing.T) {
})

t.Run("all namespaces from all", func(t *testing.T) {
cl, err := cluster.NewSingleCluster(clusterName, &rest.Config{}, nil)
cl, err := cluster.NewSingleCluster(clusterName, &rest.Config{}, nil, kube.UserPrefixes{})
g.Expect(err).NotTo(HaveOccurred())
nsMap := un.GetAll(user, []cluster.Cluster{cl})
g.Expect(nsMap).To(Equal(map[string][]v1.Namespace{clusterName: {ns}}))
Expand All @@ -51,10 +52,10 @@ func TestClusters(t *testing.T) {
c1 := "cluster-1"
c2 := "cluster-2"

cluster1, err := cluster.NewSingleCluster(c1, &rest.Config{}, nil)
cluster1, err := cluster.NewSingleCluster(c1, &rest.Config{}, nil, kube.UserPrefixes{})
g.Expect(err).NotTo(HaveOccurred())

cluster2, err := cluster.NewSingleCluster(c2, &rest.Config{}, nil)
cluster2, err := cluster.NewSingleCluster(c2, &rest.Config{}, nil, kube.UserPrefixes{})
g.Expect(err).NotTo(HaveOccurred())

testClusters := []cluster.Cluster{cluster1, cluster2}
Expand Down Expand Up @@ -127,7 +128,7 @@ func TestClusterSet_Set(t *testing.T) {
}

func newTestCluster(t *testing.T, name, server string) cluster.Cluster {
c, err := cluster.NewSingleCluster(name, &rest.Config{Host: server}, nil)
c, err := cluster.NewSingleCluster(name, &rest.Config{Host: server}, nil, kube.UserPrefixes{})
if err != nil {
t.Error("Expected error to be nil, got", err)
}
Expand Down
7 changes: 4 additions & 3 deletions core/clustersmngr/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/weaveworks/weave-gitops/core/nsaccess"
"github.com/weaveworks/weave-gitops/core/nsaccess/nsaccessfakes"
"github.com/weaveworks/weave-gitops/pkg/featureflags"
"github.com/weaveworks/weave-gitops/pkg/kube"
"github.com/weaveworks/weave-gitops/pkg/server/auth"
"golang.org/x/net/context"
v1 "k8s.io/api/core/v1"
Expand All @@ -33,7 +34,7 @@ func TestGetImpersonatedClient(t *testing.T) {
nsChecker := &nsaccessfakes.FakeChecker{}
nsChecker.FilterAccessibleNamespacesReturns([]v1.Namespace{*ns2}, nil)

cluster, err := cluster.NewSingleCluster("test", k8sEnv.Rest, nil, cluster.DefaultKubeConfigOptions...)
cluster, err := cluster.NewSingleCluster("test", k8sEnv.Rest, nil, kube.UserPrefixes{}, cluster.DefaultKubeConfigOptions...)
g.Expect(err).To(BeNil())

clustersFetcher := fetcher.NewSingleClusterFetcher(cluster)
Expand Down Expand Up @@ -84,7 +85,7 @@ func TestUseUserClientForNamespaces(t *testing.T) {
nsChecker := &nsaccessfakes.FakeChecker{}
nsChecker.FilterAccessibleNamespacesReturns([]v1.Namespace{*ns2}, nil)

cluster, err := cluster.NewSingleCluster("test", k8sEnv.Rest, nil, cluster.DefaultKubeConfigOptions...)
cluster, err := cluster.NewSingleCluster("test", k8sEnv.Rest, nil, kube.UserPrefixes{}, cluster.DefaultKubeConfigOptions...)
g.Expect(err).To(BeNil())

clustersFetcher := fetcher.NewSingleClusterFetcher(cluster)
Expand Down Expand Up @@ -134,7 +135,7 @@ func TestGetImpersonatedDiscoveryClient(t *testing.T) {
nsChecker := &nsaccessfakes.FakeChecker{}
nsChecker.FilterAccessibleNamespacesReturns([]v1.Namespace{*ns1}, nil)

cl, err := cluster.NewSingleCluster(cluster.DefaultCluster, k8sEnv.Rest, nil, cluster.DefaultKubeConfigOptions...)
cl, err := cluster.NewSingleCluster(cluster.DefaultCluster, k8sEnv.Rest, nil, kube.UserPrefixes{}, cluster.DefaultKubeConfigOptions...)
g.Expect(err).To(BeNil())

clustersFetcher := fetcher.NewSingleClusterFetcher(cl)
Expand Down
3 changes: 2 additions & 1 deletion core/clustersmngr/fetcher/single_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
. "github.com/onsi/gomega"
"github.com/weaveworks/weave-gitops/core/clustersmngr/cluster"
"github.com/weaveworks/weave-gitops/core/clustersmngr/fetcher"
"github.com/weaveworks/weave-gitops/pkg/kube"
"k8s.io/client-go/rest"
)

Expand All @@ -18,7 +19,7 @@ func TestSingleFetcher(t *testing.T) {

g := NewGomegaWithT(t)

cluster, err := cluster.NewSingleCluster("Default", config, nil)
cluster, err := cluster.NewSingleCluster("Default", config, nil, kube.UserPrefixes{})
g.Expect(err).To(BeNil())

fetcher := fetcher.NewSingleClusterFetcher(cluster)
Expand Down
5 changes: 3 additions & 2 deletions core/clustersmngr/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/weaveworks/weave-gitops/core/clustersmngr/cluster"
"github.com/weaveworks/weave-gitops/pkg/kube"
"github.com/weaveworks/weave-gitops/pkg/testutils"
"k8s.io/client-go/rest"
)
Expand All @@ -30,7 +31,7 @@ func TestMain(m *testing.M) {
}

func makeLeafCluster(t *testing.T, name string) cluster.Cluster {
cluster, err := cluster.NewSingleCluster(name, k8sEnv.Rest, nil)
cluster, err := cluster.NewSingleCluster(name, k8sEnv.Rest, nil, kube.UserPrefixes{})
if err != nil {
t.Error("Expected err to be nil, got", err)
}
Expand All @@ -45,7 +46,7 @@ func makeUnreachableLeafCluster(t *testing.T, name string) cluster.Cluster {
// FIXME: better addresses?
c.Host = "0.0.0.0:65535"

cluster, err := cluster.NewSingleCluster(name, c, nil)
cluster, err := cluster.NewSingleCluster(name, c, nil, kube.UserPrefixes{})
if err != nil {
t.Error("Expected err to be nil, got", err)
}
Expand Down
2 changes: 1 addition & 1 deletion core/nsaccess/nsaccess_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ func newRestConfigWithRole(t *testing.T, testCfg *rest.Config, roleName types.Na
t.Fatal(err)
}

cluster, err := cluster.NewSingleCluster("test", testCfg, scheme)
cluster, err := cluster.NewSingleCluster("test", testCfg, scheme, kube.UserPrefixes{})
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion core/server/featureflags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestGetFeatureFlags(t *testing.T) {
t.Fatal(err)
}

cluster, err := cluster.NewSingleCluster("Default", k8sEnv.Rest, scheme)
cluster, err := cluster.NewSingleCluster("Default", k8sEnv.Rest, scheme, kube.UserPrefixes{})
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion core/server/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func makeGRPCServer(cfg *rest.Config, t *testing.T) pb.CoreClient {
t.Fatal(err)
}

cluster, err := cluster.NewSingleCluster("Default", k8sEnv.Rest, scheme)
cluster, err := cluster.NewSingleCluster("Default", k8sEnv.Rest, scheme, kube.UserPrefixes{})
if err != nil {
t.Fatal(err)
}
Expand Down
39 changes: 31 additions & 8 deletions pkg/kube/config_getter.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import (
"k8s.io/client-go/rest"
)

// UserPrefixes contains the prefixes for the user and groups
// that will be used when impersonating a user.
type UserPrefixes struct {
UsernamePrefix string
GroupsPrefix string
}

// ConfigGetter implementations should extract the details from a context and
// create a *rest.Config for use in clients.
type ConfigGetter interface {
Expand All @@ -20,14 +27,15 @@ var _ ConfigGetter = &ImpersonatingConfigGetter{}
// principal and if it finds one, it configures the *rest.Config to impersonate
// that principal. Otherwise it returns a copy of the base config.
type ImpersonatingConfigGetter struct {
insecure bool
cfg *rest.Config
insecure bool
cfg *rest.Config
userPrefixes UserPrefixes
}

// NewImpersonatingConfigGetter creates and returns a ConfigGetter with a known
// config.
func NewImpersonatingConfigGetter(cfg *rest.Config, insecure bool) *ImpersonatingConfigGetter {
return &ImpersonatingConfigGetter{cfg: cfg, insecure: insecure}
func NewImpersonatingConfigGetter(cfg *rest.Config, insecure bool, userPrefixes UserPrefixes) *ImpersonatingConfigGetter {
return &ImpersonatingConfigGetter{cfg: cfg, insecure: insecure, userPrefixes: userPrefixes}
}

// Config returns a *rest.Config configured to impersonate a user or
Expand All @@ -36,7 +44,7 @@ func (r *ImpersonatingConfigGetter) Config(ctx context.Context) *rest.Config {
cfg := rest.CopyConfig(r.cfg)

if p := auth.Principal(ctx); p != nil {
cfg = ConfigWithPrincipal(p, cfg)
cfg = ConfigWithPrincipal(p, cfg, r.userPrefixes)
}

if r.insecure {
Expand All @@ -50,19 +58,34 @@ func (r *ImpersonatingConfigGetter) Config(ctx context.Context) *rest.Config {

// ConfigWithPrincipal returns a new config with the principal set as the
// impersonated user or bearer token.
func ConfigWithPrincipal(user *auth.UserPrincipal, config *rest.Config) *rest.Config {
func ConfigWithPrincipal(user *auth.UserPrincipal, config *rest.Config, userPrefixes UserPrefixes) *rest.Config {
cfg := rest.CopyConfig(config)

if tok := user.Token(); tok != "" {
cfg.BearerToken = tok
// Clear the token file as it takes precedence over the token.
cfg.BearerTokenFile = ""
} else {
prefixedGroups := user.Groups
if prefixedGroups != nil {
prefixedGroups = addGroupsPrefix(user.Groups, userPrefixes.GroupsPrefix)
}

cfg.Impersonate = rest.ImpersonationConfig{
UserName: user.ID,
Groups: user.Groups,
UserName: userPrefixes.UsernamePrefix + user.ID,
Groups: prefixedGroups,
}
}

return cfg
}

func addGroupsPrefix(groups []string, prefix string) []string {
prefixedGroups := make([]string, len(groups))

for i, group := range groups {
prefixedGroups[i] = prefix + group
}

return prefixedGroups
}
Loading

0 comments on commit 5cd78d7

Please sign in to comment.