Skip to content

Commit

Permalink
feat: support kubernetes token review
Browse files Browse the repository at this point in the history
  • Loading branch information
shaj13 committed Aug 22, 2020
1 parent 091444e commit b4f3bb0
Show file tree
Hide file tree
Showing 12 changed files with 531 additions and 0 deletions.
41 changes: 41 additions & 0 deletions auth/strategies/kubernetes/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package kubernetes

import (
"fmt"
"net/http"

"github.com/shaj13/go-guardian/auth/strategies/token"
"github.com/shaj13/go-guardian/store"
)

func ExampleNew() {
cache := store.New(2)
kube := New(cache)
r, _ := http.NewRequest("", "/", nil)
_, err := kube.Authenticate(r.Context(), r)
fmt.Println(err != nil)
// Output:
// true
}

func ExampleGetAuthenticateFunc() {
cache := store.New(2)
fn := GetAuthenticateFunc()
kube := token.New(fn, cache)
r, _ := http.NewRequest("", "/", nil)
_, err := kube.Authenticate(r.Context(), r)
fmt.Println(err != nil)
// Output:
// true
}

func Example() {
st := SetServiceAccountToken("Service Account Token")
cache := store.New(2)
kube := New(cache, st)
r, _ := http.NewRequest("", "/", nil)
_, err := kube.Authenticate(r.Context(), r)
fmt.Println(err != nil)
// Output:
// true
}
134 changes: 134 additions & 0 deletions auth/strategies/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Package kubernetes provide auth strategy to authenticate,
// incoming HTTP requests using a Kubernetes Service Account Token.
// This authentication strategy makes it easy to introduce apps,
// into a Kubernetes Pod and make Pod authenticate Pod.
package kubernetes

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"

kubeauth "k8s.io/api/authentication/v1"
kubemeta "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/shaj13/go-guardian/auth"
"github.com/shaj13/go-guardian/auth/strategies/token"
"github.com/shaj13/go-guardian/store"
)

type kubeReview struct {
addr string
// service account token
token string
apiVersion string
audiences []string
client *http.Client
}

func (k *kubeReview) authenticate(ctx context.Context, r *http.Request, token string) (auth.Info, error) {
tr := &kubeauth.TokenReview{
Spec: kubeauth.TokenReviewSpec{
Token: token,
Audiences: k.audiences,
},
}

body, err := json.Marshal(tr)
if err != nil {
return nil, fmt.Errorf(
"strategies/kubernetes: Failed to Marshal TokenReview Err: %s",
err,
)
}

url := k.addr + "/apis/" + k.apiVersion + "/tokenreviews"

req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return nil, err
}

req.Header.Set("Authorization", "Bearer "+k.token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

resp, err := k.client.Do(req)
if err != nil {
return nil, err
}

body, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

defer resp.Body.Close()

// verify the response is not an kubernetes status error.
status := &kubemeta.Status{}
err = json.Unmarshal(body, status)
if err == nil && status.Status != kubemeta.StatusSuccess {
return nil, fmt.Errorf("strategies/kubernetes: %s", status.Message)
}

tr = &kubeauth.TokenReview{}
err = json.Unmarshal(body, tr)
if err != nil {
return nil, fmt.Errorf(
"strategies/kubernetes: Failed to Unmarshal Response body to TokenReview Err: %s",
err,
)
}

if len(tr.Status.Error) > 0 {
return nil, fmt.Errorf("strategies/kubernetes: %s", tr.Status.Error)
}

if !tr.Status.Authenticated {
return nil, fmt.Errorf("strategies/kubernetes: Token Unauthorized")
}

user := tr.Status.User
extensions := make(map[string][]string)
for k, v := range user.Extra {
extensions[k] = v
}

return auth.NewUserInfo(user.Username, user.UID, user.Groups, extensions), nil
}

// GetAuthenticateFunc return function to authenticate request using kubernetes token review.
// The returned function typically used with the token strategy.
func GetAuthenticateFunc(opts ...auth.Option) token.AuthenticateFunc {
return newKubeReview(opts...).authenticate
}

// New return strategy authenticate request using kubernetes token review.
// New is similar to token.New().
func New(c store.Cache, opts ...auth.Option) auth.Strategy {
fn := GetAuthenticateFunc(opts...)
return token.New(fn, c, opts...)
}

func newKubeReview(opts ...auth.Option) *kubeReview {
kr := &kubeReview{
addr: "http://127.0.0.1:6443",
apiVersion: "authentication.k8s.io/v1",
client: &http.Client{
Transport: &http.Transport{},
},
}

for _, opt := range opts {
opt.Apply(kr)
}

kr.addr = strings.TrimSuffix(kr.addr, "/")
kr.apiVersion = strings.TrimPrefix(strings.TrimSuffix(kr.apiVersion, "/"), "/")
return kr
}
101 changes: 101 additions & 0 deletions auth/strategies/kubernetes/kubernetes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//nolint: lll
package kubernetes

