This is an automated email from the ASF dual-hosted git repository.

mitchell852 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 0ffd719  Rewrote /user/login/token to Go (#3932)
0ffd719 is described below

commit 0ffd719e8f20a4fb1baa2f2098954e581f4182ab
Author: ocket8888 <[email protected]>
AuthorDate: Thu Oct 3 08:13:36 2019 -0600

    Rewrote /user/login/token to Go (#3932)
    
    * Rewrote user/login/token to Go
    
    * updated docs
    
    * Added an integration test
    
    * Added token login capability to TO Go client
    
    * updated changelog
    
    * Fixed the client build
---
 CHANGELOG.md                                     |  1 +
 docs/source/api/user_login_token.rst             | 23 ++++----
 lib/go-tc/users.go                               |  6 +++
 traffic_ops/client/session.go                    | 68 ++++++++++++++++++++++++
 traffic_ops/testing/api/v14/loginfail_test.go    | 24 +++++++++
 traffic_ops/testing/api/v14/tc-fixtures.json     |  2 +
 traffic_ops/traffic_ops_golang/auth/authorize.go | 18 +++++++
 traffic_ops/traffic_ops_golang/login/login.go    | 41 ++++++++++++++
 traffic_ops/traffic_ops_golang/routing/routes.go |  1 +
 9 files changed, 172 insertions(+), 12 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a0900e5..cc46a74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
   - /api/1.1/dbdump `GET`
   - /api/1.1/servers/:name/configfiles/ats/parent.config
   - /api/1.1/servers/:name/configfiles/ats/remap.config
+  - /api/1.1/user/login/token `POST`
 
 - To support reusing a single riak cluster connection, an optional parameter 
is added to riak.conf: "HealthCheckInterval". This options takes a 'Duration' 
value (ie: 10s, 5m) which affects how often the riak cluster is health checked. 
 Default is currently set to: "HealthCheckInterval": "5s".
 - Added a new Go db/admin binary to replace the Perl db/admin.pl script which 
is now deprecated and will be removed in a future release. The new db/admin 
binary is essentially a drop-in replacement for db/admin.pl since it supports 
all of the same commands and options; therefore, it should be used in place of 
db/admin.pl for all the same tasks.
diff --git a/docs/source/api/user_login_token.rst 
b/docs/source/api/user_login_token.rst
index 3b8498e..e527057 100644
--- a/docs/source/api/user_login_token.rst
+++ b/docs/source/api/user_login_token.rst
@@ -18,7 +18,6 @@
 ********************
 ``user/login/token``
 ********************
-.. caution:: This page is a stub! Much of it may be missing or just downright 
wrong - it needs a lot of love from people with the domain knowledge required 
to update it.
 
 ``POST``
 ========
@@ -30,7 +29,9 @@ Authentication of a user using a token. Normally, the token 
is obtained via a ca
 
 Request Structure
 -----------------
-:t: The login token
+:t: A :abbr:`UUID (Universal Unique Identifier)` generated for the user.
+
+       .. impl-detail:: Though not strictly necessary for authentication 
provided direct database access, the tokens generated for use with this 
endpoint are compliant with :RFC:`4122`.
 
 .. code-block:: http
        :caption: Request Example
@@ -54,21 +55,19 @@ Response Structure
 
        HTTP/1.1 200 OK
        Access-Control-Allow-Credentials: true
-       Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, 
Accept
+       Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, 
Accept, Set-Cookie, Cookie
        Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
        Access-Control-Allow-Origin: *
-       Cache-Control: no-cache, no-store, max-age=0, must-revalidate
        Content-Type: application/json
-       Date: Thu, 13 Dec 2018 22:16:25 GMT
-       Server: Mojolicious (Perl)
-       Set-Cookie: mojolicious=...; expires=Fri, 14 Dec 2018 02:16:25 GMT; 
path=/; HttpOnly
-       Vary: Accept-Encoding
-       Whole-Content-Sha512: 
uDowfYsW7ADmZyfahD21A+KuDdycQ3a4ma5kbPO/9RXsvgL9bqNC0Ocpi4QLxJN1Ffe1jroYoiqcnjlK9KX/5Q==
-       Content-Length: 65
+       Set-Cookie: mojolicious=...; Path=/; Expires=Fri, 20 Sep 2019 21:02:43 
GMT; HttpOnly
+       Whole-Content-Sha512: 
FuS3TkVosxHtpxRGMJ2on+WnFdYTNSPjxz/Gh1iT4UCJ2/P0twUbAGQ3tTx9EfGiAzg9CNQiVUFGnYjJZ6NCpg==
+       X-Server-Name: traffic_ops_golang/
+       Date: Fri, 20 Sep 2019 15:02:43 GMT
+       Content-Length: 66
 
        { "alerts": [
                {
-                       "level": "success",
-                       "text": "Successfully logged in."
+                       "text": "Successfully logged in.",
+                       "level": "success"
                }
        ]}
diff --git a/lib/go-tc/users.go b/lib/go-tc/users.go
index 25c3fc8..7cf6967 100644
--- a/lib/go-tc/users.go
+++ b/lib/go-tc/users.go
@@ -25,6 +25,11 @@ type UserCredentials struct {
        Password string `json:"p"`
 }
 
+// UserToken represents a request payload containing a UUID token for 
authentication
+type UserToken struct {
+       Token string `json:"t"`
+}
+
 // UserV13 contains non-nullable TO user information
 type UserV13 struct {
        Username         string    `json:"username"`
@@ -71,6 +76,7 @@ type commonUserFields struct {
        StateOrProvince *string `json:"stateOrProvince" db:"state_or_province"`
        Tenant          *string `json:"tenant"`
        TenantID        *int    `json:"tenantId" db:"tenant_id"`
+       Token           *string `json:"-" db:"token"`
        UID             *int    `json:"uid"`
        //Username        *string    `json:"username" db:"username"`  //not 
including major change due to naming incompatibility
        LastUpdated *TimeNoMod `json:"lastUpdated" db:"last_updated"`
diff --git a/traffic_ops/client/session.go b/traffic_ops/client/session.go
index 116a48c..a0acb4f 100644
--- a/traffic_ops/client/session.go
+++ b/traffic_ops/client/session.go
@@ -101,6 +101,20 @@ func loginCreds(toUser string, toPasswd string) ([]byte, 
error) {
        return js, nil
 }
 
+// loginToken gathers token login credentials for Traffic Ops.
+func loginToken(token string) ([]byte, error) {
+       form := tc.UserToken {
+               Token: token,
+       }
+
+       j, e := json.Marshal(form)
+       if e != nil {
+               e := fmt.Errorf("Error creating token login json: %v", e)
+               return nil, e
+       }
+       return j, nil
+}
+
 // Deprecated: Login is deprecated, use LoginWithAgent instead. The `Login` 
function with its present signature will be removed in the next version and 
replaced with `Login(toURL string, toUser string, toPasswd string, insecure 
bool, userAgent string)`. The `LoginWithAgent` function will be removed the 
version after that.
 func Login(toURL string, toUser string, toPasswd string, insecure bool) 
(*Session, error) {
        s, _, err := LoginWithAgent(toURL, toUser, toPasswd, insecure, 
"traffic-ops-client", false, DefaultTimeout)
@@ -142,6 +156,29 @@ func (to *Session) login() (net.Addr, error) {
        return remoteAddr, nil
 }
 
+func (to *Session) loginWithToken(token []byte) (net.Addr, error) {
+       path := apiBase + "/user/login/token"
+       resp, remoteAddr, err := to.rawRequest(http.MethodPost, path, token)
+       resp, remoteAddr, err = to.ErrUnlessOK(resp, remoteAddr, err, path)
+       if err != nil {
+               return remoteAddr, fmt.Errorf("requesting: %v", err)
+       }
+       defer resp.Body.Close()
+
+       var alerts tc.Alerts
+       if err := json.NewDecoder(resp.Body).Decode(&alerts); err != nil {
+               return remoteAddr, fmt.Errorf("decoding response JSON: %v", err)
+       }
+
+       for _, alert := range alerts.Alerts {
+               if alert.Level == tc.SuccessLevel.String() && alert.Text == 
"Successfully logged in." {
+                       return remoteAddr, nil
+               }
+       }
+
+       return remoteAddr, fmt.Errorf("Login failed, alerts string: %+v", 
alerts)
+}
+
 // logout of Traffic Ops
 func (to *Session) logout() (net.Addr, error) {
        credentials, err := loginCreds(to.UserName, to.Password)
@@ -207,6 +244,37 @@ func LoginWithAgent(toURL string, toUser string, toPasswd 
string, insecure bool,
        return to, remoteAddr, nil
 }
 
+func LoginWithToken(toURL string, token string, insecure bool, userAgent 
string, useCache bool, requestTimeout time.Duration) (*Session, net.Addr, 
error) {
+       options := cookiejar.Options {
+               PublicSuffixList: publicsuffix.List,
+       }
+
+       jar, err := cookiejar.New(&options)
+       if err != nil {
+               return nil, nil, err
+       }
+
+       client := http.Client {
+               Timeout: requestTimeout,
+               Transport: &http.Transport {
+                       TLSClientConfig: &tls.Config{InsecureSkipVerify: 
insecure},
+               },
+               Jar: jar,
+       }
+
+       to := NewSession("", "", toURL, userAgent, &client, useCache)
+       tBts, err := loginToken(token)
+       if err != nil {
+               return nil, nil, fmt.Errorf("logging in: %v", err)
+       }
+
+       remoteAddr, err := to.loginWithToken(tBts)
+       if err != nil {
+               return nil, remoteAddr, fmt.Errorf("logging in: %v", err)
+       }
+       return to, remoteAddr, nil
+}
+
 // Logout of traffic_ops
 func LogoutWithAgent(toURL string, toUser string, toPasswd string, insecure 
bool, userAgent string, useCache bool, requestTimeout time.Duration) (*Session, 
net.Addr, error) {
        options := cookiejar.Options{
diff --git a/traffic_ops/testing/api/v14/loginfail_test.go 
b/traffic_ops/testing/api/v14/loginfail_test.go
index 71bc226..13bba43 100644
--- a/traffic_ops/testing/api/v14/loginfail_test.go
+++ b/traffic_ops/testing/api/v14/loginfail_test.go
@@ -32,6 +32,7 @@ func TestLoginFail(t *testing.T) {
                PostTestLoginFail(t)
                LoginWithEmptyCredentialsTest(t)
        })
+       WithObjs(t, []TCObj{Users, Roles})
 }
 
 func PostTestLoginFail(t *testing.T) {
@@ -68,6 +69,29 @@ func LoginWithEmptyCredentialsTest(t *testing.T) {
        }
 }
 
+func LoginWithTokenTest(t *testing.T) {
+       userAgent := "to-api-v14-client-tests-loginfailtest"
+       s, _, err := toclient.LoginWithToken(Config.TrafficOps.URL, "test", 
true, userAgent, false, 
time.Second*time.Duration(Config.Default.Session.TimeoutInSecs))
+       if err != nil {
+               t.Fatalf("unexpected error when logging in with a token: %v", 
err)
+       }
+       if s == nil {
+               t.Fatalf("returned client was nil")
+       }
+
+       // disallowed token
+       _, _, err := toclient.LoginWithToken(Config.TrafficOps.URL, "quest", 
true, userAgent, false, 
time.Second*time.Duration(Config.Default.Session.TimeoutInSecs))
+       if err == nil {
+               t.Fatalf("expected an error when logging in with a disallowed 
token, actual nil")
+       }
+
+       // nonexistent token
+       _, _, err := toclient.LoginWithToken(Config.TrafficOps.URL, 
"notarealtoken", true, userAgent, false, 
time.Second*time.Duration(Config.Default.Session.TimeoutInSecs))
+       if err == nil {
+               t.Fatalf("expected an error when logging in with a nonexistent 
token, actual nil")
+       }
+}
+
 func getUninitializedTOClient(user, pass, uri, agent string, reqTimeout 
time.Duration) (*toclient.Session, error) {
        insecure := true
        useCache := false
diff --git a/traffic_ops/testing/api/v14/tc-fixtures.json 
b/traffic_ops/testing/api/v14/tc-fixtures.json
index 1e1e647..740059c 100644
--- a/traffic_ops/testing/api/v14/tc-fixtures.json
+++ b/traffic_ops/testing/api/v14/tc-fixtures.json
@@ -1986,6 +1986,7 @@
             "role": 4,
             "stateOrProvince": "LA",
             "tenant": "root",
+            "token": "test",
             "uid": 0,
             "username": "adminuser"
         },
@@ -2008,6 +2009,7 @@
             "role": 1,
             "stateOrProvince": "",
             "tenant": "tenant1",
+            "token": "quest",
             "uid": 0,
             "username": "disalloweduser"
         },
diff --git a/traffic_ops/traffic_ops_golang/auth/authorize.go 
b/traffic_ops/traffic_ops_golang/auth/authorize.go
index babeebd..4b1588f 100644
--- a/traffic_ops/traffic_ops_golang/auth/authorize.go
+++ b/traffic_ops/traffic_ops_golang/auth/authorize.go
@@ -172,7 +172,25 @@ func CheckLocalUserPassword(form PasswordForm, db 
*sqlx.DB, timeout time.Duratio
        return true, nil, nil
 }
 
+// CheckLocalUserToken checks the passed token against the records in the db 
for a match, up to a
+// maximum duration of timeout.
+func CheckLocalUserToken(token string, db *sqlx.DB, timeout time.Duration) 
(bool, string, error) {
+       dbCtx, dbClose := context.WithTimeout(context.Background(), timeout)
+       defer dbClose()
+
+       var username string
+       err := db.GetContext(dbCtx, &username, `SELECT username FROM tm_user 
WHERE token=$1 AND role!=(SELECT role.id FROM role WHERE role.name=$2)`, token, 
disallowed)
+       if err != nil {
+               if err == sql.ErrNoRows {
+                       return false, "", nil
+               }
+               return false, "", err
+       }
+       return true, username, nil
+}
+
 func sha1Hex(s string) (string, error) {
+       // SHA1 hash
        hash := sha1.New()
        if _, err := hash.Write([]byte(s)); err != nil {
                return "", err
diff --git a/traffic_ops/traffic_ops_golang/login/login.go 
b/traffic_ops/traffic_ops_golang/login/login.go
index b873ef1..463f691 100644
--- a/traffic_ops/traffic_ops_golang/login/login.go
+++ b/traffic_ops/traffic_ops_golang/login/login.go
@@ -115,6 +115,47 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) 
http.HandlerFunc {
        }
 }
 
+func TokenLoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
+       return func(w http.ResponseWriter, r *http.Request) {
+               defer r.Body.Close()
+               var t tc.UserToken
+               if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
+                       api.HandleErr(w, r, nil, http.StatusBadRequest, 
fmt.Errorf("Invalid request: %v", err), nil)
+                       return
+               }
+
+               tokenMatches, username, err := 
auth.CheckLocalUserToken(t.Token, db, 
time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second)
+               if err != nil {
+                       sysErr := fmt.Errorf("Checking token: %v", err)
+                       errCode := http.StatusInternalServerError
+                       api.HandleErr(w, r, nil, errCode, nil, sysErr)
+                       return
+               } else if !tokenMatches {
+                       userErr := errors.New("Invalid token. Please contact 
your administrator.")
+                       errCode := http.StatusUnauthorized
+                       api.HandleErr(w, r, nil, errCode, userErr, nil)
+                       return
+               }
+
+               expiry := time.Now().Add(time.Hour * 6)
+               cookie := tocookie.New(username, expiry, cfg.Secrets[0])
+               httpCookie := http.Cookie{Name: "mojolicious", Value: cookie, 
Path: "/", Expires: expiry, HttpOnly: true}
+               http.SetCookie(w, &httpCookie)
+               respBts, err := json.Marshal(tc.CreateAlerts(tc.SuccessLevel, 
"Successfully logged in."))
+               if err != nil {
+                       sysErr := fmt.Errorf("Marshaling response: %v", err)
+                       errCode := http.StatusInternalServerError
+                       api.HandleErr(w, r, nil, errCode, nil, sysErr)
+                       return
+               }
+
+               w.Header().Set(tc.ContentType, tc.ApplicationJson)
+               w.Write(append(respBts, '\n'))
+
+               // TODO: afaik, Perl never clears these tokens. They should be 
reset to NULL on login, I think.
+       }
+}
+
 // OauthLoginHandler accepts a JSON web token previously obtained from an 
OAuth provider, decodes it, validates it, authorizes the user against the 
database, and returns the login result as either an error or success message
 func OauthLoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go 
b/traffic_ops/traffic_ops_golang/routing/routes.go
index 9dd1403..57bd354 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -181,6 +181,7 @@ func Routes(d ServerData) ([]Route, []RawRoute, 
http.Handler, error) {
                {1.1, http.MethodGet, 
`user/{id}/deliveryservices/available/?(\.json)?$`, user.GetAvailableDSes, 
auth.PrivLevelReadOnly, Authenticated, nil},
                {1.1, http.MethodPost, `user/login/?$`, 
login.LoginHandler(d.DB, d.Config), 0, NoAuth, nil},
                {1.4, http.MethodPost, `user/login/oauth/?$`, 
login.OauthLoginHandler(d.DB, d.Config), 0, NoAuth, nil},
+               {1.1, http.MethodPost, `user/login/token(/|\.json)?$`, 
login.TokenLoginHandler(d.DB, d.Config), 0, NoAuth, nil},
 
                //User: CRUD
                {1.1, http.MethodGet, `users/?(\.json)?$`, 
api.ReadHandler(&user.TOUser{}), auth.PrivLevelReadOnly, Authenticated, nil},

Reply via email to