Skip to content

Commit 833a491

Browse files
committed
Implement authHandler
1 parent 313f8fb commit 833a491

File tree

3 files changed

+214
-7
lines changed

3 files changed

+214
-7
lines changed

pkg/cvo/metrics.go

Lines changed: 55 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,34 @@ 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+
isAuthenticated := result.Status.Authenticated
166+
isPrometheus := result.Status.User.Username == "system:serviceaccount:openshift-monitoring:prometheus-k8s"
167+
if !isAuthenticated {
168+
klog.V(4).Info("The token cannot be authenticated.")
169+
} else if !isPrometheus {
170+
klog.V(4).Infof("Access the metrics from the unexpected user %s is denied.", result.Status.User.Username)
171+
}
172+
return isAuthenticated && isPrometheus, nil
143173
}
144174

145175
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -149,12 +179,25 @@ func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
149179
return
150180
}
151181
token := strings.TrimPrefix(authHeader, "Bearer ")
182+
if token == "" {
183+
http.Error(w, "empty Bearer token", http.StatusUnauthorized)
184+
return
185+
}
152186
if token == authHeader {
153187
http.Error(w, "failed to get the Bearer token", http.StatusUnauthorized)
154188
return
155189
}
156190

157-
// TODO use the token
191+
authorized, err := a.authorize(token)
192+
if err != nil {
193+
klog.Warningf("Failed to authorize token: %v", err)
194+
http.Error(w, "failed to authorize due to an internal error", http.StatusInternalServerError)
195+
return
196+
}
197+
if !authorized {
198+
http.Error(w, "failed to authorize", http.StatusUnauthorized)
199+
return
200+
}
158201
a.downstream.ServeHTTP(w, r)
159202
}
160203

@@ -203,7 +246,7 @@ func handleServerResult(result asyncResult, lastLoopError error) error {
203246
// Also detects changes to metrics certificate files upon which
204247
// the metrics HTTP server is shutdown and recreated with a new
205248
// TLS configuration.
206-
func RunMetrics(runContext context.Context, shutdownContext context.Context, listenAddress, certFile, keyFile string) error {
249+
func RunMetrics(runContext context.Context, shutdownContext context.Context, listenAddress, certFile, keyFile string, restConfig *rest.Config) error {
207250
var tlsConfig *tls.Config
208251
if listenAddress != "" {
209252
var err error
@@ -214,7 +257,13 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, lis
214257
} else {
215258
return errors.New("TLS configuration is required to serve metrics")
216259
}
217-
server := createHttpServer()
260+
261+
client, err := authenticationclientsetv1.NewForConfig(restConfig)
262+
if err != nil {
263+
return fmt.Errorf("failed to create config: %w", err)
264+
}
265+
266+
server := createHttpServer(runContext, client)
218267

219268
resultChannel := make(chan asyncResult, 1)
220269
resultChannelCount := 1
@@ -268,7 +317,7 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, lis
268317
case result := <-resultChannel: // crashed before a shutdown was requested or metrics server recreated
269318
if restartServer {
270319
klog.Info("Creating metrics server with updated TLS configuration.")
271-
server = createHttpServer()
320+
server = createHttpServer(runContext, client)
272321
go startListening(server, tlsConfig, listenAddress, resultChannel)
273322
restartServer = false
274323
continue

pkg/cvo/metrics_test.go

Lines changed: 158 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,154 @@ 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: "empty bearer token",
1074+
handler: &authHandler{
1075+
ctx: context.TODO(),
1076+
downstream: &okHandler{},
1077+
client: &fakeClient{},
1078+
},
1079+
method: "GET",
1080+
headerKey: "Authorization",
1081+
headerValue: "Bearer ",
1082+
expectedStatusCode: 401,
1083+
expectedBody: "empty Bearer token\n",
1084+
},
1085+
{
1086+
name: "authenticated",
1087+
handler: &authHandler{
1088+
ctx: context.TODO(),
1089+
downstream: &okHandler{},
1090+
client: &fakeClient{},
1091+
},
1092+
method: "GET",
1093+
headerKey: "Authorization",
1094+
headerValue: "Bearer authenticated",
1095+
expectedStatusCode: 401,
1096+
expectedBody: "failed to authorize\n",
1097+
},
1098+
{
1099+
name: "bad",
1100+
handler: &authHandler{
1101+
ctx: context.TODO(),
1102+
downstream: &okHandler{},
1103+
client: &fakeClient{},
1104+
},
1105+
method: "GET",
1106+
headerKey: "Authorization",
1107+
headerValue: "Bearer bad",
1108+
expectedStatusCode: 401,
1109+
expectedBody: "failed to authorize\n",
1110+
},
1111+
{
1112+
name: "failed to get the Authorization header",
1113+
handler: &authHandler{
1114+
ctx: context.TODO(),
1115+
downstream: &okHandler{},
1116+
client: &fakeClient{},
1117+
},
1118+
method: "GET",
1119+
expectedStatusCode: 401,
1120+
expectedBody: "failed to get the Authorization header\n",
1121+
},
1122+
{
1123+
name: "failed to get the Bearer token",
1124+
handler: &authHandler{
1125+
ctx: context.TODO(),
1126+
downstream: &okHandler{},
1127+
client: &fakeClient{},
1128+
},
1129+
method: "GET",
1130+
headerKey: "Authorization",
1131+
headerValue: "xxx bad",
1132+
expectedStatusCode: 401,
1133+
expectedBody: "failed to get the Bearer token\n",
1134+
},
1135+
{
1136+
name: "error",
1137+
handler: &authHandler{
1138+
ctx: context.TODO(),
1139+
downstream: &okHandler{},
1140+
client: &fakeClient{},
1141+
},
1142+
method: "GET",
1143+
headerKey: "Authorization",
1144+
headerValue: "Bearer error",
1145+
expectedStatusCode: 500,
1146+
expectedBody: "failed to authorize due to an internal error\n",
1147+
},
1148+
}
1149+
for _, tt := range tests {
1150+
t.Run(tt.name, func(t *testing.T) {
1151+
rr := httptest.NewRecorder()
1152+
1153+
req, err := http.NewRequest(tt.method, "url-not-important", tt.body)
1154+
if err != nil {
1155+
t.Fatal(err)
1156+
}
1157+
req.Header.Set(tt.headerKey, tt.headerValue)
1158+
1159+
tt.handler.ServeHTTP(rr, req)
1160+
if diff := cmp.Diff(tt.expectedStatusCode, rr.Code); diff != "" {
1161+
t.Errorf("%s: status differs from expected:\n%s", tt.name, diff)
1162+
}
1163+
1164+
if diff := cmp.Diff(tt.expectedBody, rr.Body.String()); diff != "" {
1165+
t.Errorf("%s: body differs from expected:\n%s", tt.name, diff)
1166+
}
1167+
})
1168+
}
1169+
}

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)