import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"

"github.com/shaj13/go-guardian/auth"
)

func TestNewKubeReview(t *testing.T) {
// Round #1 -- check default
kr := newKubeReview()
assert.NotNil(t, kr.client)
assert.NotNil(t, kr.client.Transport)
assert.Equal(t, kr.apiVersion, "authentication.k8s.io/v1")
assert.Equal(t, kr.addr, "http://127.0.0.1:6443")

// Round #2 -- apply opt and trim "/"
ver := SetAPIVersion("/test/v1/")
addr := SetAddress("http://127.0.0.1:8080/")
kr = newKubeReview(ver, addr)
assert.Equal(t, kr.apiVersion, "test/v1")
assert.Equal(t, kr.addr, "http://127.0.0.1:8080")
}

func TestKubeReview(t *testing.T) {
table := []struct {
name string
code int
file string
err error
info auth.Info
}{
{
name: "it return error when server return error status",
code: 200,
file: "error_meta_status",
err: fmt.Errorf("strategies/kubernetes: Kube API Error"),
},
{
name: "it return error when server return invalid token review",
code: 200,
file: "invalid_token_review",
err: fmt.Errorf(`strategies/kubernetes: Failed to Unmarshal Response body to TokenReview Err: invalid character 'i' looking for beginning of value`),
},
{
name: "it return error when server return Status.Error",
code: 200,
file: "error_token_review",
err: fmt.Errorf("strategies/kubernetes: Failed to authenticate token"),
},
{
name: "it return error when server return Status.Authenticated false",
code: 200,
file: "unauthorized_token_review",
err: fmt.Errorf("strategies/kubernetes: Token Unauthorized"),
},
{
name: "it return user info",
code: 200,
file: "user_token_review",
info: auth.NewUserInfo("test", "1", nil, map[string][]string{"ext": {"1"}}),
},
}

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
srv := mockKubeAPIServer(t, tt.file, tt.code)
kr := &kubeReview{
addr: srv.URL,
client: srv.Client(),
}
r, _ := http.NewRequest("", "", nil)
info, err := kr.authenticate(r.Context(), r, "")

assert.Equal(t, tt.err, err)
assert.Equal(t, tt.info, info)
})
}
}

func mockKubeAPIServer(tb testing.TB, file string, code int) *httptest.Server {
body, err := ioutil.ReadFile("./testdata/" + file)

if err != nil {
tb.Fatalf("Failed to read testdata file Err: %s", err)
}

h := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(code)
w.Write(body)
}

return httptest.NewServer(http.HandlerFunc(h))
}
75 changes: 75 additions & 0 deletions auth/strategies/kubernetes/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package kubernetes

import (
"crypto/tls"
"net/http"

"github.com/shaj13/go-guardian/auth"
)

// SetServiceAccountToken sets kubernetes service account token
// for token review API.
func SetServiceAccountToken(token string) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if k, ok := v.(*kubeReview); ok {
k.token = token
}
})
}

// SetHTTPClient sets underlying http client.
func SetHTTPClient(c *http.Client) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if k, ok := v.(*kubeReview); ok {
k.client = c
}
})
}

// SetTLSConfig sets tls config for kubernetes api.
func SetTLSConfig(tls *tls.Config) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if k, ok := v.(*kubeReview); ok {
k.client.Transport.(*http.Transport).TLSClientConfig = tls
}
})
}

// SetClientTransport sets underlying http client transport.
func SetClientTransport(rt http.RoundTripper) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if k, ok := v.(*kubeReview); ok {
k.client.Transport = rt
}
})
}

// SetAddress sets kuberntess api server address
// e.g http://host:port or https://host:port.
func SetAddress(addr string) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if k, ok := v.(*kubeReview); ok {
k.addr = addr
}
})
}

// SetAPIVersion sets kuberntess api version.
// e.g authentication.k8s.io/v1
func SetAPIVersion(version string) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if k, ok := v.(*kubeReview); ok {
k.apiVersion = version
}
})
}

// SetAudiences sets the list of the identifiers that the resource server presented
// with the token identifies as.
func SetAudiences(auds []string) auth.Option {
return auth.OptionFunc(func(v interface{}) {
if k, ok := v.(*kubeReview); ok {
k.audiences = auds
}
})
}
Loading

0 comments on commit b4f3bb0

Please sign in to comment.