Skip to content

Commit

Permalink
Merge pull request #13 from aiyengar2/perform_token_checks
Browse files Browse the repository at this point in the history
Perform deep token checks against kube-api-server for token validation
  • Loading branch information
Arvind Iyengar authored Dec 14, 2022
2 parents ef0bcdc + ac91c77 commit 331bee6
Show file tree
Hide file tree
Showing 10 changed files with 1,349 additions and 4 deletions.
12 changes: 12 additions & 0 deletions pkg/agent/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/urfave/cli"
"golang.org/x/net/netutil"
"google.golang.org/grpc"
authentication "k8s.io/api/authentication/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -101,8 +102,10 @@ func (a *agentConfig) String() string {

type agent struct {
cfg *agentConfig
userInfo authentication.UserInfo
listener net.Listener
namespaces kube.Namespaces
tokens kube.Tokens
remoteAPI promapiv1.API
}

Expand Down Expand Up @@ -181,10 +184,19 @@ func createAgent(cfg *agentConfig) (*agent, error) {
return nil, errors.Annotate(err, "unable to new Prometheus client")
}

// create tokens client and get userInfo
tokens := kube.NewTokens(cfg.ctx, k8sClient)
userInfo, err := tokens.Authenticate(cfg.myToken)
if err != nil {
return nil, errors.Annotate(err, "unable to get userInfo from agent token")
}

return &agent{
cfg: cfg,
userInfo: userInfo,
listener: listener,
namespaces: kube.NewNamespaces(cfg.ctx, k8sClient),
tokens: tokens,
remoteAPI: promapiv1.NewAPI(promClient),
}, nil
}
Expand Down
18 changes: 16 additions & 2 deletions pkg/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import (
"time"

"github.com/gorilla/mux"
"github.com/juju/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rancher/prometheus-auth/pkg/kube"
log "github.com/sirupsen/logrus"
authentication "k8s.io/api/authentication/v1"
)

