Skip to content

Commit ff47b13

Browse files
authored
fix: skip login screen when auth header is present (#8762)
1 parent f46f99c commit ff47b13

File tree

19 files changed

+245
-36
lines changed

19 files changed

+245
-36
lines changed

charts/kubernetes-dashboard/templates/config/gateway.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ data:
3737
paths:
3838
- /api/v1/csrftoken/login
3939
strip_path: false
40+
- name: authMe
41+
paths:
42+
- /api/v1/me
43+
strip_path: false
4044
- name: api
4145
host: {{ template "kubernetes-dashboard.fullname" . }}-{{ .Values.api.role }}
4246
port: 8000

hack/gateway/dev.kong.yml

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ services:
2828
paths:
2929
- /api/v1/csrftoken/login
3030
strip_path: false
31+
- name: authMe
32+
paths:
33+
- /api/v1/me
34+
strip_path: false
3135
- name: api
3236
host: api # Internal name of docker service
3337
port: 8000

hack/gateway/prod.kong.yml

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ services:
2828
paths:
2929
- /api/v1/csrftoken/login
3030
strip_path: false
31+
- name: authMe
32+
paths:
33+
- /api/v1/me
34+
strip_path: false
3135
- name: api
3236
host: api # Internal name of docker service
3337
port: 8000

modules/auth/go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ go 1.22.0
44

55
require (
66
github.com/gin-gonic/gin v1.9.1
7+
github.com/golang-jwt/jwt/v4 v4.5.0
78
github.com/spf13/pflag v1.0.5
89
golang.org/x/net v0.21.0
910
k8s.io/dashboard/client v0.0.0-00010101000000-000000000000
1011
k8s.io/dashboard/csrf v0.0.0-00010101000000-000000000000
1112
k8s.io/dashboard/errors v0.0.0-00010101000000-000000000000
1213
k8s.io/dashboard/helpers v0.0.0-00010101000000-000000000000
14+
k8s.io/dashboard/types v0.0.0-00010101000000-000000000000
1315
k8s.io/klog/v2 v2.120.1
1416
)
1517

@@ -64,7 +66,6 @@ require (
6466
k8s.io/apiextensions-apiserver v0.29.2 // indirect
6567
k8s.io/apimachinery v0.29.2 // indirect
6668
k8s.io/client-go v0.29.2 // indirect
67-
k8s.io/dashboard/types v0.0.0-00010101000000-000000000000 // indirect
6869
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
6970
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
7071
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect

modules/auth/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
4040
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
4141
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
4242
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
43+
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
44+
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
4345
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
4446
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
4547
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=

modules/auth/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
// Importing route packages forces route registration
2828
_ "k8s.io/dashboard/auth/pkg/routes/csrftoken"
2929
_ "k8s.io/dashboard/auth/pkg/routes/login"
30+
_ "k8s.io/dashboard/auth/pkg/routes/me"
3031
)
3132

3233
func main() {

modules/auth/pkg/routes/login/handler.go

-5
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626

2727
func init() {
2828
router.V1().POST("/login", handleLogin)
29-
router.V1().GET("/login/status", handleLoginStatus)
3029
}
3130

3231
func handleLogin(c *gin.Context) {
@@ -47,7 +46,3 @@ func handleLogin(c *gin.Context) {
4746

4847
c.JSON(code, response)
4948
}
50-
51-
func handleLoginStatus(c *gin.Context) {
52-
c.JSON(http.StatusOK, "OK")
53-
}

modules/auth/pkg/routes/me/handler.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2017 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package login
16+
17+
import (
18+
"net/http"
19+
20+
"github.com/gin-gonic/gin"
21+
"k8s.io/klog/v2"
22+
23+
"k8s.io/dashboard/auth/pkg/router"
24+
)
25+
26+
func init() {
27+
router.V1().GET("/me", handleMe)
28+
}
29+
30+
func handleMe(c *gin.Context) {
31+
response, code, err := me(c.Request)
32+
if err != nil {
33+
klog.ErrorS(err, "Could not get user")
34+
c.JSON(code, err)
35+
return
36+
}
37+
38+
c.JSON(http.StatusOK, response)
39+
}

modules/auth/pkg/routes/me/me.go

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2017 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package login
16+
17+
import (
18+
"bytes"
19+
"encoding/json"
20+
"net/http"
21+
22+
"github.com/golang-jwt/jwt/v4"
23+
24+
"k8s.io/dashboard/client"
25+
"k8s.io/dashboard/errors"
26+
"k8s.io/dashboard/types"
27+
)
28+
29+
const (
30+
tokenServiceAccountKey = "serviceaccount"
31+
)
32+
33+
type ServiceAccount struct {
34+
Name string `json:"name"`
35+
UID string `json:"uid"`
36+
}
37+
38+
func me(request *http.Request) (*types.User, int, error) {
39+
k8sClient, err := client.Client(request)
40+
if err != nil {
41+
return nil, http.StatusInternalServerError, err
42+
}
43+
44+
// Make sure that authorization token is valid
45+
if _, err = k8sClient.Discovery().ServerVersion(); err != nil {
46+
code, err := errors.HandleError(err)
47+
return nil, code, err
48+
}
49+
50+
return getUserFromToken(client.GetBearerToken(request)), http.StatusOK, nil
51+
}
52+
53+
func getUserFromToken(token string) *types.User {
54+
parsed, _ := jwt.Parse(token, nil)
55+
if parsed == nil {
56+
return &types.User{Authenticated: true}
57+
}
58+
59+
claims := parsed.Claims.(jwt.MapClaims)
60+
61+
found, value := traverse(tokenServiceAccountKey, claims)
62+
if !found {
63+
return &types.User{Authenticated: true}
64+
}
65+
66+
var sa ServiceAccount
67+
ok := transcode(value, &sa)
68+
if !ok {
69+
return &types.User{Authenticated: true}
70+
}
71+
72+
return &types.User{Name: sa.Name, Authenticated: true}
73+
}
74+
75+
func traverse(key string, m map[string]interface{}) (found bool, value interface{}) {
76+
for k, v := range m {
77+
if k == key {
78+
return true, v
79+
}
80+
81+
if innerMap, ok := v.(map[string]interface{}); ok {
82+
return traverse(key, innerMap)
83+
}
84+
}
85+
86+
return false, ""
87+
}
88+
89+
func transcode(in, out interface{}) bool {
90+
buf := new(bytes.Buffer)
91+
err := json.NewEncoder(buf).Encode(in)
92+
if err != nil {
93+
return false
94+
}
95+
96+
err = json.NewDecoder(buf).Decode(out)
97+
return err == nil
98+
}

modules/common/types/common.go

+5
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,8 @@ func APIMappingByKind(kind ResourceKind) (apiMapping APIMapping, exists bool) {
8282
apiMapping, exists = kindToAPIMapping[kind]
8383
return
8484
}
85+
86+
type User struct {
87+
Name string `json:"name,omitempty"`
88+
Authenticated bool `json:"authenticated"`
89+
}

modules/web/src/chrome/userpanel/component.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import {Component, ViewChild} from '@angular/core';
1616
import {MatMenuTrigger} from '@angular/material/menu';
1717
import {AuthService} from '@common/services/global/authentication';
18+
import {MeService} from "@common/services/global/me";
1819

1920
@Component({
2021
selector: 'kd-user-panel',
@@ -25,18 +26,12 @@ export class UserPanelComponent /* implements OnInit */ {
2526
@ViewChild(MatMenuTrigger)
2627
private readonly trigger_: MatMenuTrigger;
2728

28-
constructor(private readonly authService_: AuthService) {}
29+
constructor(private readonly authService_: AuthService, private readonly _meService: MeService) {}
2930

3031
get username(): string {
31-
return ''; // todo
32+
return this._meService.getUserName()
3233
}
3334

34-
// ngOnInit(): void {
35-
// this.authService_.getLoginStatus().subscribe(status => {
36-
// this.loginStatus = status;
37-
// });
38-
// }
39-
4035
hasAuthHeader(): boolean {
4136
return this.authService_.hasAuthHeader();
4237
}

modules/web/src/chrome/userpanel/style.scss

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
}
4343

4444
.username {
45-
font-size: $body-font-size-base;
46-
margin-right: 2 * $baseline-grid;
45+
font-size: $caption-font-size-base;
46+
margin-top: .5 * $baseline-grid;
4747
}
4848

4949
.method {

modules/web/src/chrome/userpanel/template.html

+3-9
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,8 @@
3030
fxFlex
3131
fxFlexAlign=" center"
3232
fxLayout="column"
33-
class="method"
33+
class="method kd-muted-light"
3434
>
35-
<div
36-
fxFlex
37-
fxFlexAlign=" center"
38-
class="username"
39-
>
40-
{{ username }}
41-
</div>
4235
<ng-container
4336
*ngIf="hasAuthHeader()"
4437
i18n
@@ -49,6 +42,7 @@
4942
i18n
5043
>Logged in with token
5144
</ng-container>
45+
<span class="username kd-muted">{{ username }}</span>
5246
</div>
5347

5448
<button
@@ -72,7 +66,7 @@
7266
</button>
7367
<button
7468
mat-menu-item
75-
*ngIf="isAuthenticated()"
69+
*ngIf="isAuthenticated() && !hasAuthHeader()"
7670
(click)="logout()"
7771
i18n
7872
>

modules/web/src/common/services/global/authentication.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {CONFIG_DI_TOKEN} from '../../../index.config';
2525
import {CsrfTokenService} from './csrftoken';
2626
import {KdStateService} from './state';
2727
import isEmpty from 'lodash-es/isEmpty';
28+
import {MeService} from "@common/services/global/me";
2829

2930
@Injectable()
3031
export class AuthService {
@@ -36,6 +37,7 @@ export class AuthService {
3637
private readonly http_: HttpClient,
3738
private readonly csrfTokenService_: CsrfTokenService,
3839
private readonly stateService_: KdStateService,
40+
private readonly _meService: MeService,
3941
@Inject(CONFIG_DI_TOKEN) private readonly config_: IConfig
4042
) {
4143
this.stateService_.onBefore.subscribe(_ => this.refreshToken());
@@ -60,13 +62,15 @@ export class AuthService {
6062
this.setTokenCookie_(authResponse.token);
6163
}
6264

65+
this._meService.refresh();
6366
return of(void 0);
6467
})
6568
);
6669
}
6770

6871
logout(): void {
6972
this.removeTokenCookie();
73+
this._meService.reset();
7074
this.router_.navigate(['login']);
7175
}
7276

@@ -99,15 +103,11 @@ export class AuthService {
99103
}
100104

101105
isAuthenticated(): boolean {
102-
return this.hasAuthHeader() || this.hasTokenCookie();
106+
return this._meService.getUser()?.authenticated || this.hasTokenCookie();
103107
}
104108

105109
hasAuthHeader(): boolean {
106-
return this._hasAuthHeader;
107-
}
108-
109-
setHasAuthHeader(hasAuthHeader: boolean) {
110-
this._hasAuthHeader = hasAuthHeader;
110+
return this._meService.getUser()?.authenticated && !this.hasTokenCookie();
111111
}
112112

113113
private getTokenCookie(): string {

modules/web/src/common/services/global/interceptor.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,22 @@ import {CookieService} from 'ngx-cookie-service';
1919
import {Observable} from 'rxjs';
2020
import {CONFIG_DI_TOKEN} from '../../../index.config';
2121
import {AuthService} from '@common/services/global/authentication';
22+
import {MeService} from "@common/services/global/me";
2223

2324
@Injectable()
2425
export class AuthInterceptor implements HttpInterceptor {
2526
constructor(
2627
private readonly cookies_: CookieService,
2728
private readonly _authService: AuthService,
29+
private readonly _meService: MeService,
2830
@Inject(CONFIG_DI_TOKEN) private readonly appConfig_: IConfig
2931
) {}
3032

3133
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
32-
if (req.headers.get(this.appConfig_.authTokenHeaderName)) {
33-
this._authService.setHasAuthHeader(true);
34+
if (this._authService.isAuthenticated() && !this._authService.hasTokenCookie()) {
3435
return next.handle(req);
3536
}
3637

37-
this._authService.setHasAuthHeader(false);
3838
const token = this.cookies_.get(this.appConfig_.authTokenCookieName);
3939
// Filter requests made to our backend starting with 'api/v1' and append request header
4040
// with token stored in a cookie.

0 commit comments

Comments
 (0)