diff --git a/glide.lock b/glide.lock index 59b895d..9348887 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 586f06dac64626a980055d13a01b7e19966239a07ac7a1bdffd61c52b1e33219 -updated: 2018-01-16T15:48:55.20164-08:00 +hash: 76fa71a7a967d86b98a17d084d6ff4a1a572f96418730d4fad33c82ff5d282cc +updated: 2018-05-16T01:07:31.651399512-07:00 imports: - name: github.com/alecthomas/template version: a0175ee3bccc567396460bf5acd36800cb10c49c @@ -80,6 +80,10 @@ imports: version: fd36b3595eb2ec8da4b8153b107f7ea08504899d subpackages: - fs +- name: github.com/spf13/afero + version: 63644898a8da0bc22138abf860edaf5277b6102e + subpackages: + - mem - name: github.com/spf13/pflag version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7 - name: go.uber.org/atomic diff --git a/glide.yaml b/glide.yaml index ebc4a84..cb4122e 100644 --- a/glide.yaml +++ b/glide.yaml @@ -20,6 +20,8 @@ import: subpackages: - tools/clientcmd - tools/clientcmd/api +- package: github.com/spf13/afero + version: ^1.1.0 testImport: - package: github.com/go-test/deep version: v1.0.0 diff --git a/kuberos.go b/kuberos.go index fc72bd7..9fb95a9 100644 --- a/kuberos.go +++ b/kuberos.go @@ -4,16 +4,20 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" + "path/filepath" "github.com/negz/kuberos/extractor" oidc "github.com/coreos/go-oidc" "github.com/gorilla/schema" "github.com/pkg/errors" + "github.com/spf13/afero" "go.uber.org/zap" "golang.org/x/oauth2" + "k8s.io/api/core/v1" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" ) @@ -23,6 +27,9 @@ const ( // be redirected after authentication. DefaultKubeCfgEndpoint = "ui" + // DefaultAPITokenMountPath is the default mount path for API tokens + DefaultAPITokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount" + schemeHTTP = "http" schemeHTTPS = "https" @@ -67,6 +74,8 @@ var ( decoder = schema.NewDecoder() + appFs = afero.NewOsFs() + approvalConsent = oauth2.SetAuthURLParam("prompt", "consent") ) @@ -358,9 +367,26 @@ func populateUser(cfg *api.Config, p *extractor.OIDCAuthenticationParams) api.Co }, }, } + for name, cluster := range cfg.Clusters { + // If the cluster definition does not come with certificate-authority-data nor + // certificate-authority, then check if kuberos has access to the cluster's CA + // certificate and include it when possible. Assume all errors are non-fatal. + if len(cluster.CertificateAuthorityData) == 0 && cluster.CertificateAuthority == "" { + caPath := filepath.Join(DefaultAPITokenMountPath, v1.ServiceAccountRootCAKey) + if caFile, err := appFs.Open(caPath); err == nil { + if caCert, err := ioutil.ReadAll(caFile); err == nil { + cluster.CertificateAuthorityData = caCert + } + } else { + fmt.Printf("Error: %+v\n", err) + } + } c.Clusters[name] = cluster - c.Contexts[name] = &api.Context{Cluster: name, AuthInfo: p.Username} + c.Contexts[name] = &api.Context{ + Cluster: name, + AuthInfo: p.Username, + } } return c } diff --git a/kuberos_test.go b/kuberos_test.go index 37cfacc..23a9693 100644 --- a/kuberos_test.go +++ b/kuberos_test.go @@ -8,6 +8,7 @@ import ( oidc "github.com/coreos/go-oidc" "github.com/go-test/deep" + "github.com/spf13/afero" "golang.org/x/oauth2" "github.com/negz/kuberos/extractor" @@ -83,6 +84,7 @@ func TestPopulateUser(t *testing.T) { cases := []struct { name string cfg *api.Config + files map[string]string params *extractor.OIDCAuthenticationParams want api.Config }{ @@ -94,6 +96,7 @@ func TestPopulateUser(t *testing.T) { "b": &api.Cluster{Server: "https://example.net", CertificateAuthorityData: []byte("PAM")}, }, }, + files: map[string]string{}, params: &extractor.OIDCAuthenticationParams{ Username: "example@example.org", ClientID: "id", @@ -136,6 +139,7 @@ func TestPopulateUser(t *testing.T) { }, CurrentContext: "a", }, + files: map[string]string{}, params: &extractor.OIDCAuthenticationParams{ Username: "example@example.org", ClientID: "id", @@ -170,10 +174,97 @@ func TestPopulateUser(t *testing.T) { CurrentContext: "a", }, }, + { + name: "SingleClusterWithCAOnDisk", + cfg: &api.Config{ + Clusters: map[string]*api.Cluster{ + "a": &api.Cluster{Server: "https://example.org"}, + }, + }, + files: map[string]string{ + "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt": "PEM", + }, + params: &extractor.OIDCAuthenticationParams{ + Username: "example@example.org", + ClientID: "id", + ClientSecret: "secret", + IDToken: "token", + RefreshToken: "refresh", + IssuerURL: "https://example.org", + }, + want: api.Config{ + Clusters: map[string]*api.Cluster{ + "a": &api.Cluster{Server: "https://example.org", CertificateAuthorityData: []byte("PEM")}, + }, + Contexts: map[string]*api.Context{ + "a": &api.Context{AuthInfo: "example@example.org", Cluster: "a"}, + }, + AuthInfos: map[string]*api.AuthInfo{ + "example@example.org": &api.AuthInfo{ + AuthProvider: &api.AuthProviderConfig{ + Name: templateAuthProvider, + Config: map[string]string{ + templateOIDCClientID: "id", + templateOIDCClientSecret: "secret", + templateOIDCIDToken: "token", + templateOIDCRefreshToken: "refresh", + templateOIDCIssuer: "https://example.org", + }, + }, + }, + }, + }, + }, + { + name: "SingleClusterWithoutCA", + cfg: &api.Config{ + Clusters: map[string]*api.Cluster{ + "a": &api.Cluster{Server: "https://example.org"}, + }, + }, + files: map[string]string{}, + params: &extractor.OIDCAuthenticationParams{ + Username: "example@example.org", + ClientID: "id", + ClientSecret: "secret", + IDToken: "token", + RefreshToken: "refresh", + IssuerURL: "https://example.org", + }, + want: api.Config{ + Clusters: map[string]*api.Cluster{ + "a": &api.Cluster{Server: "https://example.org"}, + }, + Contexts: map[string]*api.Context{ + "a": &api.Context{AuthInfo: "example@example.org", Cluster: "a"}, + }, + AuthInfos: map[string]*api.AuthInfo{ + "example@example.org": &api.AuthInfo{ + AuthProvider: &api.AuthProviderConfig{ + Name: templateAuthProvider, + Config: map[string]string{ + templateOIDCClientID: "id", + templateOIDCClientSecret: "secret", + templateOIDCIDToken: "token", + templateOIDCRefreshToken: "refresh", + templateOIDCIssuer: "https://example.org", + }, + }, + }, + }, + }, + }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { + appFs = afero.NewMemMapFs() + for filename, content := range tt.files { + if err := afero.WriteFile(appFs, filename, []byte(content), 0644); err != nil { + t.Errorf("error writing file %q: %v", filename, err) + } + } + got := populateUser(tt.cfg, tt.params) if diff := deep.Equal(got, tt.want); diff != nil { t.Errorf("populateUser(...): got != want: %v", diff)