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},