Skip to content

Commit

Permalink
Allow multiple values for labels
Browse files Browse the repository at this point in the history
  • Loading branch information
Allex1 authored and angelbarrera92 committed Sep 17, 2024
1 parent 30da6cd commit 14f2560
Show file tree
Hide file tree
Showing 12 changed files with 73 additions and 61 deletions.
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ type Authn struct {

// User Identifies a user including the tenant
type User struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
Namespace string `yaml:"namespace"`
Namespaces []string `yaml:"namespaces"`
Labels map[string]string `yaml:"labels"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Namespace string `yaml:"namespace"`
Namespaces []string `yaml:"namespaces"`
Labels map[string][]string `yaml:"labels"`
}
```

Expand Down Expand Up @@ -130,19 +130,25 @@ users:
- username: Happy
password: Prometheus
labels:
app: happy
team: america
app:
- happy
- sad
team:
- america
- username: Sad
password: Prometheus
labels:
namespace: kube-system
namespace:
- kube-system
- monitoring
- username: bored
password: Prometheus
namespaces:
- default
- kube-system
labels:
dep: system
dep:
- system
```

#### Configure the proxy for JWT authentication
Expand Down Expand Up @@ -177,8 +183,8 @@ For the token to be valid, it must:
```json
{
"labels": {
"app": "happy",
"team": "america"
"app": ["happy", "sad"],
"team": ["america"]
}
}
```
Expand Down
11 changes: 8 additions & 3 deletions configs/sample.labels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ users:
- username: Happy
password: Prometheus
labels:
app: happy
app:
- happy
- sad
team: america
- username: Sad
password: Prometheus
labels:
namespace: kube-system
namespace:
- kube-system
- monitoring
- username: bored
password: Prometheus
namespaces:
- default
- kube-system
labels:
dep: system
dep:
- system
2 changes: 1 addition & 1 deletion internal/app/prometheus-multi-tenant-proxy/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type key int
// Auth implements an authentication middleware
type Auth interface {
// IsAuthorized authenticates a request and returns the list of namespaces the user has access to
IsAuthorized(r *http.Request) (bool, []string, map[string]string)
IsAuthorized(r *http.Request) (bool, []string, map[string][]string)
// WriteUnauthorisedResponse writes an HTTP response in case the user is forbidden
WriteUnauthorisedResponse(w http.ResponseWriter)
// Load loads or reloads the configuration
Expand Down
10 changes: 5 additions & 5 deletions internal/app/prometheus-multi-tenant-proxy/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import (
type testAuth struct {
authorized bool
namespaces []string
labels map[string]string
labels map[string][]string
wasDenied bool
}

func (a *testAuth) IsAuthorized(r *http.Request) (bool, []string, map[string]string) {
func (a *testAuth) IsAuthorized(r *http.Request) (bool, []string, map[string][]string) {
return a.authorized, a.namespaces, a.labels
}

Expand All @@ -29,7 +29,7 @@ func (a *testAuth) Load() bool {

func TestAuth_Ctx(t *testing.T) {
ns := []string{"ns1", "ns2"}
labels := map[string]string{"label1": "value1", "label2": "value2"}
labels := map[string][]string{"label1": ["value1"], "label2": ["value2"]}
auth := &testAuth{
authorized: true,
namespaces: ns,
Expand All @@ -49,7 +49,7 @@ func TestAuth_Ctx(t *testing.T) {
if !reflect.DeepEqual(ns, r.Context().Value(Namespaces).([]string)) {
t.Errorf("Namespaces should be set")
}
if !reflect.DeepEqual(labels, r.Context().Value(Labels).(map[string]string)) {
if !reflect.DeepEqual(labels, r.Context().Value(Labels).(map[string][]string)) {
t.Errorf("Labels should be set")
}
}
Expand All @@ -58,7 +58,7 @@ func TestAuth_Whitelist(t *testing.T) {
auth := &testAuth{
authorized: true,
namespaces: []string{"ns"},
labels: map[string]string{},
labels: map[string][]string{},
}
r := httptest.NewRequest("GET", "http://example.com/foo", nil)
h := func(w http.ResponseWriter, req *http.Request) {}
Expand Down
4 changes: 2 additions & 2 deletions internal/app/prometheus-multi-tenant-proxy/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ func (auth *BasicAuth) Load() bool {

// IsAuthorized uses the basic authentication and the Authn file to authenticate a user
// and return the namespace he has access to
func (auth *BasicAuth) IsAuthorized(r *http.Request) (bool, []string, map[string]string) {
func (auth *BasicAuth) IsAuthorized(r *http.Request) (bool, []string, map[string][]string) {
user, pass, ok := r.BasicAuth()
if !ok {
return false, nil, nil
}
return auth.isAuthorized(user, pass)
}

func (auth *BasicAuth) isAuthorized(user, pass string) (bool, []string, map[string]string) {
func (auth *BasicAuth) isAuthorized(user, pass string) (bool, []string, map[string][]string) {
authConfig := auth.getConfig()
for _, v := range authConfig.Users {
if subtle.ConstantTimeCompare([]byte(user), []byte(v.Username)) == 1 && subtle.ConstantTimeCompare([]byte(pass), []byte(v.Password)) == 1 {
Expand Down
2 changes: 1 addition & 1 deletion internal/app/prometheus-multi-tenant-proxy/basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestBasic_isAuthorized(t *testing.T) {
args args
want bool
want1 []string
want2 map[string]string
want2 map[string][]string
}{
{
"Valid User",
Expand Down
8 changes: 4 additions & 4 deletions internal/app/prometheus-multi-tenant-proxy/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type NamespaceClaim struct {
// Namespaces contains the list of namespaces a user has access to
Namespaces []string `json:"namespaces"`
// Labels contains a map of labels that will be injected for the user
Labels map[string]string `json:"labels"`
Labels map[string][]string `json:"labels"`
jwt.RegisteredClaims
}

Expand Down Expand Up @@ -131,7 +131,7 @@ func (auth *JwtAuth) loadFromFile(location *string) bool {

// IsAuthorized validates the user by verifying the JWT token in
// the request and returning the namespaces claim found in token the payload.
func (auth *JwtAuth) IsAuthorized(r *http.Request) (bool, []string, map[string]string) {
func (auth *JwtAuth) IsAuthorized(r *http.Request) (bool, []string, map[string][]string) {
tokenString := extractTokens(&r.Header)
if tokenString == "" {
log.Printf("Token is missing from header request")
Expand All @@ -146,7 +146,7 @@ func (auth *JwtAuth) WriteUnauthorisedResponse(w http.ResponseWriter) {
w.Write([]byte("Unauthorised\n"))
}

func (auth *JwtAuth) isAuthorized(tokenString string) (bool, []string, map[string]string) {
func (auth *JwtAuth) isAuthorized(tokenString string) (bool, []string, map[string][]string) {
token, err := jwt.ParseWithClaims(tokenString, &NamespaceClaim{}, auth.jwks.Keyfunc)
if err != nil || !token.Valid {
log.Printf("%s\n", err)
Expand All @@ -158,7 +158,7 @@ func (auth *JwtAuth) isAuthorized(tokenString string) (bool, []string, map[strin
claims.Namespaces = []string{}
}
if claims.Labels == nil {
claims.Labels = make(map[string]string)
claims.Labels = make(map[string][]string)
}
return true, claims.Namespaces, claims.Labels
}
Expand Down
10 changes: 5 additions & 5 deletions internal/app/prometheus-multi-tenant-proxy/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ const (
validRsaToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2LWtleSIsInR5cCI6IkpXVCJ9.eyJuYW1lc3BhY2VzIjpbInByb21ldGhldXMiLCJhcHAtMSJdfQ.n_hy5yqjFkpD00VNGCLkRyeOBdcjeu9Yp1TVzV5jSKaX32Idrl2jv1mHCX5JJfM-tyLXxCQJcze9q7IXpN0_x-E7iE_uAvDT7BiMWSwy7lWW2eRuffggv2EG8HP3_kGgsH-RcP4B5VbaKeB9N1RNrHwvxoiYKhcFQCTJzsf010s10nUYmfL0jQ8hW--yTX2kly8zXxBoJXu6rluNMXWL7o8Tx9ONHLLlz-trP7s9xFN_GQtbZ3lKZ5n8XESccctXWAdIqtYtlTA4KCr0krIX7cRbLdni5QOPBTwQxdOBujdDaXZqo8K8PJfaZ93oyJUdYe7rnX0Lz_dT1EJLWYvm-A"
// kid = "rs256-key", payload = {"namespaces": []}}
validEmptyNamespacesRSAToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2LWtleSIsInR5cCI6IkpXVCJ9.eyJuYW1lc3BhY2VzIjpbXX0.LWPlxmZPDaKA3Z-IbRAoBYuymCx3cdZvXHzlSfVIhj4TjoQZ8Rom5IWtJpoEiq-_DkQHFgFRnLTsFE8CcaYM_eLWRZPK7c_rDwzfJDxDVhIL3k6krL5gq_4Y6nOGnjktJkIJvJstl9FDc7gyx0EBvUX-cgQzh-my9whMXBrZ0oybVyiBGlAZbVOiW-BObm3U0hYF4Xt6HOTm4khAEsZPnS4rglQpQki_q4w67OaMcTwfO_hr6KJtwzavLLCWJhijWdON93ueubn4Z294TM5SWQFzPM-knFDaBfzq5k94NQviBoT7ekb9RsGLrjKsrVOdOVMM8b4BEFXMtZpVENLgQg"
// kid = "rs256-key", payload = {"labels": {"app":"ecom","team":"europe"}}}
// kid = "rs256-key", payload = {"labels": {"app":["ecom"],"team":["europe"]}}}
validTwoLabelsRSAToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2LWtleSJ9.eyJsYWJlbHMiOnsiYXBwIjoiZWNvbSIsInRlYW0iOiJldXJvcGUifX0.Xb9-WPdP-yL7afsYShGQt1p3YVhNcufY-6dxtCVnbhgKLotqgy81tS-5RxF7KdSlSkfuwNyZCuE_qnKO_seOxczHOkARWnvZ5jlfPoPI8adKiVykeDR6q6fj3fO5Mp7BDNVXBwb9_wQ08Y3JwONdoNmvdnUz6aspD7IVIL41t64kst-GTxvvkdA-1Xfh9LB0zmyaCEgYiaByNJevtqnwFociTzRbWR2yXcEkhzbqKSGG6ia55It5CeN3GB9sjAWOEd57fSgDJwr0D80zxFoXtLeX64gcCjNsxJsh5ZrQ8U34fdo-73mPDJPCOBkowiamPDWOkBQ54U5lesbE5R3KPA"
// kid = "rs256-key", payload = {"labels":{"app":"ecom","team":"europe"},"namespaces":["kube-system","monitoring"]}
// kid = "rs256-key", payload = {"labels":{"app":["ecom"],"team":["europe"]},"namespaces":["kube-system","monitoring"]}
validTwoLabelsTwoNamespacesRSAToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2LWtleSJ9.eyJsYWJlbHMiOnsiYXBwIjoiZWNvbSIsInRlYW0iOiJldXJvcGUifSwibmFtZXNwYWNlcyI6WyJrdWJlLXN5c3RlbSIsIm1vbml0b3JpbmciXX0.Zk6hE9OBUIH5ctMzSeq2p40dJFiwZS_TghePWlTB1_-XHOzZRGvbT-sXoZnIy1__lHJZ4h8t0-P0_zwQPpZ2aB2A0Ar3wogEiIdktoRtqQcMvSjjIjwNm8e9uaE1QBpeqNtxg5i3hDMJLVfsoXta0PJ9YW4hbuhnpaThhji9M7duOXv9eeW4nJHSFr3YVCn75qR35O8z3Pwjo_06OhSpK5sy1PbqQLNvzkWdKYiqAjezWnnh6kO37hQfDJWUaKxkhE4TmOMJRk_mRKrUpHZ1mQ6rZ4YXyo0pBBNqJ5uJYA45bT2FJpNqJ9rXHf2qjDBwcS6SEw8pDe-iRdIC0xr1Ig"
)

Expand Down Expand Up @@ -131,13 +131,13 @@ func TestJWT_IsAuthorized(t *testing.T) {
desc string
token string
ns []string
l map[string]string
l map[string][]string
}{
{"hmac", validHmacToken, []string{"prometheus"}, map[string]string{}},
{"rsa", validRsaToken, []string{"prometheus", "app-1"}, map[string]string{}},
{"empty-namespace-rsa", validEmptyNamespacesRSAToken, []string{}, map[string]string{}},
{"two-labels-rsa", validTwoLabelsRSAToken, []string{}, map[string]string{"app": "ecom", "team": "europe"}},
{"two-labels-two-namespaces-rsa", validTwoLabelsTwoNamespacesRSAToken, []string{"kube-system", "monitoring"}, map[string]string{"app": "ecom", "team": "europe"}},
{"two-labels-rsa", validTwoLabelsRSAToken, []string{}, map[string][]string{"app": {"ecom"}, "team": {"europe"}}},
{"two-labels-two-namespaces-rsa", validTwoLabelsTwoNamespacesRSAToken, []string{"kube-system", "monitoring"}, map[string][]string{"app": {"ecom"}, "team": {"europe"}}},
}

for _, tc := range validTestCases {
Expand Down
7 changes: 4 additions & 3 deletions internal/app/prometheus-multi-tenant-proxy/reverse.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,17 @@ func (r *ReversePrometheusRoundTripper) Director(req *http.Request) {
func (r *ReversePrometheusRoundTripper) modifyRequest(req *http.Request, prometheusFormParameter string) error {

namespaces := req.Context().Value(Namespaces).([]string)
l := req.Context().Value(Labels).(map[string]string)
l := req.Context().Value(Labels).(map[string][]string)

// Convert the labels map into a slice of label matchers.
var labelMatchers []*labels.Matcher

for k, v := range l {
combinedValue := strings.Join(v, "|")
labelMatchers = append(labelMatchers, &labels.Matcher{
Name: k,
Type: labels.MatchEqual,
Value: v,
Type: labels.MatchRegexp,
Value: combinedValue,
})
}

Expand Down
10 changes: 5 additions & 5 deletions internal/app/prometheus-multi-tenant-proxy/reverse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ func ctx(namespaces []string, labels map[string]string) context.Context {
namespaces = []string{}
}
if labels == nil {
labels = map[string]string{}
labels = map[string][]string{}
}
c := context.WithValue(context.TODO(), Namespaces, namespaces)
c = context.WithValue(c, Labels, labels)
return c
}

func getRequest(url string, namespaces []string, labels map[string]string) *http.Request {
func getRequest(url string, namespaces []string, labels map[string][]string) *http.Request {
r, _ := http.NewRequest(http.MethodGet, url, nil)
return r.WithContext(ctx(namespaces, labels))
}
Expand All @@ -42,7 +42,7 @@ func ns2qs(ns []string) string {
return fmt.Sprintf("namespace=~\"%s\"", strings.Join(ns, "|"))
}

func labels2qs(labels map[string]string) string {
func labels2qs(labels map[string][]string) string {
if len(labels) == 0 {
return ""
}
Expand Down Expand Up @@ -102,7 +102,7 @@ func TestReverse_ModifyGet(t *testing.T) {
for _, tc := range testCases {
t.Run(strings.Split(tc.query, "?")[0], func(t *testing.T) {
// test labels injection
for _, labels := range []map[string]string{{"foo": "true"}, {"bar": "one", "buzz": "two"}} {
for _, labels := range []map[string][]string{{"foo": {"true"}}, {"bar": {"one"}, "buzz": {"two"}}} {
r := getRequest(fmt.Sprintf("%s/api/v1/%s", promURL, tc.query), nil, labels)
tripper.Director(r)

Expand All @@ -127,7 +127,7 @@ func TestReverse_ModifyGet(t *testing.T) {
}
// test both
ns := []string{"some-ns"}
labels := map[string]string{"some": "label"}
labels := map[string]string[]{"some": "label"}
r := getRequest(fmt.Sprintf("%s/api/v1/%s", promURL, tc.query), ns, labels)
tripper.Director(r)

Expand Down
12 changes: 6 additions & 6 deletions internal/pkg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ type Authn struct {

// User Identifies a user including the tenant
type User struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
Namespace string `yaml:"namespace"`
Namespaces []string `yaml:"namespaces"`
Labels map[string]string `yaml:"labels"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Namespace string `yaml:"namespace"`
Namespaces []string `yaml:"namespaces"`
Labels map[string][]string `yaml:"labels"`
}

// ParseConfig read a configuration file in the path `location` and returns an Authn object
Expand All @@ -39,7 +39,7 @@ func ParseConfig(location *string) (*Authn, error) {
authn.Users[i].Namespaces = []string{}
}
if authn.Users[i].Labels == nil {
authn.Users[i].Labels = map[string]string{}
authn.Users[i].Labels = map[string][]string{}
}
}
return &authn, nil
Expand Down
Loading

0 comments on commit 14f2560

Please sign in to comment.