From 7cdbda11adf51f47bdcf1ffaa5170c9d1f96fa50 Mon Sep 17 00:00:00 2001 From: Mike Lambert Date: Sat, 6 Feb 2021 01:07:04 -0600 Subject: [PATCH 1/7] Prune apiserver logging, roll forward os/node versions for gui, speed up apiserver and gui container builds --- apiserver/.dockerignore | 2 +- apiserver/Dockerfile | 5 ++++- apiserver/entrypoint.sh | 18 ++++++++++++++++++ apiserver/pkg/kube/kube.go | 26 +++++++++++++------------- gui/Dockerfile | 8 +++++--- gui/Dockerfile.ng-base | 6 ++++-- gui/server.js | 18 ++++++++++++++---- 7 files changed, 59 insertions(+), 24 deletions(-) diff --git a/apiserver/.dockerignore b/apiserver/.dockerignore index 4cd8861f..3916d059 100644 --- a/apiserver/.dockerignore +++ b/apiserver/.dockerignore @@ -1,4 +1,4 @@ -*/build/* +**/build/* **/vendor/* .dockerignore diff --git a/apiserver/Dockerfile b/apiserver/Dockerfile index e1fe2d07..7c260642 100644 --- a/apiserver/Dockerfile +++ b/apiserver/Dockerfile @@ -18,7 +18,9 @@ COPY glide.* ./ RUN glide install --strip-vendor # Build golang code -COPY . ./ +COPY pkg ./pkg/ +COPY cmd ./cmd/ +COPY build.sh ./ RUN ./build.sh docker # Create runtime container @@ -37,6 +39,7 @@ COPY --from=gobuild /go/src/github.com/ndslabs/apiserver/build/bin/ndslabsctl-*- COPY --from=gobuild /go/src/github.com/ndslabs/apiserver/build/bin/apiserver-linux-amd64 /usr/local/bin/apiserver COPY entrypoint.sh /entrypoint.sh +COPY postman /postman/ COPY templates /templates ENTRYPOINT ["/entrypoint.sh"] diff --git a/apiserver/entrypoint.sh b/apiserver/entrypoint.sh index 9ea36548..1545a150 100755 --- a/apiserver/entrypoint.sh +++ b/apiserver/entrypoint.sh @@ -11,6 +11,16 @@ if [ "$1" = 'apiserver' ]; then if [ -z "$ETCD_ADDR" ]; then ETCD_ADDR="localhost:4001" fi + + ETCD_HOST="$(echo $ETCD_ADDR | awk -F[/:] '{print $1}')" + if [ -z "$ETCD_HOST" ]; then + ETCD_HOST="localhost" + fi + + ETCD_PORT="$(echo $ETCD_ADDR | awk -F[/:] '{print $2}')" + if [ -z "$ETCD_PORT" ]; then + ETCD_PORT="4001" + fi if [ -z "$KUBERNETES_ADDR" ]; then KUBERNETES_ADDR="https://localhost:6443" @@ -169,6 +179,14 @@ cat << EOF > /apiserver.json } EOF + + # Wait for etcd to come online + echo "Waiting for etcd at $ETCD_ADDR..." + while ! nc -z $ETCD_HOST $ETCD_PORT; do + sleep 0.1 # wait for 1/10 of the second before check again + done + echo "Connecting to etcd at $ETCD_ADDR..." + if [ -z "$SPEC_GIT_REPO" ]; then SPEC_GIT_REPO=https://github.com/nds-org/ndslabs-specs fi diff --git a/apiserver/pkg/kube/kube.go b/apiserver/pkg/kube/kube.go index 928361a2..ba12ac7f 100644 --- a/apiserver/pkg/kube/kube.go +++ b/apiserver/pkg/kube/kube.go @@ -5,7 +5,7 @@ import ( "bytes" "crypto/tls" "encoding/base64" - "encoding/json" + // "encoding/json" "fmt" "io/ioutil" "net/http" @@ -739,25 +739,25 @@ func (k *KubeHelper) WatchEvents(handler events.EventHandler) { cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { if obj.(*v1.Pod).GetCreationTimestamp().After(startTime) { - fmt.Printf("pod added: %s \n", obj.(*v1.Pod).Name) + // fmt.Printf("pod added: %s \n", obj.(*v1.Pod).Name) handler.HandlePodEvent("ADDED", obj.(*v1.Pod)) - data, _ := json.MarshalIndent(obj.(*v1.Pod), "", " ") - fmt.Println(string(data)) + // data, _ := json.MarshalIndent(obj.(*v1.Pod), "", " ") + // fmt.Println(string(data)) } }, DeleteFunc: func(obj interface{}) { if obj.(*v1.Pod).GetDeletionTimestamp().After(startTime) { - fmt.Printf("pod deleted: %s \n", obj.(*v1.Pod).Name) + // fmt.Printf("pod deleted: %s \n", obj.(*v1.Pod).Name) handler.HandlePodEvent("DELETED", obj.(*v1.Pod)) - data, _ := json.MarshalIndent(obj.(*v1.Pod), "", " ") - fmt.Println(string(data)) + // data, _ := json.MarshalIndent(obj.(*v1.Pod), "", " ") + // fmt.Println(string(data)) } }, UpdateFunc: func(oldObj, newObj interface{}) { - fmt.Printf("pod updated: %s %s \n", oldObj.(*v1.Pod).Name, newObj.(*v1.Pod).Name) + // fmt.Printf("pod updated: %s %s \n", oldObj.(*v1.Pod).Name, newObj.(*v1.Pod).Name) handler.HandlePodEvent("UPDATED", newObj.(*v1.Pod)) - data, _ := json.MarshalIndent(newObj.(*v1.Pod), "", " ") - fmt.Println(string(data)) + // data, _ := json.MarshalIndent(newObj.(*v1.Pod), "", " ") + // fmt.Println(string(data)) }, }, ) @@ -774,16 +774,16 @@ func (k *KubeHelper) WatchEvents(handler events.EventHandler) { cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { if obj.(*v1.ReplicationController).GetCreationTimestamp().After(startTime) { - fmt.Printf("rc added: %s \n", obj.(*v1.ReplicationController).Name) + // fmt.Printf("rc added: %s \n", obj.(*v1.ReplicationController).Name) } }, DeleteFunc: func(obj interface{}) { if obj.(*v1.ReplicationController).GetDeletionTimestamp().After(startTime) { - fmt.Printf("rc deleted: %s \n", obj.(*v1.ReplicationController).Name) + // fmt.Printf("rc deleted: %s \n", obj.(*v1.ReplicationController).Name) } }, UpdateFunc: func(oldObj, newObj interface{}) { - fmt.Printf("rc changed %s -> %s\n", oldObj.(*v1.ReplicationController).Name, newObj.(*v1.ReplicationController).Name) + // fmt.Printf("rc changed %s -> %s\n", oldObj.(*v1.ReplicationController).Name, newObj.(*v1.ReplicationController).Name) }, }, ) diff --git a/gui/Dockerfile b/gui/Dockerfile index c98af3e6..a43f18d0 100644 --- a/gui/Dockerfile +++ b/gui/Dockerfile @@ -1,7 +1,7 @@ # # Build: docker build -t ndslabs/angular-ui . # -FROM ndslabs/ng-base:latest +FROM ndslabs/ng-base:focal-erbium # Set build information here before building (or at build time with --build-args version=X.X.X) ARG version="1.2.0" @@ -12,8 +12,11 @@ ENV DEBIAN_FRONTEND="noninteractive" \ NODE_ENV="production" \ BASEDIR="/home" + # Copy in our app source WORKDIR $BASEDIR +COPY package.json package-lock.json $BASEDIR/ +RUN npm install COPY . $BASEDIR/ # Update build date / version number and enable backports @@ -21,8 +24,7 @@ RUN /bin/sed -i -e "s#^\.constant('BuildVersion', '.*')#.constant('BuildVersion' /bin/sed -i -e "s#^\.constant('BuildDate', .*)#.constant('BuildDate', new Date('$(date)'))#" "$BASEDIR/ConfigModule.js" # Install app dependencies -RUN npm install && \ - grunt swagger-js-codegen +RUN grunt swagger-js-codegen # Set up some default environment variable ENV APISERVER_HOST="localhost" \ diff --git a/gui/Dockerfile.ng-base b/gui/Dockerfile.ng-base index af7058d7..a9e070b2 100644 --- a/gui/Dockerfile.ng-base +++ b/gui/Dockerfile.ng-base @@ -1,4 +1,6 @@ -FROM ubuntu:xenial +FROM ubuntu:focal + +ARG DEBIAN_FRONTEND=noninteractive # Install NodeJS and friends RUN apt-get -qq update && \ @@ -10,7 +12,7 @@ RUN apt-get -qq update && \ npm \ wget \ curl && \ - curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - && \ + curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - && \ apt-get -qq install nodejs && \ apt-get -qq autoremove && \ apt-get -qq autoclean && \ diff --git a/gui/server.js b/gui/server.js index 1c1a4967..0bd79fce 100644 --- a/gui/server.js +++ b/gui/server.js @@ -225,26 +225,35 @@ app.get('/cauth/auth', function(req, res) { let token = req.cookies['token']; logger.log("debug", "Checking user session: " + token); - let requestedUrl = req.headers['x-original-url']; + let requestedUrl = req.headers['x-original-url'] || req.headers['origin']; let prefix = (secureCookie ? 'https://' : 'http://') + subdomainPrefix; let checkHost = ''; if (!token) { res.sendStatus(401); return; - } else if (requestedUrl.indexOf(prefix+cookieDomain) !== 0) { + } else if (requestedUrl && requestedUrl.indexOf(prefix+cookieDomain) !== 0) { // if request starts with an arbitrary host (e.g. not 'subdomainPrefix.'), // we need to check authorization checkHost = requestedUrl.replace(/https?:\/\//, '').replace(/\/.*$/, ''); - //logger.log("debug", `Checking token's access to ${checkHost}`); + logger.log("info", `Checking token's access to ${checkHost}`); } + logger.log("info", `Requested URL: ${requestedUrl}`); + logger.log("info", `Prefix: ${prefix}`); + logger.log("info", `Cookie Domain: ${cookieDomain}`); + logger.log("info", `Auth-free: ${prefix+cookieDomain}`); + logger.log("info", `Checking Host: ${checkHost}`); + + let apiPathSuffix = apiPath + '/check_token' + (checkHost ? '?host=' + checkHost : ''); + logger.log("info", `CheckHost Path Suffix: ${apiPathSuffix}`); + // If token was given, check that it's valid http.get({ protocol: apiProtocol, host: apiHost, port: apiPort, - path: apiPath + '/check_token' + (checkHost ? '?host=' + checkHost : ''), + path: apiPathSuffix, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token @@ -259,6 +268,7 @@ app.get('/cauth/auth', function(req, res) { `Status Code: ${statusCode}`); } if (error) { + logger.log('error', 'CheckHost Failed') logger.log('error', error.message); // consume response data to free up memory resp.resume(); From 6aaf9c33d80b75e77f004727bf67711b870fa5f3 Mon Sep 17 00:00:00 2001 From: Mike Lambert Date: Sat, 6 Feb 2021 03:56:51 -0600 Subject: [PATCH 2/7] Stop building if an error is encountered --- apiserver/build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/build.sh b/apiserver/build.sh index 1ac9ee16..ff6d1a74 100755 --- a/apiserver/build.sh +++ b/apiserver/build.sh @@ -4,6 +4,7 @@ BUILD_DATE=`date +%Y-%m-%d\ %H:%M` VERSIONFILE="pkg/version/version.go" VERSION="1.2.0" +set -e if [ "$1" == "local" ] || [ "$1" == "docker" ]; then From b87024a14cd6156c54925f3dca2cc00c43f454ee Mon Sep 17 00:00:00 2001 From: Mike Lambert Date: Fri, 12 Feb 2021 16:43:39 -0600 Subject: [PATCH 3/7] Wait for etcd to startup before trying to connect to it --- apiserver/Dockerfile | 2 +- apiserver/entrypoint.sh | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apiserver/Dockerfile b/apiserver/Dockerfile index e1fe2d07..d82093c1 100644 --- a/apiserver/Dockerfile +++ b/apiserver/Dockerfile @@ -26,7 +26,7 @@ FROM debian:buster # Install runtime dependencies RUN apt-get -qq update && \ - apt-get -qq install --no-install-recommends binutils git ca-certificates netcat && \ + apt-get -qq install --no-install-recommends binutils git ca-certificates netcat curl && \ apt-get -qq autoremove && \ apt-get -qq autoclean && \ apt-get -qq clean all && \ diff --git a/apiserver/entrypoint.sh b/apiserver/entrypoint.sh index 9ea36548..fc1ea52b 100755 --- a/apiserver/entrypoint.sh +++ b/apiserver/entrypoint.sh @@ -169,6 +169,15 @@ cat << EOF > /apiserver.json } EOF + + # Wait for etcd to come online + echo -n "Waiting for etcd at $ETCD_ADDR..." + until $(curl -XGET --output /dev/null --silent --fail ${ETCD_ADDR}/version); do + echo -n "." + sleep 3 + done + echo -e "\netcd is online: $ETCD_ADDR" + if [ -z "$SPEC_GIT_REPO" ]; then SPEC_GIT_REPO=https://github.com/nds-org/ndslabs-specs fi @@ -185,7 +194,7 @@ EOF umask 0 if [ -z "$TEST" ]; then - apiserver -conf /apiserver.json --logtostderr=true -v=1 -passwd $ADMIN_PASSWORD + apiserver -conf /apiserver.json --logtostderr=true -v=4 -passwd $ADMIN_PASSWORD else echo "Running binary with test/coverage instrumentation" echo "Writing output to $VOLUME_PATH/coverage.out" From 871b840bc096d3ef65d7b13937b6c59010851a45 Mon Sep 17 00:00:00 2001 From: Mike Lambert Date: Fri, 12 Feb 2021 16:47:58 -0600 Subject: [PATCH 4/7] Add new ValidateOAuth endpoint that can validate oauth2-proxy cookie to authenticate users --- apiserver/cmd/server/server.go | 155 +++++++++++++++++++++++++++++++ gui/dashboard/DashboardModule.js | 46 ++++++++- gui/swagger.yaml | 14 +++ 3 files changed, 210 insertions(+), 5 deletions(-) diff --git a/apiserver/cmd/server/server.go b/apiserver/cmd/server/server.go index ac364adb..2cf639ae 100644 --- a/apiserver/cmd/server/server.go +++ b/apiserver/cmd/server/server.go @@ -329,6 +329,7 @@ func (s *Server) start(cfg *config.Config, adminPasswd string) { routes = append(routes, rest.Get(s.prefix, s.GetPaths), rest.Get(s.prefix+"version", Version), + rest.Get(s.prefix+"validate", s.ValidateOAuth), rest.Post(s.prefix+"authenticate", jwt.LoginHandler), rest.Delete(s.prefix+"authenticate", s.Logout), rest.Get(s.prefix+"check_token", s.CheckToken), @@ -537,6 +538,160 @@ func Version(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(fmt.Sprintf("%s %s", version.VERSION, version.BUILD_DATE)) } +func (s *Server) ValidateOAuth(w rest.ResponseWriter, r *rest.Request) { + oauth_cookie, err := r.Cookie("_oauth2_proxy") // cookie_segments[1] + glog.Info("Checking for OAuth2 cookie...") + + if err != nil { + glog.Error(err) + w.WriteHeader(http.StatusUnauthorized) + return + } + + oauth_url := "https://ngress-ingress-nginx-controller.kube-system.svc.cluster.local/oauth2/userinfo" + glog.Infof("Validating OAuth2 cookie: %s", oauth_url) + req, err := http.NewRequest("GET", oauth_url, nil) + if err != nil { + glog.Error(err) + w.WriteHeader(http.StatusUnauthorized) + return + } + + client := &http.Client{} + host := "www." + s.Config.Domain + req.Header.Add("Host", host) + req.AddCookie(oauth_cookie) + resp, err := client.Do(req) + if err != nil { + glog.Error(err) + w.WriteHeader(http.StatusUnauthorized) + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + glog.Error("Got response from /userinfo that was not OK: " + string(resp.StatusCode)) + w.WriteHeader(http.StatusUnauthorized) + return + } + + body_bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + glog.Error(err) + w.WriteHeader(http.StatusUnauthorized) + return + } + + var oauth_fields map[string]string + err = json.Unmarshal(body_bytes, &oauth_fields) + if err != nil { + glog.Errorf("Failed to deserialize JSON: %s\n", oauth_fields) + w.WriteHeader(http.StatusUnauthorized) + return + } + + oauth_accessToken := oauth_fields["accessToken"] + // oauth_otherTokenStr := oauth_fields["otherTokens"] + oauth_email := oauth_fields["email"] + oauth_name := strings.Split(oauth_fields["name"], "@")[0] + oauth_user := oauth_fields["preferredUsername"] + + // TODO: do we need to support rd parameter? + + if oauth_email == "" || oauth_user == "" { + glog.Warning("No oauth header found: " + oauth_name + " (" + oauth_user + ": " + oauth_email + ") ") // + oauth_accessToken) + w.WriteHeader(http.StatusUnauthorized) + return + } + +/* + tokens := make(map[string]string) + otherTokens := strings.Split(otherTokenStr, " ") + for _, kvpair := range otherTokens { + kv := strings.Split(kvpair, "=") + tokens[kv[0]] = kv[1] + } + + err := s.writeAuthPayload(user, tokens) + if err != nil { + glog.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } +*/ + + + // OAuth2 token is valid and contains everything we need, register account if necessary + glog.Infof("Creating/updating account for %s %s %s %s\n", oauth_user, oauth_email, oauth_name, oauth_accessToken) + // glog.Infof("Other tokens %s\n", otherTokens) + + + oauth_account := s.getAccountByEmail(oauth_email) + if oauth_account == nil { + act := api.Account{ + Name: oauth_name, + Description: "Oauth shadow account", // Fetch this from other OAuth scope info? + Namespace: oauth_user, + EmailAddress: oauth_email, + Password: s.kube.RandomString(10), + Organization: "", // Fetch this from other OAuth scope info? + Created: time.Now().Unix(), + LastLogin: time.Now().Unix(), + NextURL: "", // TODO: rd, + } + act.Status = api.AccountStatusApproved + + err := s.etcd.PutAccount(act.Namespace, &act, true) + if err != nil { + glog.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = s.setupAccount(&act) + if err != nil { + glog.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + } else { + oauth_account.LastLogin = time.Now().Unix() + oauth_account.NextURL = "" // TODO: rd + + err := s.etcd.PutAccount(oauth_account.Namespace, oauth_account, true) + if err != nil { + glog.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + } + + token, err := s.getTemporaryToken(oauth_user) + if err != nil { + glog.Error(err) + w.WriteHeader(http.StatusOK) + return + } + + + // Issue JWT + tokenCookie := http.Cookie{Name: "token", Value: token, Domain: s.domain, Path: "/", Secure: true, HttpOnly: false, Expires: time.Now().AddDate(0,0,1)} + namespaceCookie := http.Cookie{Name: "namespace", Value: oauth_user, Domain: s.domain, Path: "/", Secure: true, HttpOnly: false, Expires: time.Now().AddDate(0,0,1)} + + glog.Info("Setting Cookies:\n") + fmt.Println(tokenCookie) + fmt.Println(namespaceCookie) + //expiration := time.Now().Add(365 * 24 * time.Hour) + // http.SetCookie(w, &tokenCookie) + // http.SetCookie(w, &namespaceCookie) + + w.Header().Add("Set-Cookie", "token=" + token) + w.Header().Add("Set-Cookie", "namespace=" + oauth_user) + + //w.WriteHeader(http.StatusOK) + w.WriteJson(&token) + return +} + func (s *Server) CheckToken(w rest.ResponseWriter, r *rest.Request) { // Basic token validation is handled by jwt middleware userId := s.getUser(r) diff --git a/gui/dashboard/DashboardModule.js b/gui/dashboard/DashboardModule.js index 27faf42a..2813476e 100755 --- a/gui/dashboard/DashboardModule.js +++ b/gui/dashboard/DashboardModule.js @@ -130,9 +130,28 @@ angular.module('ndslabs', [ 'navbar', 'footer', 'ndslabs-services', 'ndslabs-fil // Every so often, check that our token is still valid var checkToken = function() { - NdsLabsApi.getCheckToken().then(function() { $log.debug('Token is still valid.'); }, function() { - $log.error('Token expired, redirecting to login.'); - terminateSession(); + NdsLabsApi.getCheckToken().then(function() { $log.debug('Token is still valid.'); }, function(chkTokenErr) { + NdsLabsApi.getValidate().then(function(resp) { + $log.debug('Valid OAuth2 session detected. Token issued!'); + var token = resp.data; + var tokenFields = parseJwt(token); + var username = tokenFields['user']; + + // Save to AuthInfo provider + authInfo.get().token = token; + authInfo.get().namespace = username; + + // Save as cookie + $cookies.put('token', token, CookieOptions); + $cookies.put('namespace', username, CookieOptions); + + Loading.set(ServerData.populateAll(authInfo.get().namespace)); + }, function(oauthErr) { + $log.error('Token expired, redirecting to login.'); + var token = $cookies.get('token', CookieOptions); + console.log('Detecting cookie checkToken: ' + token); + terminateSession(); + }); }); }; @@ -142,14 +161,31 @@ angular.module('ndslabs', [ 'navbar', 'footer', 'ndslabs-services', 'ndslabs-fil $window.document.title = current.$$route.title; } }); + + + var parseJwt = function(token) { + var base64Url = token.split('.')[1]; + var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + var jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + + return JSON.parse(jsonPayload); + } + // When user changes routes, check that they are still authed $rootScope.$on( "$routeChangeStart", function(event, next, current) { // Check if the token is still valid on route changes var token = $cookies.get('token', CookieOptions); + console.log('Detecting cookie $routeChangeStart: ' + token); if (token) { authInfo.get().token = token; - authInfo.get().namespace = $cookies.get('namespace', CookieOptions); + var tokenFields = parseJwt(token); + var username = tokenFields['user']; + $cookies.put('namespace', username, CookieOptions); + authInfo.get().namespace = username; + Loading.set(ServerData.populateAll(authInfo.get().namespace)); NdsLabsApi.getRefreshToken().then(function() { $log.debug('Token refreshed: ' + authInfo.get().token); Loading.set(ServerData.populateAll(authInfo.get().namespace)); @@ -162,7 +198,7 @@ angular.module('ndslabs', [ 'navbar', 'footer', 'ndslabs-services', 'ndslabs-fil authInfo.tokenInterval = authInterval = $interval(checkToken, tokenCheckMs); }, function() { - $log.debug('Failed to refresh token!'); + $log.error('Failed to refresh token!'); // TODO: Allow login page to reroute user to destination? // XXX: This would matter more if there are multiple views diff --git a/gui/swagger.yaml b/gui/swagger.yaml index 7ad19180..ed9c1b00 100644 --- a/gui/swagger.yaml +++ b/gui/swagger.yaml @@ -399,6 +399,20 @@ paths: properties: data: type: string + /validate: + get: + description: | + Check if the user has an active/valid OAuth session + responses: + '200': + description: OK + schema: + type: object + properties: + data: + $ref: '#/definitions/Token' + '401': + description: Not logged in /register: post: description: | From 6e9bbcdcd1a2a1e4911a81565777109b8db24f56 Mon Sep 17 00:00:00 2001 From: Mike Lambert Date: Sat, 13 Feb 2021 03:01:19 -0600 Subject: [PATCH 5/7] Sync --- apiserver/cmd/server/server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apiserver/cmd/server/server.go b/apiserver/cmd/server/server.go index 2cf639ae..6b6f3cb6 100644 --- a/apiserver/cmd/server/server.go +++ b/apiserver/cmd/server/server.go @@ -548,7 +548,8 @@ func (s *Server) ValidateOAuth(w rest.ResponseWriter, r *rest.Request) { return } - oauth_url := "https://ngress-ingress-nginx-controller.kube-system.svc.cluster.local/oauth2/userinfo" + host := "www." + s.Config.Domain + oauth_url := "https://" + host + "/oauth2/userinfo" glog.Infof("Validating OAuth2 cookie: %s", oauth_url) req, err := http.NewRequest("GET", oauth_url, nil) if err != nil { @@ -558,7 +559,6 @@ func (s *Server) ValidateOAuth(w rest.ResponseWriter, r *rest.Request) { } client := &http.Client{} - host := "www." + s.Config.Domain req.Header.Add("Host", host) req.AddCookie(oauth_cookie) resp, err := client.Do(req) @@ -592,8 +592,8 @@ func (s *Server) ValidateOAuth(w rest.ResponseWriter, r *rest.Request) { oauth_accessToken := oauth_fields["accessToken"] // oauth_otherTokenStr := oauth_fields["otherTokens"] oauth_email := oauth_fields["email"] - oauth_name := strings.Split(oauth_fields["name"], "@")[0] - oauth_user := oauth_fields["preferredUsername"] + oauth_name := oauth_fields["name"] + oauth_user := strings.Split(auth_fields["preferredUsername"], "@")[0] // TODO: do we need to support rd parameter? From 7c7aa3376a44277bda760cb6a11bcfb845b68b59 Mon Sep 17 00:00:00 2001 From: Mike Lambert Date: Sat, 13 Feb 2021 03:20:32 -0600 Subject: [PATCH 6/7] Handle sign)out properly when OAuth is configured --- apiserver/entrypoint.sh | 2 +- gui/shared/navbar/NavbarController.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apiserver/entrypoint.sh b/apiserver/entrypoint.sh index 497d4a0c..f7d76c53 100755 --- a/apiserver/entrypoint.sh +++ b/apiserver/entrypoint.sh @@ -192,7 +192,7 @@ EOF umask 0 if [ -z "$TEST" ]; then - apiserver -conf /apiserver.json --logtostderr=true -v=4 -passwd $ADMIN_PASSWORD + apiserver -conf /apiserver.json --logtostderr=true -v=2 -passwd $ADMIN_PASSWORD else echo "Running binary with test/coverage instrumentation" echo "Writing output to $VOLUME_PATH/coverage.out" diff --git a/gui/shared/navbar/NavbarController.js b/gui/shared/navbar/NavbarController.js index 73144543..a4f74518 100755 --- a/gui/shared/navbar/NavbarController.js +++ b/gui/shared/navbar/NavbarController.js @@ -79,7 +79,11 @@ angular $cookies.remove('namespace', CookieOptions); // TODO: Can we avoid hard-coding this URL? - $window.location.href = '/landing/'; + if ($scope.enableOAuth) { + $window.location.href = '/oauth2/sign_out'; // '/login/' + ($rootScope.rd ? 'rd=' : ''); + } else { + $window.location.href = '/landing/'; + } }; $scope.brand = From 2af123149746d4e6751333488c412545e9f6c3d6 Mon Sep 17 00:00:00 2001 From: Mike Lambert Date: Sat, 13 Feb 2021 03:30:06 -0600 Subject: [PATCH 7/7] Cleanup ValidateOAuth endpoint --- apiserver/cmd/server/server.go | 192 +++++++-------------------------- 1 file changed, 38 insertions(+), 154 deletions(-) diff --git a/apiserver/cmd/server/server.go b/apiserver/cmd/server/server.go index 6b6f3cb6..00826f04 100644 --- a/apiserver/cmd/server/server.go +++ b/apiserver/cmd/server/server.go @@ -406,21 +406,11 @@ func (s *Server) start(cfg *config.Config, adminPasswd string) { } glog.Infof("Listening on %s", cfg.Port) - // internal admin server, currently only handling oauth registration - adminsrv := &http.Server{ - Addr: ":" + cfg.AdminPort, - Handler: http.HandlerFunc(s.RegisterUserOauth), - } - glog.Infof("Admin server listening on %s", cfg.AdminPort) - stop := make(chan os.Signal, 2) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) go func() { httpsrv.ListenAndServe() }() - go func() { - adminsrv.ListenAndServe() - }() <-stop // Handle shutdown @@ -539,17 +529,16 @@ func Version(w rest.ResponseWriter, r *rest.Request) { } func (s *Server) ValidateOAuth(w rest.ResponseWriter, r *rest.Request) { - oauth_cookie, err := r.Cookie("_oauth2_proxy") // cookie_segments[1] glog.Info("Checking for OAuth2 cookie...") - + oauth_cookie, err := r.Cookie("_oauth2_proxy") // cookie_segments[1] if err != nil { glog.Error(err) w.WriteHeader(http.StatusUnauthorized) return } - host := "www." + s.Config.Domain - oauth_url := "https://" + host + "/oauth2/userinfo" + oauth_host := "https://www." + s.Config.Domain + oauth_url := oauth_host + "/oauth2/userinfo" glog.Infof("Validating OAuth2 cookie: %s", oauth_url) req, err := http.NewRequest("GET", oauth_url, nil) if err != nil { @@ -558,9 +547,10 @@ func (s *Server) ValidateOAuth(w rest.ResponseWriter, r *rest.Request) { return } - client := &http.Client{} - req.Header.Add("Host", host) + req.Header.Add("Host", "www." + s.Config.Domain) req.AddCookie(oauth_cookie) + + client := &http.Client{} resp, err := client.Do(req) if err != nil { glog.Error(err) @@ -589,21 +579,33 @@ func (s *Server) ValidateOAuth(w rest.ResponseWriter, r *rest.Request) { return } - oauth_accessToken := oauth_fields["accessToken"] - // oauth_otherTokenStr := oauth_fields["otherTokens"] oauth_email := oauth_fields["email"] - oauth_name := oauth_fields["name"] - oauth_user := strings.Split(auth_fields["preferredUsername"], "@")[0] - - // TODO: do we need to support rd parameter? - - if oauth_email == "" || oauth_user == "" { - glog.Warning("No oauth header found: " + oauth_name + " (" + oauth_user + ": " + oauth_email + ") ") // + oauth_accessToken) + if oauth_email == "" { + glog.Warning("No OAuth fields found.") // + oauth_accessToken) w.WriteHeader(http.StatusUnauthorized) return } -/* + + // Fallback to Email prefix if username not available + oauth_user := strings.Split(oauth_fields["preferredUsername"], "@")[0] + if oauth_user == "" { + oauth_user = strings.Split(oauth_email, "@")[0] + } + + // Fallback to Username if full name not available + oauth_name := oauth_fields["name"] + if oauth_name == "" { + oauth_name = oauth_user + } + + // TODO: Wire up accessToken? Is this needed for anything? + // oauth_accessToken := oauth_fields["accessToken"] + + // TODO: Wire up otherTokens. This is needed for (at least) the MDF Forge Notebook + // Assign other tokens, if presented +/* oauth_otherTokenStr := oauth_fields["otherTokens"] + if oauth_otherTokensStr != "" { tokens := make(map[string]string) otherTokens := strings.Split(otherTokenStr, " ") for _, kvpair := range otherTokens { @@ -611,18 +613,19 @@ func (s *Server) ValidateOAuth(w rest.ResponseWriter, r *rest.Request) { tokens[kv[0]] = kv[1] } + // Write token(s) to user's home directory err := s.writeAuthPayload(user, tokens) if err != nil { glog.Error(err) w.WriteHeader(http.StatusInternalServerError) return } -*/ - + } +*/ // OAuth2 token is valid and contains everything we need, register account if necessary - glog.Infof("Creating/updating account for %s %s %s %s\n", oauth_user, oauth_email, oauth_name, oauth_accessToken) - // glog.Infof("Other tokens %s\n", otherTokens) + glog.Infof("Creating/updating account for %s %s %s\n", oauth_user, oauth_email, oauth_name) //, oauth_accessToken) +// glog.V(4).Infof("Other tokens %s\n", otherTokens) oauth_account := s.getAccountByEmail(oauth_email) @@ -665,29 +668,14 @@ func (s *Server) ValidateOAuth(w rest.ResponseWriter, r *rest.Request) { } } + // Issue JWT token, err := s.getTemporaryToken(oauth_user) if err != nil { glog.Error(err) - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusUnauthorized) return } - - - // Issue JWT - tokenCookie := http.Cookie{Name: "token", Value: token, Domain: s.domain, Path: "/", Secure: true, HttpOnly: false, Expires: time.Now().AddDate(0,0,1)} - namespaceCookie := http.Cookie{Name: "namespace", Value: oauth_user, Domain: s.domain, Path: "/", Secure: true, HttpOnly: false, Expires: time.Now().AddDate(0,0,1)} - - glog.Info("Setting Cookies:\n") - fmt.Println(tokenCookie) - fmt.Println(namespaceCookie) - //expiration := time.Now().Add(365 * 24 * time.Hour) - // http.SetCookie(w, &tokenCookie) - // http.SetCookie(w, &namespaceCookie) - - w.Header().Add("Set-Cookie", "token=" + token) - w.Header().Add("Set-Cookie", "namespace=" + oauth_user) - //w.WriteHeader(http.StatusOK) w.WriteJson(&token) return } @@ -899,10 +887,7 @@ func (s *Server) createLMABasicAuthSecret() error { } func (s *Server) setupAccount(account *api.Account) error { - _, err := s.kube.CreateNamespace(account.Namespace) - if err != nil { - return err - } + s.kube.CreateNamespace(account.Namespace) // Create a PVC for this user's data storageClass := s.Config.Kubernetes.StorageClass @@ -919,19 +904,12 @@ func (s *Server) setupAccount(account *api.Account) error { StorageQuota: s.Config.DefaultLimits.StorageDefault, } } - _, err = s.kube.CreateResourceQuota(account.Namespace, + s.kube.CreateResourceQuota(account.Namespace, account.ResourceLimits.CPUMax, account.ResourceLimits.MemoryMax) - if err != nil { - return err - } - - _, err = s.kube.CreateLimitRange(account.Namespace, + s.kube.CreateLimitRange(account.Namespace, account.ResourceLimits.CPUDefault, account.ResourceLimits.MemoryDefault) - if err != nil { - return err - } return nil } @@ -3369,97 +3347,3 @@ func (s *Server) writeAuthPayload(userId string, tokens map[string]string) error }*/ return nil } - -// Register a user via oauth -func (s *Server) RegisterUserOauth(w http.ResponseWriter, r *http.Request) { - - rd := r.FormValue("rd") - if rd == "" { - rd = "https://www." + s.domain + "/dashboard" - } - - accessToken := r.Header.Get("X-Forwarded-Access-Token") - otherTokenStr := r.Header.Get("X-Forwarded-Other-Tokens") - email := r.Header.Get("X-Forwarded-Email") - user := r.Header.Get("X-Forwarded-User") - - if accessToken == "" || email == "" || user == "" { - glog.Warning("No oauth header found") - w.WriteHeader(http.StatusUnauthorized) - return - } - tokens := make(map[string]string) - otherTokens := strings.Split(otherTokenStr, " ") - for _, kvpair := range otherTokens { - kv := strings.Split(kvpair, "=") - tokens[kv[0]] = kv[1] - } - - err := s.writeAuthPayload(user, tokens) - if err != nil { - glog.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - glog.Infof("Creating/updating account for %s %s %s\n", user, email, accessToken) - glog.Infof("Other tokens %s\n", otherTokens) - - account := s.getAccountByEmail(email) - if account == nil { - act := api.Account{ - Name: user, - Description: "Oauth shadow account", - Namespace: user, - EmailAddress: email, - Password: s.kube.RandomString(10), - Organization: "", - Created: time.Now().Unix(), - LastLogin: time.Now().Unix(), - NextURL: rd, - } - act.Status = api.AccountStatusApproved - - err := s.etcd.PutAccount(act.Namespace, &act, true) - if err != nil { - glog.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - err = s.setupAccount(&act) - if err != nil { - glog.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - } else { - account.LastLogin = time.Now().Unix() - account.NextURL = rd - - err := s.etcd.PutAccount(account.Namespace, account, true) - if err != nil { - glog.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - } - - token, err := s.getTemporaryToken(user) - if err != nil { - glog.Error(err) - w.WriteHeader(http.StatusOK) - return - } - - glog.Infof("Setting Cookie\n") - //expiration := time.Now().Add(365 * 24 * time.Hour) - http.SetCookie(w, &http.Cookie{Name: "token", Value: token, Domain: s.domain, Path: "/"}) - http.SetCookie(w, &http.Cookie{Name: "namespace", Value: user, Domain: s.domain, Path: "/"}) - - glog.Infof("Redirecting to %s\n", rd) - http.Redirect(w, r, rd, 301) - return -}