Skip to content

Commit 535df13

Browse files
committed
Implement authHandler
1 parent 313f8fb commit 535df13

File tree

3 files changed

+195
-7
lines changed

3 files changed

+195
-7
lines changed

pkg/cvo/metrics.go

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ import (
1717

1818
"github.com/prometheus/client_golang/prometheus"
1919
"github.com/prometheus/client_golang/prometheus/promhttp"
20+
authenticationv1 "k8s.io/api/authentication/v1"
2021
corev1 "k8s.io/api/core/v1"
2122
apierrors "k8s.io/apimachinery/pkg/api/errors"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2224
"k8s.io/apimachinery/pkg/labels"
2325
"k8s.io/apimachinery/pkg/util/sets"
26+
authenticationclientsetv1 "k8s.io/client-go/kubernetes/typed/authentication/v1"
27+
"k8s.io/client-go/rest"
2428
"k8s.io/client-go/tools/cache"
2529
"k8s.io/klog/v2"
2630

@@ -128,8 +132,8 @@ type asyncResult struct {
128132
error error
129133
}
130134

131-
func createHttpServer() *http.Server {
132-
auth := authHandler{downstream: promhttp.Handler()}
135+
func createHttpServer(ctx context.Context, client *authenticationclientsetv1.AuthenticationV1Client) *http.Server {
136+
auth := authHandler{downstream: promhttp.Handler(), ctx: ctx, client: client.TokenReviews()}
133137
handler := http.NewServeMux()
134138
handler.Handle("/metrics", &auth)
135139
server := &http.Server{
@@ -138,8 +142,32 @@ func createHttpServer() *http.Server {
138142
return server
139143
}
140144

145+
type TokenReviewInterface interface {
146+
Create(ctx context.Context, tokenReview *authenticationv1.TokenReview, opts metav1.CreateOptions) (*authenticationv1.TokenReview, error)
147+
}
148+
141149
type authHandler struct {
142150
downstream http.Handler
151+
ctx context.Context
152+
client TokenReviewInterface
153+
}
154+
155+
func (a *authHandler) authorize(token string) (bool, error) {
156+
tr := &authenticationv1.TokenReview{
157+
Spec: authenticationv1.TokenReviewSpec{
158+
Token: token,
159+
},
160+
}
161+
result, err := a.client.Create(a.ctx, tr, metav1.CreateOptions{})
162+
if err != nil {
163+
return false, fmt.Errorf("failed to check token: %w", err)
164+
}
165+
if !result.Status.Authenticated {
166+
klog.V(2).Info("The token cannot be authenticated.")
167+
} else if user := result.Status.User.Username; user != "system:serviceaccount:openshift-monitoring:prometheus-k8s" {
168+
klog.V(2).Infof("Access the metrics from he unexpected user %s is denied.", user)
169+
}
170+
return result.Status.Authenticated && result.Status.User.Username == "system:serviceaccount:openshift-monitoring:prometheus-k8s", nil
143171
}
144172

145173
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -154,7 +182,16 @@ func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
154182
return
155183
}
156184

157-
// TODO use the token
185+
authorized, err := a.authorize(token)
186+
if err != nil {
187+
klog.Warningf("Failed to authorize token: %v", err)
188+
http.Error(w, "failed to authorize due to an internal error", http.StatusInternalServerError)
189+
return
190+
}
191+
if !authorized {
192+
http.Error(w, "failed to authorize", http.StatusUnauthorized)
193+
return
194+
}
158195
a.downstream.ServeHTTP(w, r)
159196
}
160197

@@ -203,7 +240,7 @@ func handleServerResult(result asyncResult, lastLoopError error) error {
203240
// Also detects changes to metrics certificate files upon which
204241
// the metrics HTTP server is shutdown and recreated with a new
205242
// TLS configuration.
206-
func RunMetrics(runContext context.Context, shutdownContext context.Context, listenAddress, certFile, keyFile string) error {
243+
func RunMetrics(runContext context.Context, shutdownContext context.Context, listenAddress, certFile, keyFile string, restConfig *rest.Config) error {
207244
var tlsConfig *tls.Config
208245
if listenAddress != "" {
209246
var err error
@@ -214,7 +251,13 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, lis
214251
} else {
215252
return errors.New("TLS configuration is required to serve metrics")
216253
}
217-
server := createHttpServer()
254+
255+
client, err := authenticationclientsetv1.NewForConfig(restConfig)
256+
if err != nil {
257+
return fmt.Errorf("failed to create config: %w", err)
258+
}
259+
260+
server := createHttpServer(runContext, client)
218261

219262
resultChannel := make(chan asyncResult, 1)
220263
resultChannelCount := 1
@@ -268,7 +311,7 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, lis
268311
case result := <-resultChannel: // crashed before a shutdown was requested or metrics server recreated
269312
if restartServer {
270313
klog.Info("Creating metrics server with updated TLS configuration.")
271-
server = createHttpServer()
314+
server = createHttpServer(runContext, client)
272315
go startListening(server, tlsConfig, listenAddress, resultChannel)
273316
restartServer = false
274317
continue

pkg/cvo/metrics_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
package cvo
22

33
import (
4+
"context"
45
"errors"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
510
"sort"
611
"strings"
712
"testing"
813
"time"
914

1015
"github.com/davecgh/go-spew/spew"
16+
"github.com/google/go-cmp/cmp"
1117
"github.com/prometheus/client_golang/prometheus"
1218
dto "github.com/prometheus/client_model/go"
19+
authenticationv1 "k8s.io/api/authentication/v1"
1320
corev1 "k8s.io/api/core/v1"
1421
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1522
"k8s.io/client-go/tools/record"
@@ -1009,3 +1016,141 @@ func metricParts(t *testing.T, metric prometheus.Metric, labels ...string) strin
10091016
}
10101017
return strings.Join(parts, " ")
10111018
}
1019+
1020+
type fakeClient struct {
1021+
}
1022+
1023+
func (c *fakeClient) Create(_ context.Context, tokenReview *authenticationv1.TokenReview, _ metav1.CreateOptions) (*authenticationv1.TokenReview, error) {
1024+
if tokenReview != nil {
1025+
ret := tokenReview.DeepCopy()
1026+
if tokenReview.Spec.Token == "good" {
1027+
ret.Status.Authenticated = true
1028+
ret.Status.User.Username = "system:serviceaccount:openshift-monitoring:prometheus-k8s"
1029+
}
1030+
if tokenReview.Spec.Token == "authenticated" {
1031+
ret.Status.Authenticated = true
1032+
}
1033+
if tokenReview.Spec.Token == "error" {
1034+
return nil, errors.New("fake error")
1035+
}
1036+
return ret, nil
1037+
}
1038+
return nil, errors.New("nil input")
1039+
}
1040+
1041+
type okHandler struct {
1042+
}
1043+
1044+
func (h *okHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1045+
_, _ = fmt.Fprintf(w, "ok")
1046+
}
1047+
1048+
func Test_authHandler(t *testing.T) {
1049+
tests := []struct {
1050+
name string
1051+
handler *authHandler
1052+
method string
1053+
body io.Reader
1054+
headerKey string
1055+
headerValue string
1056+
expectedStatusCode int
1057+
expectedBody string
1058+
}{
1059+
{
1060+
name: "good",
1061+
handler: &authHandler{
1062+
ctx: context.TODO(),
1063+
downstream: &okHandler{},
1064+
client: &fakeClient{},
1065+
},
1066+
method: "GET",
1067+
headerKey: "Authorization",
1068+
headerValue: "Bearer good",
1069+
expectedStatusCode: http.StatusOK,
1070+
expectedBody: "ok",
1071+
},
1072+
{
1073+
name: "authenticated",
1074+
handler: &authHandler{
1075+
ctx: context.TODO(),
1076+
downstream: &okHandler{},
1077+
client: &fakeClient{},
1078+
},
1079+
method: "GET",
1080+
headerKey: "Authorization",
1081+
headerValue: "Bearer authenticated",
1082+
expectedStatusCode: 401,
1083+
expectedBody: "failed to authorize\n",
1084+
},
1085+
{
1086+
name: "bad",
1087+
handler: &authHandler{
1088+
ctx: context.TODO(),
1089+
downstream: &okHandler{},
1090+
client: &fakeClient{},
1091+
},
1092+
method: "GET",
1093+
headerKey: "Authorization",
1094+
headerValue: "Bearer bad",
1095+
expectedStatusCode: 401,
1096+
expectedBody: "failed to authorize\n",
1097+
},
1098+
{
1099+
name: "failed to get the Authorization header",
1100+
handler: &authHandler{
1101+
ctx: context.TODO(),
1102+
downstream: &okHandler{},
1103+
client: &fakeClient{},
1104+
},
1105+
method: "GET",
1106+
expectedStatusCode: 401,
1107+
expectedBody: "failed to get the Authorization header\n",
1108+
},
1109+
{
1110+
name: "failed to get the Bearer token",
1111+
handler: &authHandler{
1112+
ctx: context.TODO(),
1113+
downstream: &okHandler{},
1114+
client: &fakeClient{},
1115+
},
1116+
method: "GET",
1117+
headerKey: "Authorization",
1118+
headerValue: "xxx bad",
1119+
expectedStatusCode: 401,
1120+
expectedBody: "failed to get the Bearer token\n",
1121+
},
1122+
{
1123+
name: "error",
1124+
handler: &authHandler{
1125+
ctx: context.TODO(),
1126+
downstream: &okHandler{},
1127+
client: &fakeClient{},
1128+
},
1129+
method: "GET",
1130+
headerKey: "Authorization",
1131+
headerValue: "Bearer error",
1132+
expectedStatusCode: 500,
1133+
expectedBody: "failed to authorize due to an internal error\n",
1134+
},
1135+
}
1136+
for _, tt := range tests {
1137+
t.Run(tt.name, func(t *testing.T) {
1138+
rr := httptest.NewRecorder()
1139+
1140+
req, err := http.NewRequest(tt.method, "url-not-important", tt.body)
1141+
if err != nil {
1142+
t.Fatal(err)
1143+
}
1144+
req.Header.Set(tt.headerKey, tt.headerValue)
1145+
1146+
tt.handler.ServeHTTP(rr, req)
1147+
if diff := cmp.Diff(tt.expectedStatusCode, rr.Code); diff != "" {
1148+
t.Errorf("%s: status differs from expected:\n%s", tt.name, diff)
1149+
}
1150+
1151+
if diff := cmp.Diff(tt.expectedBody, rr.Body.String()); diff != "" {
1152+
t.Errorf("%s: body differs from expected:\n%s", tt.name, diff)
1153+
}
1154+
})
1155+
}
1156+
}

pkg/start/start.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ func (o *Options) run(ctx context.Context, controllerCtx *Context, lock resource
357357
resultChannelCount++
358358
go func() {
359359
defer utilruntime.HandleCrash()
360-
err := cvo.RunMetrics(postMainContext, shutdownContext, o.ListenAddr, o.ServingCertFile, o.ServingKeyFile)
360+
err := cvo.RunMetrics(postMainContext, shutdownContext, o.ListenAddr, o.ServingCertFile, o.ServingKeyFile, restConfig)
361361
resultChannel <- asyncResult{name: "metrics server", error: err}
362362
}()
363363
}

0 commit comments

Comments
 (0)