Skip to content

Commit 3bdfba4

Browse files
committed
Include extra attributes in SubjectAccessReview
Kubernetes authorization plugins can rely on extra attributes on a user, and these are provided via `X-Remote-Extra-` headers. Currently the Linkerd Viz `tap` API doesn't include these attributes when making the `SubjectAccessReview` request which means the Tap API cannot be used by end-users who's clusters use such authz plugins. This change updates the `tap` controller to parse the `X-Remote-Extra-` headers and include them in the SubjectAccessReview request. Fixed linkerd#13169 Signed-off-by: David Symons <[email protected]>
1 parent 58549a1 commit 3bdfba4

File tree

4 files changed

+63
-31
lines changed

4 files changed

+63
-31
lines changed

pkg/k8s/authz.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@ func ResourceAuthz(
4848
func ResourceAuthzForUser(
4949
ctx context.Context,
5050
client kubernetes.Interface,
51-
namespace, verb, group, version, resource, subresource, name, user string, userGroups []string) error {
51+
namespace, verb, group, version, resource, subresource, name, user string, userGroups []string, extra map[string]authV1.ExtraValue) error {
5252
sar := &authV1.SubjectAccessReview{
5353
Spec: authV1.SubjectAccessReviewSpec{
5454
User: user,
5555
Groups: userGroups,
56+
Extra: extra,
57+
5658
ResourceAttributes: &authV1.ResourceAttributes{
5759
Namespace: namespace,
5860
Verb: verb,

viz/tap/api/handlers.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"net/http"
8+
"net/url"
89
"strings"
910

1011
"github.com/go-openapi/spec"
@@ -17,17 +18,19 @@ import (
1718
"github.com/prometheus/client_golang/prometheus/promhttp"
1819
"github.com/sirupsen/logrus"
1920
"google.golang.org/grpc/metadata"
21+
authV1 "k8s.io/api/authorization/v1"
2022
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2123
"k8s.io/apimachinery/pkg/runtime/schema"
2224
"k8s.io/apimachinery/pkg/version"
2325
)
2426

2527
type handler struct {
26-
k8sAPI *k8s.API
27-
usernameHeader string
28-
groupHeader string
29-
grpcTapServer pb.TapServer
30-
log *logrus.Entry
28+
k8sAPI *k8s.API
29+
usernameHeader string
30+
groupHeader string
31+
extraHeaderPrefix string
32+
grpcTapServer pb.TapServer
33+
log *logrus.Entry
3134
}
3235

3336
// TODO: share with api_handlers.go
@@ -123,6 +126,17 @@ func (h *handler) handleTap(w http.ResponseWriter, req *http.Request, p httprout
123126
namespace, resource, name, h.usernameHeader, h.groupHeader,
124127
)
125128

129+
extra := make(map[string]authV1.ExtraValue)
130+
for key, values := range req.Header {
131+
if strings.HasPrefix(key, h.extraHeaderPrefix) {
132+
key, err := url.QueryUnescape(strings.TrimPrefix(key, h.extraHeaderPrefix))
133+
134+
if err != nil {
135+
extra[key] = values
136+
}
137+
}
138+
}
139+
126140
// TODO: it's possible this SubjectAccessReview is redundant, consider
127141
// removing, more info at https://github.com/linkerd/linkerd2/issues/3182
128142
err := pkgK8s.ResourceAuthzForUser(
@@ -137,6 +151,7 @@ func (h *handler) handleTap(w http.ResponseWriter, req *http.Request, p httprout
137151
name,
138152
req.Header.Get(h.usernameHeader),
139153
req.Header.Values(h.groupHeader),
154+
extra,
140155
)
141156
if err != nil {
142157
err = fmt.Errorf("tap authorization failed (%w), visit %s for more information", err, pkg.TapRbacURL)

viz/tap/api/server.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func NewServer(
5050
}
5151
}()
5252

53-
clientCAPem, allowedNames, usernameHeader, groupHeader, err := serverAuth(ctx, k8sAPI)
53+
clientCAPem, allowedNames, usernameHeader, groupHeader, extraHeaderPrefix, err := serverAuth(ctx, k8sAPI)
5454
if err != nil {
5555
return nil, err
5656
}
@@ -80,11 +80,12 @@ func NewServer(
8080

8181
var emptyCert atomic.Value
8282
h := &handler{
83-
k8sAPI: k8sAPI,
84-
usernameHeader: usernameHeader,
85-
groupHeader: groupHeader,
86-
grpcTapServer: grpcTapServer,
87-
log: log,
83+
k8sAPI: k8sAPI,
84+
usernameHeader: usernameHeader,
85+
groupHeader: groupHeader,
86+
extraHeaderPrefix: extraHeaderPrefix,
87+
grpcTapServer: grpcTapServer,
88+
log: log,
8889
}
8990

9091
lis, err := net.Listen("tcp", addr)
@@ -164,28 +165,28 @@ func (a *Server) validate(req *http.Request) error {
164165
// authentication.
165166
// kubectl -n kube-system get cm/extension-apiserver-authentication
166167
// accessible via the extension-apiserver-authentication-reader role
167-
func serverAuth(ctx context.Context, k8sAPI *k8s.API) (string, []string, string, string, error) {
168+
func serverAuth(ctx context.Context, k8sAPI *k8s.API) (string, []string, string, string, string, error) {
168169

169170
cm, err := k8sAPI.Client.CoreV1().
170171
ConfigMaps(metav1.NamespaceSystem).
171172
Get(ctx, pkgk8s.ExtensionAPIServerAuthenticationConfigMapName, metav1.GetOptions{})
172173
if err != nil {
173-
return "", nil, "", "", fmt.Errorf("failed to load [%s] config: %w", pkgk8s.ExtensionAPIServerAuthenticationConfigMapName, err)
174+
return "", nil, "", "", "", fmt.Errorf("failed to load [%s] config: %w", pkgk8s.ExtensionAPIServerAuthenticationConfigMapName, err)
174175
}
175176

176177
clientCAPem, ok := cm.Data[pkgk8s.ExtensionAPIServerAuthenticationRequestHeaderClientCAFileKey]
177178
if !ok {
178-
return "", nil, "", "", fmt.Errorf("no client CA cert available for apiextension-server")
179+
return "", nil, "", "", "", fmt.Errorf("no client CA cert available for apiextension-server")
179180
}
180181

181182
allowedNames, err := deserializeStrings(cm.Data["requestheader-allowed-names"])
182183
if err != nil {
183-
return "", nil, "", "", err
184+
return "", nil, "", "", "", err
184185
}
185186

186187
usernameHeaders, err := deserializeStrings(cm.Data["requestheader-username-headers"])
187188
if err != nil {
188-
return "", nil, "", "", err
189+
return "", nil, "", "", "", err
189190
}
190191
usernameHeader := ""
191192
if len(usernameHeaders) > 0 {
@@ -194,14 +195,23 @@ func serverAuth(ctx context.Context, k8sAPI *k8s.API) (string, []string, string,
194195

195196
groupHeaders, err := deserializeStrings(cm.Data["requestheader-group-headers"])
196197
if err != nil {
197-
return "", nil, "", "", err
198+
return "", nil, "", "", "", err
198199
}
199200
groupHeader := ""
200201
if len(groupHeaders) > 0 {
201202
groupHeader = groupHeaders[0]
202203
}
203204

204-
return clientCAPem, allowedNames, usernameHeader, groupHeader, nil
205+
extraHeaderPrefixes, err := deserializeStrings(cm.Data["requestheader-group-headers"])
206+
if err != nil {
207+
return "", nil, "", "", "", err
208+
}
209+
extraHeaderPrefix := ""
210+
if len(extraHeaderPrefixes) > 0 {
211+
extraHeaderPrefix = extraHeaderPrefixes[0]
212+
}
213+
214+
return clientCAPem, allowedNames, usernameHeader, groupHeader, extraHeaderPrefix, nil
205215
}
206216

207217
// copied from https://github.com/kubernetes/apiserver/blob/781c3cd1b3dc5b6f79c68ab0d16fe544600421ef/pkg/server/options/authentication.go#L360

viz/tap/api/server_test.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ import (
1818

1919
func TestAPIServerAuth(t *testing.T) {
2020
expectations := []struct {
21-
k8sRes []string
22-
clientCAPem string
23-
allowedNames []string
24-
usernameHeader string
25-
groupHeader string
26-
err error
21+
k8sRes []string
22+
clientCAPem string
23+
allowedNames []string
24+
usernameHeader string
25+
groupHeader string
26+
extraHeaderPrefix string
27+
err error
2728
}{
2829
{
2930
err: fmt.Errorf("failed to load [%s] config: configmaps %q not found", k8sutils.ExtensionAPIServerAuthenticationConfigMapName, k8sutils.ExtensionAPIServerAuthenticationConfigMapName),
@@ -44,11 +45,12 @@ data:
4445
requestheader-username-headers: '["X-Remote-User"]'
4546
`,
4647
},
47-
clientCAPem: "requestheader-client-ca-file",
48-
allowedNames: []string{"name1", "name2"},
49-
usernameHeader: "X-Remote-User",
50-
groupHeader: "X-Remote-Group",
51-
err: nil,
48+
clientCAPem: "requestheader-client-ca-file",
49+
allowedNames: []string{"name1", "name2"},
50+
usernameHeader: "X-Remote-User",
51+
groupHeader: "X-Remote-Group",
52+
extraHeaderPrefix: "X-Remote-Extra-",
53+
err: nil,
5254
},
5355
}
5456

@@ -62,7 +64,7 @@ data:
6264
t.Fatalf("NewFakeAPI returned an error: %s", err)
6365
}
6466

65-
clientCAPem, allowedNames, usernameHeader, groupHeader, err := serverAuth(ctx, k8sAPI)
67+
clientCAPem, allowedNames, usernameHeader, groupHeader, extraHeaderPrefix, err := serverAuth(ctx, k8sAPI)
6668

6769
if err != nil && exp.err != nil {
6870
if err.Error() != exp.err.Error() {
@@ -86,6 +88,9 @@ data:
8688
if groupHeader != exp.groupHeader {
8789
t.Errorf("apiServerAuth returned unexpected groupHeader: %q, expected: %q", groupHeader, exp.groupHeader)
8890
}
91+
if extraHeaderPrefix != exp.extraHeaderPrefix {
92+
t.Errorf("apiServerAuth returned unexpected extraHeaderPrefix: %q, expected: %q", extraHeaderPrefix, exp.extraHeaderPrefix)
93+
}
8994
})
9095
}
9196
}

0 commit comments

Comments
 (0)