func (a *agent) httpBackend() http.Handler {
Expand Down Expand Up @@ -59,14 +62,25 @@ func accessControl(agt *agent, proxyHandler http.Handler) http.Handler {

router.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var userInfo authentication.UserInfo
var err error
accessToken := strings.TrimPrefix(r.Header.Get(authorizationHeaderKey), "Bearer ")

// try to authenticate the access token
if len(accessToken) == 0 {
http.Error(w, "unauthorized", http.StatusUnauthorized)
err = errors.New("no access token provided")
} else {
userInfo, err = agt.tokens.Authenticate(accessToken)
}

if err != nil {
// either not token was provided or user is unauthenticated with k8s API
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

// direct proxy
if agt.cfg.myToken == accessToken {
if kube.MatchingUsers(agt.userInfo, userInfo) {
proxyHandler.ServeHTTP(w, r)
return
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/agent/http_api_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"sync"

"github.com/cockroachdb/cockroach/pkg/util/httputil"
"github.com/golang/protobuf/proto"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/juju/errors"
promapiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
Expand Down
157 changes: 156 additions & 1 deletion pkg/agent/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/rancher/prometheus-auth/pkg/data"
"github.com/rancher/prometheus-auth/pkg/kube"
"github.com/stretchr/testify/require"
authentication "k8s.io/api/authentication/v1"
)

type ScenarioType string
Expand Down Expand Up @@ -131,6 +132,80 @@ func getTestCases(t *testing.T) []httpTestCase {
Token: "someNamespacesToken",
Scenarios: samples.SomeNamespacesTokenSeriesScenarios,
},
// myToken
{
Type: FederateScenario,
HTTPMethod: http.MethodGet,
Token: "myToken",
Scenarios: samples.MyTokenFederateScenarios,
},
{
Type: LabelScenario,
HTTPMethod: http.MethodGet,
Token: "myToken",
Scenarios: samples.MyTokenLabelScenarios,
},
{
Type: QueryScenario,
HTTPMethod: http.MethodGet,
Token: "myToken",
Scenarios: samples.MyTokenQueryScenarios,
},
{
Type: QueryScenario,
HTTPMethod: http.MethodPost,
Token: "myToken",
Scenarios: samples.MyTokenQueryScenarios,
},
{
Type: ReadScenario,
HTTPMethod: http.MethodPost,
Token: "myToken",
Scenarios: samples.MyTokenReadScenarios(t),
},
{
Type: SeriesScenario,
HTTPMethod: http.MethodGet,
Token: "myToken",
Scenarios: samples.MyTokenSeriesScenarios,
},
// unauthenticated
{
Type: FederateScenario,
HTTPMethod: http.MethodGet,
Token: "unauthenticated",
Scenarios: samples.MyTokenFederateScenarios,
},
{
Type: LabelScenario,
HTTPMethod: http.MethodGet,
Token: "unauthenticated",
Scenarios: samples.MyTokenLabelScenarios,
},
{
Type: QueryScenario,
HTTPMethod: http.MethodGet,
Token: "unauthenticated",
Scenarios: samples.MyTokenQueryScenarios,
},
{
Type: QueryScenario,
HTTPMethod: http.MethodPost,
Token: "unauthenticated",
Scenarios: samples.MyTokenQueryScenarios,
},
{
Type: ReadScenario,
HTTPMethod: http.MethodPost,
Token: "unauthenticated",
Scenarios: samples.MyTokenReadScenarios(t),
},
{
Type: SeriesScenario,
HTTPMethod: http.MethodGet,
Token: "unauthenticated",
Scenarios: samples.MyTokenSeriesScenarios,
},
}
}

Expand Down Expand Up @@ -318,8 +393,13 @@ func mockAgent(t *testing.T) *agent {
}

return &agent{
cfg: agtCfg,
cfg: agtCfg,
userInfo: authentication.UserInfo{
Username: "myUser",
UID: "cluster-admin",
},
namespaces: mockOwnedNamespaces(),
tokens: mockTokenAuth(),
remoteAPI: promapiv1.NewAPI(promClient),
}
}
Expand All @@ -338,6 +418,15 @@ func (v ScenarioValidator) Validate(t *testing.T, handler http.Handler) {
return
}

// Validate unauthenticated user
if v.Token == "unauthenticated" {
// unauthenticated user
if got := res.Code; got != http.StatusUnauthorized {
t.Errorf("[series] [GET] token %q scenario %q: got code %d, want %d for unauthenticated users", v.Token, v.Name, got, http.StatusUnauthorized)
}
return
}

// Validate response code
if got, want := res.Code, v.Scenario.RespCode; got != want {
t.Errorf("[series] [GET] token %q scenario %q: got code %d, want %d", v.Token, v.Name, got, want)
Expand Down Expand Up @@ -452,6 +541,8 @@ func (v ScenarioValidator) validateProtoBody(t *testing.T, res *httptest.Respons
t.Fatal(err)
}

sortReadResponse(&protoRes)

if got, want := protoRes.Results, v.Scenario.RespBody; !reflect.DeepEqual(got, want) {
t.Errorf("[%s] [%s] token %q scenario %q: got body\n%v\n, want\n%v\n", v.Type, v.Method, v.Token, v.Name, got, want)
}
Expand Down Expand Up @@ -491,6 +582,39 @@ func jsonResponseBody(body interface{}) string {
return string(respBytes)
}

type SortableTimeSeries []*prompb.TimeSeries

func (s SortableTimeSeries) Len() int {
return len(s)
}

func (s SortableTimeSeries) Less(i, j int) bool {
k := 0
for k < len(s[i].Labels) && k < len(s[j].Labels) {
// compare keys
if s[i].Labels[k].Name != s[j].Labels[k].Name {
return s[i].Labels[k].Name < s[j].Labels[k].Name
}
// compare values
if s[i].Labels[k].Value != s[j].Labels[k].Value {
return s[i].Labels[k].Value < s[j].Labels[k].Value
}
k += 1
}
// default to preserving order
return true
}

func (s SortableTimeSeries) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

func sortReadResponse(rr *prompb.ReadResponse) {
for _, q := range rr.Results {
sort.Sort(SortableTimeSeries(q.Timeseries))
}
}

type fakeOwnedNamespaces struct {
token2Namespaces map[string]data.Set
}
Expand All @@ -508,6 +632,37 @@ func mockOwnedNamespaces() kube.Namespaces {
}
}

type fakeTokenAuth struct {
token2UserInfo map[string]authentication.UserInfo
}

func (f *fakeTokenAuth) Authenticate(token string) (authentication.UserInfo, error) {
userInfo, ok := f.token2UserInfo[token]
if !ok {
return userInfo, fmt.Errorf("user is not authenticated")
}
return userInfo, nil
}

func mockTokenAuth() kube.Tokens {
return &fakeTokenAuth{
token2UserInfo: map[string]authentication.UserInfo{
"myToken": authentication.UserInfo{
Username: "myUser",
UID: "cluster-admin",
},
"someNamespacesToken": authentication.UserInfo{
Username: "someNamespacesUser",
UID: "project-member",
},
"noneNamespacesToken": authentication.UserInfo{
Username: "noneNamespacesUser",
UID: "cluster-member",
},
},
}
}

type dbAdapter struct {
*promtsdb.DB
}
Expand Down
Loading

0 comments on commit 331bee6

Please sign in to comment.