Skip to content

Commit a0a6118

Browse files
authored
fix(auth): resolve user UID via SelfSubjectReview for external authentication (#134)
* fix(auth): resolve user UID via SelfSubjectReview for BYO external auth Use authentication.k8s.io SelfSubjectReview as the primary method to obtain the authenticated user's Kubernetes UID when verifying requests to /exec/init and /activity/tick. Fall back to the OpenShift User API when SelfSubjectReview is unavailable, preserving compatibility with older clusters. Fixes Web Terminal failures with BYO External Authentication where user.openshift.io/v1 is not available. * fix : update error messages to be more generic Signed-off-by: Rohan Kumar <rohaan@redhat.com> * fix : handle empty UID case in token claims Signed-off-by: Rohan Kumar <rohaan@redhat.com> --------- Signed-off-by: Rohan Kumar <rohaan@redhat.com>
1 parent f7f6ba8 commit a0a6118

10 files changed

Lines changed: 362 additions & 218 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ kubectl exec -it <POD_NAME> <CONTAINER_NAME> -- <COMMAND>...
4343
### Authentication
4444
Endpoints that require authentication expect a user's OpenShift token to be passed in a `X-Access-Token` or `X-Forwarded-Access-Token` header on the request. This token is used to
4545

46-
1. Verify that the user making the request is the authorized user for the current terminal
46+
1. Verify that the user making the request is the authorized user for the current terminal, by resolving the user's Kubernetes UID via `SelfSubjectReview` (with fallback to the OpenShift User API when needed)
4747
2. Execute the pods/exec API call that interacts with the container into which kubeconfig is being injected (if applicable)
4848

4949
If a token is not provided or does not match what is expected, the server returns `HTTP 401`

go.mod

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,50 @@ go 1.25.7
44

55
require (
66
github.com/sirupsen/logrus v1.9.1
7-
github.com/stretchr/testify v1.8.0
8-
k8s.io/api v0.25.4
9-
k8s.io/apimachinery v0.25.4
10-
k8s.io/client-go v0.25.4
11-
sigs.k8s.io/yaml v1.2.0
7+
github.com/stretchr/testify v1.8.4
8+
k8s.io/api v0.29.14
9+
k8s.io/apimachinery v0.29.14
10+
k8s.io/client-go v0.29.14
11+
sigs.k8s.io/yaml v1.3.0
1212
)
1313

1414
require (
15-
github.com/PuerkitoBio/purell v1.1.1 // indirect
16-
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
1715
github.com/davecgh/go-spew v1.1.1 // indirect
18-
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
16+
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
1917
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
20-
github.com/go-logr/logr v1.2.3 // indirect
21-
github.com/go-openapi/jsonpointer v0.19.5 // indirect
22-
github.com/go-openapi/jsonreference v0.19.5 // indirect
23-
github.com/go-openapi/swag v0.19.14 // indirect
18+
github.com/go-logr/logr v1.3.0 // indirect
19+
github.com/go-openapi/jsonpointer v0.19.6 // indirect
20+
github.com/go-openapi/jsonreference v0.20.2 // indirect
21+
github.com/go-openapi/swag v0.22.3 // indirect
2422
github.com/gogo/protobuf v1.3.2 // indirect
25-
github.com/golang/protobuf v1.5.2 // indirect
26-
github.com/google/gnostic v0.5.7-v3refs // indirect
27-
github.com/google/gofuzz v1.1.0 // indirect
23+
github.com/golang/protobuf v1.5.4 // indirect
24+
github.com/google/gnostic-models v0.6.8 // indirect
25+
github.com/google/gofuzz v1.2.0 // indirect
26+
github.com/google/uuid v1.3.0 // indirect
27+
github.com/gorilla/websocket v1.5.0 // indirect
2828
github.com/josharian/intern v1.0.0 // indirect
2929
github.com/json-iterator/go v1.1.12 // indirect
30-
github.com/mailru/easyjson v0.7.6 // indirect
30+
github.com/mailru/easyjson v0.7.7 // indirect
3131
github.com/moby/spdystream v0.2.0 // indirect
3232
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3333
github.com/modern-go/reflect2 v1.0.2 // indirect
3434
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
35+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
3536
github.com/pkg/errors v0.9.1 // indirect
3637
github.com/pmezard/go-difflib v1.0.0 // indirect
3738
golang.org/x/net v0.38.0 // indirect
3839
golang.org/x/oauth2 v0.27.0 // indirect
3940
golang.org/x/sys v0.31.0 // indirect
4041
golang.org/x/term v0.30.0 // indirect
4142
golang.org/x/text v0.23.0 // indirect
42-
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
43+
golang.org/x/time v0.3.0 // indirect
4344
google.golang.org/protobuf v1.33.0 // indirect
4445
gopkg.in/inf.v0 v0.9.1 // indirect
4546
gopkg.in/yaml.v2 v2.4.0 // indirect
4647
gopkg.in/yaml.v3 v3.0.1 // indirect
47-
k8s.io/klog/v2 v2.70.1 // indirect
48-
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
49-
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
50-
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
51-
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
48+
k8s.io/klog/v2 v2.110.1 // indirect
49+
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
50+
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
51+
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
52+
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
5253
)

go.sum

Lines changed: 67 additions & 133 deletions
Large diffs are not rendered by default.

pkg/auth/user.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ func Authenticate(r *http.Request, clientProvider operations.ClientProvider) err
2626
}
2727
uid, err := operations.GetCurrentUserUID(token, clientProvider)
2828
if err != nil {
29-
return fmt.Errorf("unable to verify user: %s", err)
29+
logrus.Errorf("Unable to verify user: %v", err)
30+
return fmt.Errorf("unable to verify user")
3031
}
3132
if uid != config.AuthenticatedUserID {
3233
logrus.Debugf("User failed to authenticate: authorized user = '%s', requested user = '%s'", config.AuthenticatedUserID, uid)

pkg/auth/user_test.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,23 @@ import (
1717
"github.com/redhat-developer/web-terminal-exec/pkg/config"
1818
"github.com/redhat-developer/web-terminal-exec/pkg/operations/test"
1919
"github.com/stretchr/testify/assert"
20+
apierrors "k8s.io/apimachinery/pkg/api/errors"
21+
"k8s.io/apimachinery/pkg/runtime"
22+
"k8s.io/apimachinery/pkg/runtime/schema"
23+
"k8s.io/client-go/dynamic"
24+
fakedynamic "k8s.io/client-go/dynamic/fake"
25+
"k8s.io/client-go/kubernetes"
26+
"k8s.io/client-go/kubernetes/fake"
27+
"k8s.io/client-go/rest"
28+
k8stesting "k8s.io/client-go/testing"
2029
)
2130

2231
func TestAuthenticate(t *testing.T) {
2332
tests := []struct {
2433
name string
2534
headers http.Header
2635
errRegexp string
36+
provider operationsClientProvider
2737
}{
2838
{
2939
name: "No auth token in request",
@@ -35,6 +45,12 @@ func TestAuthenticate(t *testing.T) {
3545
headers: http.Header{"X-Access-Token": []string{"incorrect"}},
3646
errRegexp: "the current user is not authorized to access this web terminal",
3747
},
48+
{
49+
name: "SelfSubjectReview failure",
50+
headers: http.Header{"X-Access-Token": []string{testToken}},
51+
errRegexp: "unable to verify user",
52+
provider: selfSubjectReviewErrorClientProvider{},
53+
},
3854
{
3955
name: "Correct token",
4056
headers: http.Header{"X-Access-Token": []string{testToken}},
@@ -45,17 +61,48 @@ func TestAuthenticate(t *testing.T) {
4561
config.AuthenticatedUserID = testToken
4662
defer config.ResetConfigForTest()
4763

48-
clientProvider := test.FakeClientProvider{
49-
UserToken: testToken,
64+
clientProvider := tt.provider
65+
if clientProvider == nil {
66+
clientProvider = test.FakeClientProvider{
67+
UserToken: testToken,
68+
}
5069
}
5170
req := &http.Request{Header: tt.headers}
5271
err := Authenticate(req, clientProvider)
5372
if tt.errRegexp != "" {
5473
assert.Error(t, err)
5574
assert.Regexp(t, tt.errRegexp, err.Error())
75+
if tt.name == "SelfSubjectReview failure" {
76+
assert.Equal(t, "unable to verify user", err.Error())
77+
assert.NotContains(t, err.Error(), "not found")
78+
}
5679
} else {
5780
assert.NoError(t, err)
5881
}
5982
})
6083
}
6184
}
85+
86+
type operationsClientProvider interface {
87+
NewDevWorkspaceClient() (dynamic.Interface, *rest.Config, error)
88+
NewClientWithToken(token string) (kubernetes.Interface, *rest.Config, error)
89+
NewOpenShiftUserClient(token string) (dynamic.Interface, *rest.Config, error)
90+
}
91+
92+
type selfSubjectReviewErrorClientProvider struct{}
93+
94+
func (selfSubjectReviewErrorClientProvider) NewDevWorkspaceClient() (dynamic.Interface, *rest.Config, error) {
95+
return nil, nil, nil
96+
}
97+
98+
func (selfSubjectReviewErrorClientProvider) NewClientWithToken(string) (kubernetes.Interface, *rest.Config, error) {
99+
client := fake.NewSimpleClientset()
100+
client.PrependReactor("create", "selfsubjectreviews", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
101+
return true, nil, apierrors.NewNotFound(schema.GroupResource{Group: "authentication.k8s.io", Resource: "selfsubjectreviews"}, "self")
102+
})
103+
return client, &rest.Config{}, nil
104+
}
105+
106+
func (selfSubjectReviewErrorClientProvider) NewOpenShiftUserClient(string) (dynamic.Interface, *rest.Config, error) {
107+
return fakedynamic.NewSimpleDynamicClient(&runtime.Scheme{}), &rest.Config{}, nil
108+
}

pkg/operations/clients.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func (defaultClientProvider) NewOpenShiftUserClient(token string) (dynamic.Inter
111111
}
112112

113113
config.APIPath = "/apis"
114-
config.GroupVersion = &devworkspaceGroupVersion
114+
config.GroupVersion = &userGroupVersion
115115
config.BearerToken = token
116116
config.BearerTokenFile = ""
117117

pkg/operations/operations.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"strings"
1919

2020
"github.com/redhat-developer/web-terminal-exec/pkg/config"
21+
authenticationv1 "k8s.io/api/authentication/v1"
2122
corev1 "k8s.io/api/core/v1"
2223
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2324
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -116,14 +117,59 @@ func GetCurrentWorkspacePod(client kubernetes.Interface) (*corev1.Pod, error) {
116117
}
117118

118119
func GetCurrentUserUID(token string, clientProvider ClientProvider) (string, error) {
120+
uid, err := getCurrentUserUIDFromSelfSubjectReview(token, clientProvider)
121+
if err == nil {
122+
return uid, nil
123+
}
124+
125+
// Fall back to the OpenShift User API on clusters where SelfSubjectReview is unavailable.
126+
uid, fallbackErr := getCurrentUserUIDFromOpenShiftUserAPI(token, clientProvider)
127+
if fallbackErr == nil {
128+
return uid, nil
129+
}
130+
131+
return "", fmt.Errorf(
132+
"failed to get current user information: SelfSubjectReview error: %w; OpenShift User API error: %w",
133+
err,
134+
fallbackErr,
135+
)
136+
}
137+
138+
func getCurrentUserUIDFromSelfSubjectReview(token string, clientProvider ClientProvider) (string, error) {
139+
client, _, err := clientProvider.NewClientWithToken(token)
140+
if err != nil {
141+
return "", fmt.Errorf("failed to create client to check user info: %w", err)
142+
}
143+
review, err := client.AuthenticationV1().SelfSubjectReviews().Create(
144+
context.Background(),
145+
&authenticationv1.SelfSubjectReview{},
146+
metav1.CreateOptions{},
147+
)
148+
if err != nil {
149+
return "", err
150+
}
151+
152+
if review.Status.UserInfo.UID == "" {
153+
return "", fmt.Errorf("SelfSubjectReview returned empty UID")
154+
}
155+
156+
return review.Status.UserInfo.UID, nil
157+
}
158+
159+
func getCurrentUserUIDFromOpenShiftUserAPI(token string, clientProvider ClientProvider) (string, error) {
119160
userClient, _, err := clientProvider.NewOpenShiftUserClient(token)
120161
if err != nil {
121-
return "", fmt.Errorf("failed to create client to check user info")
162+
return "", err
122163
}
123-
userInfo, err := userClient.Resource(userGVR).Namespace("").Get(context.TODO(), "~", metav1.GetOptions{})
164+
userInfo, err := userClient.Resource(userGVR).Namespace("").Get(context.Background(), "~", metav1.GetOptions{})
124165
if err != nil {
125-
return "", fmt.Errorf("failed to get current user information: %s", err)
166+
return "", err
167+
}
168+
169+
uid := string(userInfo.GetUID())
170+
if uid == "" {
171+
return "", fmt.Errorf("OpenShift User API returned empty UID")
126172
}
127173

128-
return string(userInfo.GetUID()), nil
174+
return uid, nil
129175
}

0 commit comments

Comments
 (0)