This is an automated email from the ASF dual-hosted git repository.
srijeet0406 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 f9a450b JWT Authorization (#6577)
f9a450b is described below
commit f9a450b6822317e99663b5e92ac0065b351f758a
Author: mattjackson220 <[email protected]>
AuthorDate: Fri Mar 11 11:16:22 2022 -0700
JWT Authorization (#6577)
* Added the ability to use a JWT for authorization and added to CDNi
operations
* updated changelog
* fixed older versions
* updated per comments
* updated changelog
* updated per comments
* updated to remove authorization header
* updated per comments
* removed autofocus
* fixed logout
* Added ability to use Authorization Header instead of just cookies
* updated per comments
* updated to protect for NPE
* updated per comments
* updated per comments
* fixed test
* fixed after rebase
---
CHANGELOG.md | 1 +
docs/source/admin/traffic_ops.rst | 1 -
docs/source/api/v4/oc_ci_configuration.rst | 24 ++++---
docs/source/api/v4/oc_ci_configuration_host.rst | 24 ++++---
docs/source/api/v4/oc_fci_advertisement.rst | 26 +++----
docs/source/api/v4/users.rst | 4 +-
docs/source/api/v4/users_id.rst | 18 +++++
lib/go-rfc/http.go | 1 +
lib/go-tc/users.go | 1 +
traffic_ops/app/conf/cdn.conf | 3 +-
...022021611354000_add_user_to_ucdn_table.down.sql | 18 +++++
.../2022021611354000_add_user_to_ucdn_table.up.sql | 18 +++++
traffic_ops/traffic_ops_golang/api/api.go | 79 ++++++++++++++++++++--
traffic_ops/traffic_ops_golang/auth/authorize.go | 22 ++++--
traffic_ops/traffic_ops_golang/cdni/shared.go | 59 ++++++++++++----
traffic_ops/traffic_ops_golang/config/config.go | 3 +-
traffic_ops/traffic_ops_golang/login/login.go | 48 +++++++++++--
traffic_ops/traffic_ops_golang/login/logout.go | 9 +++
.../routing/middleware/wrappers_test.go | 2 +-
traffic_ops/traffic_ops_golang/user/user.go | 14 ++--
.../common/modules/form/user/form.user.tpl.html | 8 +++
traffic_portal/test/integration/Data/users.ts | 1 +
.../test/integration/PageObjects/UsersPage.po.ts | 3 +
23 files changed, 315 insertions(+), 72 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb316a5..5d31645 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ The format is based on [Keep a
Changelog](http://keepachangelog.com/en/1.0.0/).
- Traffic Monitor config option `distributed_polling` which enables the
ability for Traffic Monitor to poll a subset of the CDN and divide into "local
peer groups" and "distributed peer groups". Traffic Monitors in the same group
are local peers, while Traffic Monitors in other groups are distibuted peers.
Each TM group polls the same set of cachegroups and gets availability data for
the other cachegroups from other TM groups. This allows each TM to be
responsible for polling a subset of [...]
- Added support for a new Traffic Ops GLOBAL profile parameter --
`tm_query_status_override` -- to override which status of Traffic Monitors to
query (default: ONLINE).
- Traffic Router: Add support for `file`-protocol URLs for the
`geolocation.polling.url` for the Geolocation database.
+- Added functionality for login to provide a Bearer token and for that token
to be later used for authorization.
### Fixed
- Update traffic\_portal dependencies to mitigate `npm audit` issues.
diff --git a/docs/source/admin/traffic_ops.rst
b/docs/source/admin/traffic_ops.rst
index 1579ff5..623a630 100644
--- a/docs/source/admin/traffic_ops.rst
+++ b/docs/source/admin/traffic_ops.rst
@@ -500,7 +500,6 @@ This file deals with the configuration parameters of
running Traffic Ops itself.
.. versionadded:: 6.2
:dcdn_id: A string representing this :abbr:`CDN (Content Delivery
Network)` to be used in the :abbr:`JWT (JSON Web Token)` and subsequently in
:abbr:`CDNi (Content Delivery Network Interconnect)` operations.
- :jwt_decoding_secret: A string used to decode the :abbr:`JWT (JSON Web
Token)` to get information for :abbr:`CDNi (Content Delivery Network
Interconnect)` operations.
Example cdn.conf
diff --git a/docs/source/api/v4/oc_ci_configuration.rst
b/docs/source/api/v4/oc_ci_configuration.rst
index 25d9dfc..c497785 100644
--- a/docs/source/api/v4/oc_ci_configuration.rst
+++ b/docs/source/api/v4/oc_ci_configuration.rst
@@ -23,22 +23,28 @@
=======
Triggers an asynchronous task to update the configuration for the :abbr:`uCDN
(Upstream Content Delivery Network)` by adding the request to a queue to be
reviewed later. This returns a 202 Accepted status and an endpoint to be used
for status updates.
+.. note:: Users with the ``ICDN:UCDN-OVERRIDE`` permission will need to
provide a "ucdn" query parameter to bypass the need for :abbr:`uCDN (Upstream
Content Delivery Network)` information in the :abbr:`JWT (JSON Web Token)` and
allow them to view all :abbr:`CDNi (Content Delivery Network Interconnect)`
information.
+
:Auth. Required: Yes
:Roles Required: "admin" or "operations"
:Permissions Required: CDNI:UPDATE
:Response Type: Object
-:Headers Required: "Authorization"
Request Structure
-----------------
-.. table:: Request Required Headers
-
-
+-----------------+------------------------------------------------------------------------------------------------------------------------------+
- | Name | Description
|
-
+=================+==============================================================================================================================+
- | Authorization | A :abbr:`JWT (JSON Web Token)` provided by the
:abbr:`dCDN (Downstream Content Delivery Network)` to identify the |
- | | :abbr:`uCDN (Upstream Content Delivery Network)`
|
-
+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+This requires authorization using a :abbr:`JWT (JSON Web Token)` provided by
the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the
:abbr:`uCDN (Upstream Content Delivery Network)`. This token must include the
following claims:
+
+.. table:: Required JWT claims
+
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
+ | Name | Description
|
+
+=================+====================================================================================================================+
+ | iss | Issuer claim as a string key for the :abbr:`uCDN
(Upstream Content Delivery Network)` |
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
+ | aud | Audience claim as a string key for the :abbr:`dCDN
(Downstream Content Delivery Network)` |
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
+ | exp | Expiration claim as the expiration date as a Unix
epoch timestamp (in seconds) |
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
:type: A string of the type of metadata to follow. See :rfc:`8006` for
possible values. Only a selection of these are supported.
:host: A string of the domain that the requested updates will change.
diff --git a/docs/source/api/v4/oc_ci_configuration_host.rst
b/docs/source/api/v4/oc_ci_configuration_host.rst
index a44ae961..688e2bf 100644
--- a/docs/source/api/v4/oc_ci_configuration_host.rst
+++ b/docs/source/api/v4/oc_ci_configuration_host.rst
@@ -23,22 +23,28 @@
=======
Triggers an asynchronous task to update the configuration for the :abbr:`uCDN
(Upstream Content Delivery Network)` and the specified host by adding the
request to a queue to be reviewed later. This returns a 202 Accepted status and
an endpoint to be used for status updates.
+.. note:: Users with the ``ICDN:UCDN-OVERRIDE`` permission will need to
provide a "ucdn" query parameter to bypass the need for :abbr:`uCDN (Upstream
Content Delivery Network)` information in the :abbr:`JWT (JSON Web Token)` and
allow them to view all :abbr:`CDNi (Content Delivery Network Interconnect)`
information.
+
:Auth. Required: Yes
:Roles Required: "admin" or "operations"
:Permissions Required: CDNI:UPDATE
:Response Type: Object
-:Headers Required: "Authorization"
Request Structure
-----------------
-.. table:: Request Required Headers
-
-
+-----------------+------------------------------------------------------------------------------------------------------------------------------+
- | Name | Description
|
-
+=================+==============================================================================================================================+
- | Authorization | A :abbr:`JWT (JSON Web Token)` provided by the
:abbr:`dCDN (Downstream Content Delivery Network)` to identify the |
- | | :abbr:`uCDN (Upstream Content Delivery Network)`
|
-
+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+This requires authorization using a :abbr:`JWT (JSON Web Token)` provided by
the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the
:abbr:`uCDN (Upstream Content Delivery Network)`. This token must include the
following claims:
+
+.. table:: Required JWT claims
+
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
+ | Name | Description
|
+
+=================+====================================================================================================================+
+ | iss | Issuer claim as a string key for the :abbr:`uCDN
(Upstream Content Delivery Network)` |
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
+ | aud | Audience claim as a string key for the :abbr:`dCDN
(Downstream Content Delivery Network)` |
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
+ | exp | Expiration claim as the expiration date as a Unix
epoch timestamp (in seconds) |
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
.. table:: Request Path Parameters
diff --git a/docs/source/api/v4/oc_fci_advertisement.rst
b/docs/source/api/v4/oc_fci_advertisement.rst
index 319c7a9..f58e339 100644
--- a/docs/source/api/v4/oc_fci_advertisement.rst
+++ b/docs/source/api/v4/oc_fci_advertisement.rst
@@ -23,26 +23,28 @@
=======
Returns the complete footprint and capabilities information structure the
:abbr:`dCDN (Downstream Content Delivery Network)` wants to expose to a given
:abbr:`uCDN (Upstream Content Delivery Network)`.
+.. note:: Users with the ``ICDN:UCDN-OVERRIDE`` permission will need to
provide a "ucdn" query parameter to bypass the need for :abbr:`uCDN (Upstream
Content Delivery Network)` information in the :abbr:`JWT (JSON Web Token)` and
allow them to view all :abbr:`CDNi (Content Delivery Network Interconnect)`
information.
+
:Auth. Required: No
:Roles Required: "admin" or "operations"
:Permissions Required: CDNI:READ
:Response Type: Array
-:Headers Required: "Authorization"
Request Structure
-----------------
-.. table:: Request Required Headers
+This requires authorization using a :abbr:`JWT (JSON Web Token)` provided by
the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the
:abbr:`uCDN (Upstream Content Delivery Network)`. This token must include the
following claims:
+
+.. table:: Required JWT claims
-
+-----------------+------------------------------------------------------------------------------------------------------------------------------+
- | Name | Description
|
-
+=================+==============================================================================================================================+
- | Authorization | A :abbr:`JWT (JSON Web Token)` provided by the
:abbr:`dCDN (Downstream Content Delivery Network)` to identify the |
- | | :abbr:`uCDN (Upstream Content Delivery Network)`.
This token must include the following claims: |
- | |
|
- | | - ``iss`` Issuer claim as a string key for the
:abbr:`uCDN (Upstream Content Delivery Network)` |
- | | - ``aud`` Audience claim as a string key for the
:abbr:`dCDN (Downstream Content Delivery Network)` |
- | | - ``exp`` Expiration claim as the expiration date
as a Unix epoch timestamp (in seconds) |
-
+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
+ | Name | Description
|
+
+=================+====================================================================================================================+
+ | iss | Issuer claim as a string key for the :abbr:`uCDN
(Upstream Content Delivery Network)` |
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
+ | aud | Audience claim as a string key for the :abbr:`dCDN
(Downstream Content Delivery Network)` |
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
+ | exp | Expiration claim as the expiration date as a Unix
epoch timestamp (in seconds) |
+
+-----------------+--------------------------------------------------------------------------------------------------------------------+
Response Structure
------------------
diff --git a/docs/source/api/v4/users.rst b/docs/source/api/v4/users.rst
index 5df71a1..a10335f 100644
--- a/docs/source/api/v4/users.rst
+++ b/docs/source/api/v4/users.rst
@@ -90,6 +90,7 @@ Response Structure
:stateOrProvince: The name of the state or province where this user resides
:tenant: The name of the tenant to which this user belongs
:tenantId: The integral, unique identifier of the tenant to which
this user belongs
+:ucdn: The name of the :abbr:`uCDN (Upstream Content Delivery
Network)` to which the user belongs
:uid: A deprecated field only kept for legacy compatibility
reasons that used to contain the UNIX user ID of the user - now it is always
``null``
:username: The user's username
@@ -168,7 +169,8 @@ Request Structure
.. note:: This field is optional if and only if tenancy is not enabled
in Traffic Control
-:username: The new user's username
+:ucdn: An optional field (only used if :abbr:`CDNi (Content
Delivery Network Interconnect)` is in use) containing the name of the
:abbr:`uCDN (Upstream Content Delivery Network)` to which the user belongs
+:username: The new user's username
.. code-block:: http
:caption: Request Example
diff --git a/docs/source/api/v4/users_id.rst b/docs/source/api/v4/users_id.rst
index a668d70..b9a0ffb 100644
--- a/docs/source/api/v4/users_id.rst
+++ b/docs/source/api/v4/users_id.rst
@@ -69,6 +69,12 @@ Response Structure
:stateOrProvince: The name of the state or province where this user resides
:tenant: The name of the tenant to which this user belongs
:tenantId: The integral, unique identifier of the tenant to which
this user belongs
+:ucdn: The name of the :abbr:`uCDN (Upstream Content Delivery
Network)` to which the user belongs
+
+ .. versionadded:: 6.2
+
+ .. note:: This field is optional and only used if :abbr:`CDNi (Content
Delivery Network Interconnect)` is in use.
+
:uid: A deprecated field only kept for legacy compatibility
reasons that used to contain the UNIX user ID of the user - now it is always
``null``
:username: The user's username
@@ -152,6 +158,12 @@ Request Structure
.. note:: This field is optional if and only if tenancy is not enabled
in Traffic Control
+:ucdn: The name of the :abbr:`uCDN (Upstream Content Delivery
Network)` to which the user belongs
+
+ .. versionadded:: 6.2
+
+ .. note:: This field is optional and only used if :abbr:`CDNi (Content
Delivery Network Interconnect)` is in use.
+
:username: The user's username
.. code-block:: http
@@ -203,6 +215,12 @@ Response Structure
:stateOrProvince: The name of the state or province where this user resides
:tenant: The name of the tenant to which this user belongs
:tenantId: The integral, unique identifier of the tenant to which this
user belongs
+:ucdn: The name of the :abbr:`uCDN (Upstream Content Delivery
Network)` to which the user belongs
+
+ .. versionadded:: 6.2
+
+ .. note:: This field is optional and only used if :abbr:`CDNi (Content
Delivery Network Interconnect)` is in use.
+
:uid: A deprecated field only kept for legacy compatibility
reasons that used to contain the UNIX user ID of the user - now it is always
``null``
:username: The user's username
diff --git a/lib/go-rfc/http.go b/lib/go-rfc/http.go
index 08fa281..9590245 100644
--- a/lib/go-rfc/http.go
+++ b/lib/go-rfc/http.go
@@ -42,6 +42,7 @@ const (
Vary = "Vary" // RFC7231§7.1.4
Age = "Age" // RFC7234§5.1
Location = "Location" // RFC7231§7.1.2
+ Authorization = "Authorization" // RFC7235§4.2
)
// These are (some) valid values for content encoding and MIME types, for
diff --git a/lib/go-tc/users.go b/lib/go-tc/users.go
index 2b2b953..ba1b0d1 100644
--- a/lib/go-tc/users.go
+++ b/lib/go-tc/users.go
@@ -294,6 +294,7 @@ type UserV40 struct {
Tenant *string `json:"tenant"`
TenantID int `json:"tenantId" db:"tenant_id"`
Token *string `json:"-" db:"token"`
+ UCDN string `json:"ucdn"`
UID *int `json:"uid"`
Username string `json:"username" db:"username"`
}
diff --git a/traffic_ops/app/conf/cdn.conf b/traffic_ops/app/conf/cdn.conf
index 08fc3e3..3a349f8 100644
--- a/traffic_ops/app/conf/cdn.conf
+++ b/traffic_ops/app/conf/cdn.conf
@@ -100,7 +100,6 @@
"state" : ""
},
"cdni" : {
- "dcdn_id" : "",
- "jwt_decoding_secret" : ""
+ "dcdn_id" : ""
}
}
diff --git
a/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.down.sql
b/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.down.sql
new file mode 100644
index 0000000..4d8916c
--- /dev/null
+++
b/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.down.sql
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you 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.
+ */
+
+ALTER TABLE tm_user DROP COLUMN IF EXISTS ucdn;
diff --git
a/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.up.sql
b/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.up.sql
new file mode 100644
index 0000000..e588817
--- /dev/null
+++
b/traffic_ops/app/db/migrations/2022021611354000_add_user_to_ucdn_table.up.sql
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you 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.
+ */
+
+ALTER TABLE tm_user ADD COLUMN IF NOT EXISTS ucdn text NOT NULL DEFAULT '';
diff --git a/traffic_ops/traffic_ops_golang/api/api.go
b/traffic_ops/traffic_ops_golang/api/api.go
index ff66ceb..0a7d3ce 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -48,6 +48,7 @@ import (
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault/backends/disabled"
+ "github.com/dgrijalva/jwt-go"
influx "github.com/influxdata/influxdb/client/v2"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
@@ -81,6 +82,11 @@ const (
TrafficVaultContextKey = "tv"
)
+const (
+ MojoCookie = "mojoCookie"
+ AccessToken = "access_token"
+)
+
const influxServersQuery = `
SELECT (host_name||'.'||domain_name) as fqdn,
tcp_port,
@@ -1029,23 +1035,52 @@ func ParseDBError(ierr error) (error, error, int) {
// GetUserFromReq returns the current user, any user error, any system error,
and an error code to be returned if either error was not nil.
// This also uses the given ResponseWriter to refresh the cookie, if it was
valid.
func GetUserFromReq(w http.ResponseWriter, r *http.Request, secret string)
(auth.CurrentUser, error, error, int) {
- cookie, err := r.Cookie(tocookie.Name)
- if err != nil {
- return auth.CurrentUser{}, errors.New("Unauthorized, please log
in."), errors.New("error getting cookie: " + err.Error()),
http.StatusUnauthorized
+ var cookie *http.Cookie
+
+ if r.Header.Get(rfc.Authorization) != "" &&
strings.Contains(r.Header.Get(rfc.Authorization), "Bearer") {
+ givenToken := r.Header.Get(rfc.Authorization)
+ tokenSplit := strings.Split(givenToken, " ")
+ if len(tokenSplit) > 1 {
+ givenToken = tokenSplit[1]
+ }
+ bearerCookie, err := getCookieFromAccessToken(givenToken,
secret)
+ if err != nil {
+ return auth.CurrentUser{}, errors.New("unauthorized,
please log in."), err, http.StatusUnauthorized
+ }
+ cookie = bearerCookie
+ } else {
+ for _, givenCookie := range r.Cookies() {
+ if cookie != nil {
+ break
+ }
+ if givenCookie == nil {
+ continue
+ }
+ switch givenCookie.Name {
+ case AccessToken:
+ bearerCookie, err :=
getCookieFromAccessToken(givenCookie.Value, secret)
+ if err != nil {
+ return auth.CurrentUser{},
errors.New("unauthorized, please log in."), err, http.StatusUnauthorized
+ }
+ cookie = bearerCookie
+ case tocookie.Name:
+ cookie = givenCookie
+ }
+ }
}
if cookie == nil {
- return auth.CurrentUser{}, errors.New("Unauthorized, please log
in."), nil, http.StatusUnauthorized
+ return auth.CurrentUser{}, errors.New("unauthorized, please log
in."), nil, http.StatusUnauthorized
}
oldCookie, err := tocookie.Parse(secret, cookie.Value)
if err != nil {
- return auth.CurrentUser{}, errors.New("Unauthorized, please log
in."), errors.New("error parsing cookie: " + err.Error()),
http.StatusUnauthorized
+ return auth.CurrentUser{}, errors.New("unauthorized, please log
in."), errors.New("error parsing cookie: " + err.Error()),
http.StatusUnauthorized
}
username := oldCookie.AuthData
if username == "" {
- return auth.CurrentUser{}, errors.New("Unauthorized, please log
in."), nil, http.StatusUnauthorized
+ return auth.CurrentUser{}, errors.New("unauthorized, please log
in."), nil, http.StatusUnauthorized
}
db := (*sqlx.DB)(nil)
val := r.Context().Value(DBContextKey)
@@ -1075,6 +1110,38 @@ func GetUserFromReq(w http.ResponseWriter, r
*http.Request, secret string) (auth
return user, nil, nil, http.StatusOK
}
+func getCookieFromAccessToken(bearerToken string, secret string)
(*http.Cookie, error) {
+ var cookie *http.Cookie
+ claims := jwt.MapClaims{}
+ token, err := jwt.ParseWithClaims(bearerToken, claims, func(token
*jwt.Token) (interface{}, error) {
+ return []byte(secret), nil
+ })
+ if err != nil {
+ return nil, fmt.Errorf("parsing claims: %w", err)
+ }
+ if token == nil {
+ return nil, errors.New("parsing claims: parsed nil token")
+ }
+ if !token.Valid {
+ return nil, errors.New("invalid token")
+ }
+
+ for key, val := range claims {
+ switch key {
+ case MojoCookie:
+ mojoVal, ok := val.(string)
+ if !ok {
+ return nil, errors.New("invalid token - " +
MojoCookie + " must be a string")
+ }
+ cookie = &http.Cookie{
+ Value: mojoVal,
+ }
+ }
+ }
+
+ return cookie, nil
+}
+
func AddUserToReq(r *http.Request, u auth.CurrentUser) {
ctx := r.Context()
ctx = context.WithValue(ctx, auth.CurrentUserKey, u)
diff --git a/traffic_ops/traffic_ops_golang/auth/authorize.go
b/traffic_ops/traffic_ops_golang/auth/authorize.go
index 0801ed1..df230a1 100644
--- a/traffic_ops/traffic_ops_golang/auth/authorize.go
+++ b/traffic_ops/traffic_ops_golang/auth/authorize.go
@@ -44,6 +44,7 @@ type CurrentUser struct {
Role int `json:"role" db:"role"`
RoleName string `json:"roleName" db:"role_name"`
Capabilities pq.StringArray `json:"capabilities" db:"capabilities"`
+ UCDN string `json:"ucdn" db:"ucdn"`
perms map[string]struct{}
}
@@ -115,7 +116,8 @@ SELECT
u.id,
u.username,
COALESCE(u.tenant_id, -1) AS tenant_id,
- ARRAY(SELECT rc.cap_name FROM role_capability AS rc WHERE rc.role_id=r.id)
AS capabilities
+ ARRAY(SELECT rc.cap_name FROM role_capability AS rc WHERE rc.role_id=r.id)
AS capabilities,
+ u.ucdn
FROM
tm_user AS u
JOIN
@@ -126,14 +128,14 @@ WHERE
var currentUserInfo CurrentUser
if DB == nil {
- return CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid,
-1, "", []string{}, nil}, nil, errors.New("no db provided to
GetCurrentUserFromDB"), http.StatusInternalServerError
+ return CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid,
-1, "", []string{}, "", nil}, nil, errors.New("no db provided to
GetCurrentUserFromDB"), http.StatusInternalServerError
}
dbCtx, dbClose := context.WithTimeout(context.Background(), timeout)
defer dbClose()
err := DB.GetContext(dbCtx, ¤tUserInfo, qry, user)
if err != nil {
- invalidUser := CurrentUser{"-", -1, PrivLevelInvalid,
TenantIDInvalid, -1, "", []string{}, nil}
+ invalidUser := CurrentUser{"-", -1, PrivLevelInvalid,
TenantIDInvalid, -1, "", []string{}, "", nil}
if errors.Is(err, sql.ErrNoRows) {
return invalidUser, errors.New("user not found"),
fmt.Errorf("checking user %v info: user not in database", user),
http.StatusUnauthorized
}
@@ -160,7 +162,7 @@ func GetCurrentUser(ctx context.Context) (*CurrentUser,
error) {
return nil, fmt.Errorf("CurrentUser found with bad
type: %T", v)
}
}
- return &CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "",
[]string{}, nil}, errors.New("No user found in Context")
+ return &CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "",
[]string{}, "", nil}, errors.New("No user found in Context")
}
func CheckLocalUserIsAllowed(form PasswordForm, db *sqlx.DB, ctx
context.Context) (bool, error, error) {
@@ -181,6 +183,18 @@ func CheckLocalUserIsAllowed(form PasswordForm, db
*sqlx.DB, ctx context.Context
return false, nil, nil
}
+// GetUserUcdn returns the Upstream CDN to which the user belongs for CDNi
operations.
+func GetUserUcdn(form PasswordForm, db *sqlx.DB, ctx context.Context) (string,
error) {
+ var ucdn string
+
+ err := db.GetContext(ctx, &ucdn, "SELECT ucdn FROM tm_user where
username=$1", form.Username)
+ if err != nil {
+ return "", err
+ }
+
+ return ucdn, nil
+}
+
func CheckLocalUserPassword(form PasswordForm, db *sqlx.DB, ctx
context.Context) (bool, error, error) {
var hashedPassword string
diff --git a/traffic_ops/traffic_ops_golang/cdni/shared.go
b/traffic_ops/traffic_ops_golang/cdni/shared.go
index bcf3161..0775d6e 100644
--- a/traffic_ops/traffic_ops_golang/cdni/shared.go
+++ b/traffic_ops/traffic_ops_golang/cdni/shared.go
@@ -33,7 +33,6 @@ import (
"github.com/apache/trafficcontrol/lib/go-rfc"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
-
"github.com/dgrijalva/jwt-go"
"github.com/lib/pq"
)
@@ -73,14 +72,16 @@ func GetCapabilities(w http.ResponseWriter, r
*http.Request) {
}
defer inf.Close()
- if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" ||
inf.Config.Cdni.DCdnId == "" {
+ if inf.Config.Cdni == nil || inf.Config.Secrets[0] == "" ||
inf.Config.Cdni.DCdnId == "" {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError,
nil, errors.New("cdn.conf does not contain CDNi information"))
return
}
- ucdn, err := checkBearerToken(r.Header.Get("Authorization"), inf)
+ bearerToken := getBearerToken(r)
+
+ ucdn, err := checkBearerToken(bearerToken, inf)
if err != nil {
- api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError,
nil, err)
+ api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
return
}
@@ -106,6 +107,24 @@ func GetCapabilities(w http.ResponseWriter, r
*http.Request) {
api.WriteRespRaw(w, r, fciCaps)
}
+func getBearerToken(r *http.Request) string {
+ if r.Header.Get(rfc.Authorization) != "" &&
strings.Contains(r.Header.Get(rfc.Authorization), "Bearer") {
+ givenTokenSplit :=
strings.Split(r.Header.Get(rfc.Authorization), " ")
+ if len(givenTokenSplit) < 2 {
+ return ""
+ }
+
+ return givenTokenSplit[1]
+ }
+ for _, cookie := range r.Cookies() {
+ switch cookie.Name {
+ case api.AccessToken:
+ return cookie.Value
+ }
+ }
+ return ""
+}
+
func PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
if userErr != nil || sysErr != nil {
@@ -120,14 +139,15 @@ func PutHostConfiguration(w http.ResponseWriter, r
*http.Request) {
return
}
- if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" ||
inf.Config.Cdni.DCdnId == "" {
+ if inf.Config.Cdni == nil || inf.Config.Secrets[0] == "" ||
inf.Config.Cdni.DCdnId == "" {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError,
nil, errors.New("cdn.conf does not contain CDNi information"))
return
}
- ucdn, err := checkBearerToken(r.Header.Get("Authorization"), inf)
+ bearerToken := getBearerToken(r)
+ ucdn, err := checkBearerToken(bearerToken, inf)
if err != nil {
- api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError,
nil, err)
+ api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
return
}
@@ -190,14 +210,15 @@ func PutConfiguration(w http.ResponseWriter, r
*http.Request) {
}
defer inf.Close()
- if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" ||
inf.Config.Cdni.DCdnId == "" {
+ if inf.Config.Cdni == nil || inf.Config.Secrets[0] == "" ||
inf.Config.Cdni.DCdnId == "" {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError,
nil, errors.New("cdn.conf does not contain CDNi information"))
return
}
- ucdn, err := checkBearerToken(r.Header.Get("Authorization"), inf)
+ bearerToken := getBearerToken(r)
+ ucdn, err := checkBearerToken(bearerToken, inf)
if err != nil {
- api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError,
nil, err)
+ api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil)
return
}
@@ -472,12 +493,12 @@ func validateHostExists(host string, tx *sql.Tx) (int,
error, error) {
func checkBearerToken(bearerToken string, inf *api.APIInfo) (string, error) {
if bearerToken == "" {
- return "", errors.New("bearer token header is required")
+ return "", errors.New("bearer token is required")
}
claims := jwt.MapClaims{}
token, err := jwt.ParseWithClaims(bearerToken, claims, func(token
*jwt.Token) (interface{}, error) {
- return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
+ return []byte(inf.Config.Secrets[0]), nil
})
if err != nil {
return "", fmt.Errorf("parsing claims: %w", err)
@@ -517,8 +538,20 @@ func checkBearerToken(bearerToken string, inf
*api.APIInfo) (string, error) {
if dcdn != inf.Config.Cdni.DCdnId {
return "", errors.New("invalid token - incorrect dcdn")
}
+
+ if ucdn != inf.User.UCDN {
+ return "", errors.New("user ucdn did not match token ucdn")
+ }
+
if ucdn == "" {
- return "", errors.New("invalid token - empty ucdn field")
+ if inf.User.Can("ICDN:UCDN-OVERRIDE") {
+ ucdn = inf.Params["ucdn"]
+ if ucdn == "" {
+ return "", errors.New("admin level ucdn
requests require a ucdn query parameter")
+ }
+ } else {
+ return "", errors.New("invalid token - empty ucdn
field")
+ }
}
return ucdn, nil
diff --git a/traffic_ops/traffic_ops_golang/config/config.go
b/traffic_ops/traffic_ops_golang/config/config.go
index 4062565..147d876 100644
--- a/traffic_ops/traffic_ops_golang/config/config.go
+++ b/traffic_ops/traffic_ops_golang/config/config.go
@@ -240,8 +240,7 @@ type ConfigInflux struct {
}
type CdniConf struct {
- DCdnId string `json:"dcdn_id"`
- JwtDecodingSecret string `json:"jwt_decoding_secret"`
+ DCdnId string `json:"dcdn_id"`
}
// NewFakeConfig returns a fake Config struct with just enough data to view
Routes.
diff --git a/traffic_ops/traffic_ops_golang/login/login.go
b/traffic_ops/traffic_ops_golang/login/login.go
index 72661b4..2afcd45 100644
--- a/traffic_ops/traffic_ops_golang/login/login.go
+++ b/traffic_ops/traffic_ops_golang/login/login.go
@@ -42,9 +42,10 @@ import (
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tocookie"
+ jwt "github.com/dgrijalva/jwt-go"
"github.com/jmoiron/sqlx"
"github.com/lestrrat-go/jwx/jwk"
- "github.com/lestrrat-go/jwx/jwt"
+ ljwt "github.com/lestrrat-go/jwx/jwt"
)
type emailFormatter struct {
@@ -156,6 +157,39 @@ func LoginHandler(db *sqlx.DB, cfg config.Config)
http.HandlerFunc {
httpCookie := tocookie.GetCookie(form.Username,
defaultCookieDuration, cfg.Secrets[0])
http.SetCookie(w, httpCookie)
+ var jwtToken *jwt.Token
+ var jwtSigned string
+ claims := jwt.MapClaims{}
+
+ emptyConf := config.CdniConf{}
+ if cfg.Cdni != nil && *cfg.Cdni != emptyConf {
+ ucdn, err := auth.GetUserUcdn(form, db,
dbCtx)
+ if err != nil {
+ // log but do not error out
since this is optional in the JWT for CDNi integration
+ log.Errorf("getting ucdn for
user %s: %v", form.Username, err)
+ }
+ claims["iss"] = ucdn
+ claims["aud"] = cfg.Cdni.DCdnId
+ }
+
+ claims["exp"] = httpCookie.Expires.Unix()
+ claims[api.MojoCookie] = httpCookie.Value
+ jwtToken =
jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+
+ jwtSigned, err =
jwtToken.SignedString([]byte(cfg.Secrets[0]))
+ if err != nil {
+ api.HandleErr(w, r, nil,
http.StatusInternalServerError, nil, err)
+ return
+ }
+
+ http.SetCookie(w, &http.Cookie{
+ Name: api.AccessToken,
+ Value: jwtSigned,
+ Path: "/",
+ MaxAge: httpCookie.MaxAge,
+ HttpOnly: true, // prevents the cookie
being accessed by Javascript. DO NOT remove, security vulnerability
+ })
+
// If all's well until here, then update last
authenticated time
tx, txErr := db.BeginTx(dbCtx, nil)
if txErr != nil {
@@ -370,15 +404,15 @@ func OauthLoginHandler(db *sqlx.DB, cfg config.Config)
http.HandlerFunc {
if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
log.Warnf("Error parsing JSON response from oAuth: %s",
err.Error())
encodedToken = buf.String()
- } else if _, ok := result["access_token"]; !ok {
+ } else if _, ok := result[api.AccessToken]; !ok {
sysErr := fmt.Errorf("Missing access token in response:
%s\n", buf.String())
usrErr := errors.New("Bad response from OAuth2.0
provider")
api.HandleErr(w, r, nil, http.StatusBadGateway, usrErr,
sysErr)
return
} else {
- switch t := result["access_token"].(type) {
+ switch t := result[api.AccessToken].(type) {
case string:
- encodedToken = result["access_token"].(string)
+ encodedToken = result[api.AccessToken].(string)
default:
sysErr := fmt.Errorf("Incorrect type of
access_token! Expected 'string', got '%v'\n", t)
usrErr := errors.New("Bad response from
OAuth2.0 provider")
@@ -392,10 +426,10 @@ func OauthLoginHandler(db *sqlx.DB, cfg config.Config)
http.HandlerFunc {
return
}
- decodedToken, err := jwt.Parse(
+ decodedToken, err := ljwt.Parse(
[]byte(encodedToken),
- jwt.WithVerifyAuto(true),
- jwt.WithJWKSetFetcher(jwksFetcher),
+ ljwt.WithVerifyAuto(true),
+ ljwt.WithJWKSetFetcher(jwksFetcher),
)
if err != nil {
api.HandleErr(w, r, nil,
http.StatusInternalServerError, nil, errors.New("Error decoding token with
message: "+err.Error()))
diff --git a/traffic_ops/traffic_ops_golang/login/logout.go
b/traffic_ops/traffic_ops_golang/login/logout.go
index 955d153..348c811 100644
--- a/traffic_ops/traffic_ops_golang/login/logout.go
+++ b/traffic_ops/traffic_ops_golang/login/logout.go
@@ -23,6 +23,7 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "time"
"github.com/apache/trafficcontrol/lib/go-rfc"
"github.com/apache/trafficcontrol/lib/go-tc"
@@ -42,6 +43,14 @@ func LogoutHandler(secret string) http.HandlerFunc {
cookie := tocookie.GetCookie(inf.User.UserName, 0, secret)
http.SetCookie(w, cookie)
+ http.SetCookie(w, &http.Cookie{
+ Name: "access_token",
+ Value: "",
+ Path: "/",
+ Expires: time.Now().Add(0),
+ MaxAge: 0,
+ HttpOnly: true, // prevents the cookie being accessed
by Javascript. DO NOT remove, security vulnerability
+ })
resp := struct {
tc.Alerts
}{tc.CreateAlerts(tc.SuccessLevel, "You are logged out.")}
diff --git a/traffic_ops/traffic_ops_golang/routing/middleware/wrappers_test.go
b/traffic_ops/traffic_ops_golang/routing/middleware/wrappers_test.go
index ee677e3..29303e8 100644
--- a/traffic_ops/traffic_ops_golang/routing/middleware/wrappers_test.go
+++ b/traffic_ops/traffic_ops_golang/routing/middleware/wrappers_test.go
@@ -234,7 +234,7 @@ func TestWrapAuth(t *testing.T) {
f(w, r)
- expectedError := `{"alerts":[{"text":"Unauthorized, please log
in.","level":"error"}]}` + "\n"
+ expectedError := `{"alerts":[{"text":"unauthorized, please log
in.","level":"error"}]}` + "\n"
if *debugLogging {
fmt.Printf("received: %s\n expected: %s\n", w.Body.Bytes(),
expectedError)
diff --git a/traffic_ops/traffic_ops_golang/user/user.go
b/traffic_ops/traffic_ops_golang/user/user.go
index 746c48e..8cd9df7 100644
--- a/traffic_ops/traffic_ops_golang/user/user.go
+++ b/traffic_ops/traffic_ops_golang/user/user.go
@@ -527,7 +527,8 @@ func UpdateQueryV40() string {
postal_code=:postal_code,
country=:country,
tenant_id=:tenant_id,
- local_passwd=COALESCE(:local_passwd, local_passwd)
+ local_passwd=COALESCE(:local_passwd, local_passwd),
+ ucdn=:ucdn
WHERE id=:id
RETURNING last_updated,
(SELECT t.name FROM tenant t WHERE id = u.tenant_id),
@@ -552,7 +553,8 @@ func InsertQueryV40() string {
postal_code,
country,
tenant_id,
- local_passwd
+ local_passwd,
+ ucdn
) VALUES (
:username,
:public_ssh_key,
@@ -569,7 +571,8 @@ func InsertQueryV40() string {
:postal_code,
:country,
:tenant_id,
- :local_passwd
+ :local_passwd,
+ :ucdn
) RETURNING id, last_updated,
(SELECT t.name FROM tenant t WHERE id = tm_user.tenant_id),
(SELECT r.name FROM role r WHERE id = tm_user.role)`
@@ -598,7 +601,8 @@ SELECT
u.registration_sent,
u.tenant_id,
t.name AS tenant,
- u.last_updated,`
+ u.last_updated,
+ u.ucdn,`
const readQuery = readBaseQuery + `
u.last_authenticated,
@@ -607,7 +611,7 @@ r.name as role
FROM tm_user u
LEFT JOIN tenant t ON u.tenant_id = t.id
LEFT JOIN role r ON u.role = r.id
-LEFT JOIN role_capability rc on rc.role_id = r.id
+LEFT JOIN role_capability rc ON rc.role_id = r.id
`
const legacyReadQuery = readBaseQuery + `
diff --git a/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
b/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
index dd98f71..b1d4c1c 100644
--- a/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
@@ -82,6 +82,14 @@ under the License.
<small ng-show="user.tenantId"><a
href="/#!/tenants/{{user.tenantId}}" target="_blank">View Details <i
class="fa fs-xs fa-external-link"></i></a></small>
</div>
</div>
+ <div class="form-group" ng-class="{'has-error':
hasError(userForm.uCDN), 'has-feedback': hasError(userForm.uCDN)}">
+ <label for="uCDN" class="control-label col-md-2 col-sm-2
col-xs-12">Upstream CDN</label>
+ <div class="col-md-10 col-sm-10 col-xs-12">
+ <input id="uCDN" name="uCDN" type="text"
class="form-control" ng-model="user.ucdn" ng-pattern="/^\S*$/">
+ <small class="input-error"
ng-show="hasPropertyError(userForm.ucdn, 'pattern')">No Spaces</small>
+ <span ng-show="hasError(userForm.ucdn)"
class="form-control-feedback"><i class="fa fa-times"></i></span>
+ </div>
+ </div>
<div class="form-group" ng-class="{'has-error':
hasError(userForm.uPass), 'has-feedback': hasError(userForm.uPass)}">
<label class="control-label col-md-2 col-sm-2
col-xs-12">Password <span ng-if="settings.isNew">*</span></label>
<div class="col-md-10 col-sm-10 col-xs-12">
diff --git a/traffic_portal/test/integration/Data/users.ts
b/traffic_portal/test/integration/Data/users.ts
index 884b4b3..92bb871 100644
--- a/traffic_portal/test/integration/Data/users.ts
+++ b/traffic_portal/test/integration/Data/users.ts
@@ -53,6 +53,7 @@ export const users = {
Email: "[email protected]",
Role: "admin",
Tenant: "- tenantSame",
+ UCDN: "",
Password: "qwe@123#rty",
ConfirmPassword: "qwe@123#rty",
PublicSSHKey: "",
diff --git a/traffic_portal/test/integration/PageObjects/UsersPage.po.ts
b/traffic_portal/test/integration/PageObjects/UsersPage.po.ts
index 00b7e26..6d52288 100644
--- a/traffic_portal/test/integration/PageObjects/UsersPage.po.ts
+++ b/traffic_portal/test/integration/PageObjects/UsersPage.po.ts
@@ -27,6 +27,7 @@ interface User {
Email: string;
Role: string;
Tenant: string;
+ UCDN: string;
Password: string;
ConfirmPassword: string;
PublicSSHKey: string;
@@ -63,6 +64,7 @@ export class UsersPage extends BasePage {
private txtEmail = element(by.name('email'));
private txtRole = element(by.name('role'));
private txtTenant = element(by.name('tenantId'));
+ private txtUCDN = element(by.name('uCDN'));
private txtPassword = element(by.name('uPass'));
private txtConfirmPassword = element(by.name('confirmPassword'));
private txtPublicSSHKey = element(by.name('publicSshKey'));
@@ -110,6 +112,7 @@ export class UsersPage extends BasePage {
await this.txtEmail.sendKeys(this.randomize + user.Email);
await this.txtRole.sendKeys(user.Role);
await this.txtTenant.sendKeys(user.Tenant+this.randomize);
+ await this.txtUCDN.sendKeys(user.UCDN);
await this.txtPassword.sendKeys(user.Password);
await this.txtConfirmPassword.sendKeys(user.ConfirmPassword);
await this.txtPublicSSHKey.sendKeys(user.PublicSSHKey);