Skip to content

Commit

Permalink
basic auth service (kubeflow#2262)
Browse files Browse the repository at this point in the history
* [WIP] basic auth service

* complete cookie setting and auth logic

* fmt

* add ksonnet lib; fix bugs

* corrent redirect; update image

* address review feedbacks

* update comments
  • Loading branch information
kunmingg authored and Kam D Kasravi committed Feb 8, 2019
1 parent a5eb153 commit 673c933
Show file tree
Hide file tree
Showing 10 changed files with 530 additions and 0 deletions.
19 changes: 19 additions & 0 deletions components/gatekeeper/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM golang:1.11.2 as bootstrap_base

RUN mkdir -p /opt/kubeflow
RUN mkdir -p $GOPATH/src/github.com/kubeflow/kubeflow/components/gatekeeper
WORKDIR $GOPATH/src/github.com/kubeflow/kubeflow/components/gatekeeper

ENV PATH /go/bin:/usr/local/go/bin:$PATH
# use go modules
ENV GO111MODULE=on

COPY . .
RUN go mod download
RUN go build -gcflags 'all=-N -l' -o /opt/kubeflow/gatekeeper cmd/gatekeeper/main.go

RUN chmod a+rx /opt/kubeflow/gatekeeper

EXPOSE 8085

CMD ["/opt/kubeflow/gatekeeper"]
7 changes: 7 additions & 0 deletions components/gatekeeper/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
GCLOUD_PROJECT ?= kubeflow-images-public
GOLANG_VERSION ?= 1.11.2
IMG ?= gcr.io/$(GCLOUD_PROJECT)/gatekeeper
TAG ?= $(eval TAG := $(shell date +v%Y%m%d)-$(shell git describe --tags --always --dirty)-$(shell git diff | shasum -a256 | cut -c -6))$(TAG)

build:
docker build -t $(IMG):$(TAG) .
14 changes: 14 additions & 0 deletions components/gatekeeper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## Http basic Auth for Kubeflow

### To build image

make build

### Prerequisites

golang to 1.11.2

```sh
$ ☞ go version
go version go1.11.2 darwin/amd64
```
202 changes: 202 additions & 0 deletions components/gatekeeper/auth/AuthServer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Copyright 2019 The Kubeflow 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 auth

import (
"encoding/base64"
"fmt"
"github.com/kubeflow/kubeflow/components/gatekeeper/cmd/gatekeeper/options"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"
"math/rand"
"net/http"
"path"
"strings"
"sync"
"time"
)

// start with easy case: each server struct only has one valid pair of u/p
type authServer struct {
username string
// password bcrypt hash
pwhash string
// authorized cookies and their expire time (12 hour by default)
cookies map[string]time.Time
serverMux sync.Mutex
allowHttp bool
}

const CookieName = "KUBEFLOW-AUTH-KEY"
const LoginPagePath = "kflogin"
const LoginPageHeader = "x-from-login"

func NewAuthServer(opt *options.ServerOption) *authServer {
data, err := base64.StdEncoding.DecodeString(opt.Pwhash)
if err != nil {
log.Fatal("error:", err)
}
server := &authServer{
username: opt.Username,
pwhash: string(data),
cookies: make(map[string]time.Time),
allowHttp: opt.AllowHttp,
}
return server
}

// Default auth check service
func (s *authServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if (!s.allowHttp) && r.Header.Get("X-Forwarded-Proto") != "https" {
log.Infof("Redirect http traffic.")
// redirect to login page
s.redirectToLogin(w, r)
return
}
log.Infof("Path check, url: %v, path: %v", r.URL, r.URL.Path)
// login page open to everyone; all other path requires auth with Password or cookie
if strings.HasPrefix(r.URL.Path, "/" + LoginPagePath) || s.authCookie(r) == true {
// Handle user's re-login
// They already have auth cookie in browser, so "StatusResetContent" bring them to kubeflow central dashboard.
if r.Header.Get(LoginPageHeader) != "" {
w.WriteHeader(http.StatusResetContent)
w.Write([]byte(http.StatusText(http.StatusResetContent)))
return
}
// Allow browser request
log.Infof("Allow browser request")
w.WriteHeader(http.StatusOK)
w.Write([]byte(http.StatusText(http.StatusOK)))
return
}

if s.authpwd(r) == true {
log.Infof("P/W passed")
// Handle request from login page
if r.Header.Get(LoginPageHeader) != "" {
s.setCookieAndReset(w, r)
return
}
// Allow requst from API call
w.WriteHeader(http.StatusOK)
w.Write([]byte(http.StatusText(http.StatusOK)))
return
}
// If unauthorized request comes from login page, we skip redirect, just indicate username / password wrong.
if r.Header.Get(LoginPageHeader) != "" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(http.StatusText(http.StatusUnauthorized)))
return
}

log.Infof("Unauthorized, redirect to %v", "https://" + path.Join(r.Host, LoginPagePath))
// redirect to login page
s.redirectToLogin(w, r)
}

