Skip to content

Commit

Permalink
fix: skip login screen when auth header is present (#8762)
Browse files Browse the repository at this point in the history
  • Loading branch information
floreks committed Mar 7, 2024
1 parent f46f99c commit ff47b13
Show file tree
Hide file tree
Showing 19 changed files with 245 additions and 36 deletions.
4 changes: 4 additions & 0 deletions charts/kubernetes-dashboard/templates/config/gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ data:
paths:
- /api/v1/csrftoken/login
strip_path: false
- name: authMe
paths:
- /api/v1/me
strip_path: false
- name: api
host: {{ template "kubernetes-dashboard.fullname" . }}-{{ .Values.api.role }}
port: 8000
Expand Down
4 changes: 4 additions & 0 deletions hack/gateway/dev.kong.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ services:
paths:
- /api/v1/csrftoken/login
strip_path: false
- name: authMe
paths:
- /api/v1/me
strip_path: false
- name: api
host: api # Internal name of docker service
port: 8000
Expand Down
4 changes: 4 additions & 0 deletions hack/gateway/prod.kong.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ services:
paths:
- /api/v1/csrftoken/login
strip_path: false
- name: authMe
paths:
- /api/v1/me
strip_path: false
- name: api
host: api # Internal name of docker service
port: 8000
Expand Down
3 changes: 2 additions & 1 deletion modules/auth/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ go 1.22.0

require (
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/spf13/pflag v1.0.5
golang.org/x/net v0.21.0
k8s.io/dashboard/client v0.0.0-00010101000000-000000000000
k8s.io/dashboard/csrf v0.0.0-00010101000000-000000000000
k8s.io/dashboard/errors v0.0.0-00010101000000-000000000000
k8s.io/dashboard/helpers v0.0.0-00010101000000-000000000000
k8s.io/dashboard/types v0.0.0-00010101000000-000000000000
k8s.io/klog/v2 v2.120.1
)

Expand Down Expand Up @@ -64,7 +66,6 @@ require (
k8s.io/apiextensions-apiserver v0.29.2 // indirect
k8s.io/apimachinery v0.29.2 // indirect
k8s.io/client-go v0.29.2 // indirect
k8s.io/dashboard/types v0.0.0-00010101000000-000000000000 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
Expand Down
2 changes: 2 additions & 0 deletions modules/auth/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
Expand Down
1 change: 1 addition & 0 deletions modules/auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
// Importing route packages forces route registration
_ "k8s.io/dashboard/auth/pkg/routes/csrftoken"
_ "k8s.io/dashboard/auth/pkg/routes/login"
_ "k8s.io/dashboard/auth/pkg/routes/me"
)

func main() {
Expand Down
5 changes: 0 additions & 5 deletions modules/auth/pkg/routes/login/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (

func init() {
router.V1().POST("/login", handleLogin)
router.V1().GET("/login/status", handleLoginStatus)
}

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

c.JSON(code, response)
}

func handleLoginStatus(c *gin.Context) {
c.JSON(http.StatusOK, "OK")
}
39 changes: 39 additions & 0 deletions modules/auth/pkg/routes/me/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2017 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package login

import (
"net/http"

"github.com/gin-gonic/gin"
"k8s.io/klog/v2"

"k8s.io/dashboard/auth/pkg/router"
)

func init() {
router.V1().GET("/me", handleMe)
}

func handleMe(c *gin.Context) {
response, code, err := me(c.Request)
if err != nil {
klog.ErrorS(err, "Could not get user")
c.JSON(code, err)
return
}

c.JSON(http.StatusOK, response)
}
98 changes: 98 additions & 0 deletions modules/auth/pkg/routes/me/me.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2017 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package login

import (
"bytes"
"encoding/json"
"net/http"

"github.com/golang-jwt/jwt/v4"

"k8s.io/dashboard/client"
"k8s.io/dashboard/errors"
"k8s.io/dashboard/types"
)

const (
tokenServiceAccountKey = "serviceaccount"
)

type ServiceAccount struct {
Name string `json:"name"`
UID string `json:"uid"`
}

func me(request *http.Request) (*types.User, int, error) {
k8sClient, err := client.Client(request)
if err != nil {
return nil, http.StatusInternalServerError, err
}

// Make sure that authorization token is valid
if _, err = k8sClient.Discovery().ServerVersion(); err != nil {
code, err := errors.HandleError(err)
return nil, code, err
}

return getUserFromToken(client.GetBearerToken(request)), http.StatusOK, nil
}

func getUserFromToken(token string) *types.User {
parsed, _ := jwt.Parse(token, nil)
if parsed == nil {
return &types.User{Authenticated: true}
}

claims := parsed.Claims.(jwt.MapClaims)

found, value := traverse(tokenServiceAccountKey, claims)
if !found {
return &types.User{Authenticated: true}
}

var sa ServiceAccount
ok := transcode(value, &sa)
if !ok {
return &types.User{Authenticated: true}
}

return &types.User{Name: sa.Name, Authenticated: true}
}

func traverse(key string, m map[string]interface{}) (found bool, value interface{}) {
for k, v := range m {
if k == key {
return true, v
}

if innerMap, ok := v.(map[string]interface{}); ok {
return traverse(key, innerMap)
}
}

return false, ""
}

func transcode(in, out interface{}) bool {
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(in)
if err != nil {
return false
}

err = json.NewDecoder(buf).Decode(out)
return err == nil
}
5 changes: 5 additions & 0 deletions modules/common/types/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,8 @@ func APIMappingByKind(kind ResourceKind) (apiMapping APIMapping, exists bool) {
apiMapping, exists = kindToAPIMapping[kind]
return
}

type User struct {
Name string `json:"name,omitempty"`
Authenticated bool `json:"authenticated"`
}
11 changes: 3 additions & 8 deletions modules/web/src/chrome/userpanel/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import {Component, ViewChild} from '@angular/core';
import {MatMenuTrigger} from '@angular/material/menu';
import {AuthService} from '@common/services/global/authentication';
import {MeService} from "@common/services/global/me";

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

constructor(private readonly authService_: AuthService) {}
constructor(private readonly authService_: AuthService, private readonly _meService: MeService) {}

get username(): string {
return ''; // todo
return this._meService.getUserName()
}

// ngOnInit(): void {
// this.authService_.getLoginStatus().subscribe(status => {
// this.loginStatus = status;
// });
// }

hasAuthHeader(): boolean {
return this.authService_.hasAuthHeader();
}
Expand Down
4 changes: 2 additions & 2 deletions modules/web/src/chrome/userpanel/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
}

.username {
font-size: $body-font-size-base;
margin-right: 2 * $baseline-grid;
font-size: $caption-font-size-base;
margin-top: .5 * $baseline-grid;
}

.method {
Expand Down
12 changes: 3 additions & 9 deletions modules/web/src/chrome/userpanel/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,8 @@
fxFlex
fxFlexAlign=" center"
fxLayout="column"
class="method"
class="method kd-muted-light"
>
<div
fxFlex
fxFlexAlign=" center"
class="username"
>
{{ username }}
</div>
<ng-container
*ngIf="hasAuthHeader()"
i18n
Expand All @@ -49,6 +42,7 @@
i18n
>Logged in with token
</ng-container>
<span class="username kd-muted">{{ username }}</span>
</div>

<button
Expand All @@ -72,7 +66,7 @@
</button>
<button
mat-menu-item
*ngIf="isAuthenticated()"
*ngIf="isAuthenticated() && !hasAuthHeader()"
(click)="logout()"
i18n
>
Expand Down
12 changes: 6 additions & 6 deletions modules/web/src/common/services/global/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {CONFIG_DI_TOKEN} from '../../../index.config';
import {CsrfTokenService} from './csrftoken';
import {KdStateService} from './state';
import isEmpty from 'lodash-es/isEmpty';
import {MeService} from "@common/services/global/me";

@Injectable()
export class AuthService {
Expand All @@ -36,6 +37,7 @@ export class AuthService {
private readonly http_: HttpClient,
private readonly csrfTokenService_: CsrfTokenService,
private readonly stateService_: KdStateService,
private readonly _meService: MeService,
@Inject(CONFIG_DI_TOKEN) private readonly config_: IConfig
) {
this.stateService_.onBefore.subscribe(_ => this.refreshToken());
Expand All @@ -60,13 +62,15 @@ export class AuthService {
this.setTokenCookie_(authResponse.token);
}

this._meService.refresh();
return of(void 0);
})
);
}

