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..b785814a 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 @@ -26,7 +28,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 && \ @@ -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/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 diff --git a/apiserver/cmd/server/server.go b/apiserver/cmd/server/server.go index ac364adb..00826f04 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), @@ -405,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 @@ -537,6 +528,158 @@ 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) { + 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 + } + + 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 { + glog.Error(err) + w.WriteHeader(http.StatusUnauthorized) + return + } + + 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) + 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_email := oauth_fields["email"] + 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 { + kv := strings.Split(kvpair, "=") + 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\n", oauth_user, oauth_email, oauth_name) //, oauth_accessToken) +// glog.V(4).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 + } + } + + // Issue JWT + token, err := s.getTemporaryToken(oauth_user) + if err != nil { + glog.Error(err) + w.WriteHeader(http.StatusUnauthorized) + return + } + + 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) @@ -744,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 @@ -764,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 } @@ -3214,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 -} diff --git a/apiserver/entrypoint.sh b/apiserver/entrypoint.sh index 9ea36548..f7d76c53 100755 --- a/apiserver/entrypoint.sh +++ b/apiserver/entrypoint.sh @@ -11,7 +11,7 @@ if [ "$1" = 'apiserver' ]; then if [ -z "$ETCD_ADDR" ]; then ETCD_ADDR="localhost:4001" fi - + if [ -z "$KUBERNETES_ADDR" ]; then KUBERNETES_ADDR="https://localhost:6443" fi @@ -169,6 +169,13 @@ cat << EOF > /apiserver.json } EOF + 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 +192,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=2 -passwd $ADMIN_PASSWORD else echo "Running binary with test/coverage instrumentation" echo "Writing output to $VOLUME_PATH/coverage.out" 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/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/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(); 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 = 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: |