// auth with basic pw
func (s *authServer) authpwd(r *http.Request) bool {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(strings.ToLower(auth), "basic ") {
return false
}

upBytes, err := base64.StdEncoding.DecodeString(auth[len("basic "):])
if err != nil {
return false
}

namepw := strings.Split(string(upBytes), ":")

if len(namepw) != 2 {
return false
}
err = bcrypt.CompareHashAndPassword([]byte(s.pwhash), []byte(namepw[1]))
if namepw[0] == s.username && err == nil {

return true
}
return false
}

// auth with cookie
func (s *authServer) authCookie(r *http.Request) bool {
if cookie, err := r.Cookie(CookieName); err == nil {
if val, ok := s.cookies[cookie.Value]; ok {
if time.Now().Before(val) {
log.Info("cookie auth: passed! %v", cookie.Value)
return true
}
log.Info("cookie auth: cookie value expired!")
return false
}
log.Info("cookie auth: cookie value not found! %v", cookie.Value)
return false
}
log.Info("cookie auth: cookie does't exist in request!")
return false
}

// redirect to login page when unauthorized
func (s *authServer) redirectToLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://" + path.Join(r.Host, LoginPagePath), http.StatusTemporaryRedirect)
}

func generateCookieValue() string {
chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 20)
for i := range b {
b[i] = chars[rand.Intn(len(chars))]
}
return string(b)
}

func (s *authServer) addNewCookieValue(cookieVal string) {
s.serverMux.Lock()
defer s.serverMux.Unlock()
// Cookie expire after 12 hours
log.Info("cookie set: set new cookie value!")
s.cookies[cookieVal] = time.Now().Add(12 * time.Hour)
}

// Set auth cookie and reset, UI will redirect to kubeflow central dashboard
func (s *authServer) setCookieAndReset(w http.ResponseWriter, r *http.Request) {
cookieVal := generateCookieValue()
s.addNewCookieValue(cookieVal)
cookie := http.Cookie{
Name: CookieName,
Value: cookieVal,
Expires: time.Now().Add(12 * time.Hour),
Path: "/",
// prevent cross-origin information leakage.
SameSite: http.SameSiteStrictMode,
}
log.Info("set Cookie And Redirect!")
http.SetCookie(w, &cookie)
w.WriteHeader(http.StatusResetContent)
w.Write([]byte(http.StatusText(http.StatusResetContent)))
}

func (s *authServer) Start(port int) {
if port <= 0 {
log.Fatal("port must be > 0.")
}
rand.Seed(time.Now().UTC().UnixNano())
log.Info("Auth Service starts")
// All request
http.Handle("/", s)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}

42 changes: 42 additions & 0 deletions components/gatekeeper/cmd/gatekeeper/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2019 The Kubeflow 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 main

import (
"flag"
"github.com/kubeflow/kubeflow/components/gatekeeper/auth"
"github.com/kubeflow/kubeflow/components/gatekeeper/cmd/gatekeeper/options"
"github.com/onrik/logrus/filename"
log "github.com/sirupsen/logrus"
)

func init() {
// Add filename as one of the fields of the structured log message
filenameHook := filename.NewHook()
filenameHook.Field = "filename"
log.AddHook(filenameHook)
}

func main() {
sop := options.NewServerOption()
sop.AddFlags(flag.CommandLine)

flag.Parse()
if sop.Username == "" || sop.Pwhash == "" {
log.Fatal("Username or Pwhash empty, exit now")
}
s := auth.NewAuthServer(sop)
s.Start(8085)
}
36 changes: 36 additions & 0 deletions components/gatekeeper/cmd/gatekeeper/options/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2019 The Kubeflow 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 options

import "flag"

type ServerOption struct {
Username string
Pwhash string
AllowHttp bool
// Email for password reset?
// Email string
}

func NewServerOption() *ServerOption {
s := ServerOption{}
return &s
}

func (s *ServerOption) AddFlags(fs *flag.FlagSet) {
fs.StringVar(&s.Username, "username", "", "Username for login")
fs.StringVar(&s.Pwhash, "pwhash", "", "Bcrypt hash of password for login.")
fs.BoolVar(&s.AllowHttp, "allowhttp", false, "Whether or not allow http traffic. Http for test only")
}
7 changes: 7 additions & 0 deletions components/gatekeeper/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/kubeflow/kubeflow/components/gatekeeper

require (
github.com/onrik/logrus v0.2.1
github.com/sirupsen/logrus v1.3.0
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
)
13 changes: 13 additions & 0 deletions components/gatekeeper/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/onrik/logrus v0.2.1 h1:xEYR+opLvr+hNixPPAimuQppFYHaZ0XLO9hZ2G8WPLI=
github.com/onrik/logrus v0.2.1/go.mod h1:qfe9NeZVAJfIxviw3cYkZo3kvBtLoPRJriAO8zl7qTk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Loading

0 comments on commit 673c933

Please sign in to comment.