logout(): void {
this.removeTokenCookie();
this._meService.reset();
this.router_.navigate(['login']);
}

Expand Down Expand Up @@ -99,15 +103,11 @@ export class AuthService {
}

isAuthenticated(): boolean {
return this.hasAuthHeader() || this.hasTokenCookie();
return this._meService.getUser()?.authenticated || this.hasTokenCookie();
}

hasAuthHeader(): boolean {
return this._hasAuthHeader;
}

setHasAuthHeader(hasAuthHeader: boolean) {
this._hasAuthHeader = hasAuthHeader;
return this._meService.getUser()?.authenticated && !this.hasTokenCookie();
}

private getTokenCookie(): string {
Expand Down
6 changes: 3 additions & 3 deletions modules/web/src/common/services/global/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,22 @@ import {CookieService} from 'ngx-cookie-service';
import {Observable} from 'rxjs';
import {CONFIG_DI_TOKEN} from '../../../index.config';
import {AuthService} from '@common/services/global/authentication';
import {MeService} from "@common/services/global/me";

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(
private readonly cookies_: CookieService,
private readonly _authService: AuthService,
private readonly _meService: MeService,
@Inject(CONFIG_DI_TOKEN) private readonly appConfig_: IConfig
) {}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.headers.get(this.appConfig_.authTokenHeaderName)) {
this._authService.setHasAuthHeader(true);
if (this._authService.isAuthenticated() && !this._authService.hasTokenCookie()) {
return next.handle(req);
}

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

0 comments on commit ff47b13

Please sign in to comment.