14262: Add createAPIToken, with test
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Mon, 22 Oct 2018 15:02:02 +0000 (11:02 -0400)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Tue, 30 Oct 2018 18:12:02 +0000 (14:12 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

lib/controller/fed_generic.go
lib/controller/federation.go
lib/controller/handler_test.go
sdk/go/arvados/api_client_authorization.go
sdk/go/arvados/client.go
sdk/go/arvadostest/fixtures.go
vendor/vendor.json

index 0630217b6e6ac0aae1f6b26893ef4e6bd855832a..63e61e6908f8b318ead4e151bd13dee302c815d3 100644 (file)
@@ -17,10 +17,20 @@ import (
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
+type federatedRequestDelegate func(
+       h *genericFederatedRequestHandler,
+       effectiveMethod string,
+       clusterId *string,
+       uuid string,
+       remainder string,
+       w http.ResponseWriter,
+       req *http.Request) bool
+
 type genericFederatedRequestHandler struct {
-       next    http.Handler
-       handler *Handler
-       matcher *regexp.Regexp
+       next      http.Handler
+       handler   *Handler
+       matcher   *regexp.Regexp
+       delegates []federatedRequestDelegate
 }
 
 func (h *genericFederatedRequestHandler) remoteQueryUUIDs(w http.ResponseWriter,
@@ -285,6 +295,12 @@ func (h *genericFederatedRequestHandler) ServeHTTP(w http.ResponseWriter, req *h
                return
        }
 
+       for _, d := range h.delegates {
+               if d(h, effectiveMethod, &clusterId, m[1], m[3], w, req) {
+                       return
+               }
+       }
+
        if clusterId == "" || clusterId == h.handler.Cluster.ClusterID {
                h.next.ServeHTTP(w, req)
        } else {
index 03d2f3fabb4db924503ed8197a1ea119349930a1..18f3e4479ed1e6097bee8642bf633f52253ce4b7 100644 (file)
@@ -7,6 +7,7 @@ package controller
 import (
        "bytes"
        "database/sql"
+       "encoding/json"
        "fmt"
        "io"
        "io/ioutil"
@@ -17,6 +18,7 @@ import (
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/auth"
+       "github.com/jmcvetta/randutil"
 )
 
 var pathPattern = `^/arvados/v1/%s(/([0-9a-z]{5})-%s-[0-9a-z]{15})?(.*)$`
@@ -82,12 +84,18 @@ func loadParamsFromForm(req *http.Request) error {
 
 func (h *Handler) setupProxyRemoteCluster(next http.Handler) http.Handler {
        mux := http.NewServeMux()
-       mux.Handle("/arvados/v1/workflows", &genericFederatedRequestHandler{next, h, wfRe})
-       mux.Handle("/arvados/v1/workflows/", &genericFederatedRequestHandler{next, h, wfRe})
-       mux.Handle("/arvados/v1/containers", &genericFederatedRequestHandler{next, h, containersRe})
-       mux.Handle("/arvados/v1/containers/", &genericFederatedRequestHandler{next, h, containersRe})
-       mux.Handle("/arvados/v1/container_requests", &genericFederatedRequestHandler{next, h, containerRequestsRe})
-       mux.Handle("/arvados/v1/container_requests/", &genericFederatedRequestHandler{next, h, containerRequestsRe})
+
+       wfHandler := &genericFederatedRequestHandler{next, h, wfRe, nil}
+       containersHandler := &genericFederatedRequestHandler{next, h, containersRe, nil}
+       containerRequestsHandler := &genericFederatedRequestHandler{next, h, containerRequestsRe,
+               []federatedRequestDelegate{remoteContainerRequestCreate}}
+
+       mux.Handle("/arvados/v1/workflows", wfHandler)
+       mux.Handle("/arvados/v1/workflows/", wfHandler)
+       mux.Handle("/arvados/v1/containers", containersHandler)
+       mux.Handle("/arvados/v1/containers/", containersHandler)
+       mux.Handle("/arvados/v1/container_requests", containerRequestsHandler)
+       mux.Handle("/arvados/v1/container_requests/", containerRequestsHandler)
        mux.Handle("/arvados/v1/collections", next)
        mux.Handle("/arvados/v1/collections/", &collectionFederatedRequestHandler{next, h})
        mux.Handle("/", next)
@@ -118,12 +126,79 @@ type CurrentUser struct {
        UUID          string
 }
 
-func (h *Handler) validateAPItoken(req *http.Request, user *CurrentUser) error {
+// validateAPItoken extracts the token from the provided http request,
+// checks it again api_client_authorizations table in the database,
+// and fills in the token scope and user UUID.  Does not handle remote
+// tokens unless they are already in the database and not expired.
+func (h *Handler) validateAPItoken(req *http.Request, token string) (*CurrentUser, error) {
+       user := CurrentUser{Authorization: arvados.APIClientAuthorization{APIToken: token}}
        db, err := h.db(req)
        if err != nil {
-               return err
+               return nil, err
+       }
+
+       var uuid string
+       if strings.HasPrefix(token, "v2/") {
+               sp := strings.Split(token, "/")
+               uuid = sp[1]
+               token = sp[2]
+       }
+       user.Authorization.APIToken = token
+       var scopes string
+       err = db.QueryRowContext(req.Context(), `SELECT api_client_authorizations.uuid, api_client_authorizations.scopes, users.uuid FROM api_client_authorizations JOIN users on api_client_authorizations.user_id=users.id WHERE api_token=$1 AND (expires_at IS NULL OR expires_at > current_timestamp) LIMIT 1`, token).Scan(&user.Authorization.UUID, &scopes, &user.UUID)
+       if err != nil {
+               return nil, err
+       }
+       if uuid != "" && user.Authorization.UUID != uuid {
+               return nil, fmt.Errorf("UUID embedded in v2 token did not match record")
+       }
+       err = json.Unmarshal([]byte(scopes), &user.Authorization.Scopes)
+       if err != nil {
+               return nil, err
+       }
+       return &user, nil
+}
+
+func (h *Handler) createAPItoken(req *http.Request, userUUID string, scopes []string) (*arvados.APIClientAuthorization, error) {
+       db, err := h.db(req)
+       if err != nil {
+               return nil, err
+       }
+       rd, err := randutil.String(15, "abcdefghijklmnopqrstuvwxyz0123456789")
+       if err != nil {
+               return nil, err
+       }
+       uuid := fmt.Sprintf("%v-gj3su-%v", h.Cluster.ClusterID, rd)
+       token, err := randutil.String(50, "abcdefghijklmnopqrstuvwxyz0123456789")
+       if err != nil {
+               return nil, err
+       }
+       if len(scopes) == 0 {
+               scopes = append(scopes, "all")
        }
-       return db.QueryRowContext(req.Context(), `SELECT api_client_authorizations.uuid, users.uuid FROM api_client_authorizations JOIN users on api_client_authorizations.user_id=users.id WHERE api_token=$1 AND (expires_at IS NULL OR expires_at > current_timestamp) LIMIT 1`, user.Authorization.APIToken).Scan(&user.Authorization.UUID, &user.UUID)
+       scopesjson, err := json.Marshal(scopes)
+       if err != nil {
+               return nil, err
+       }
+       _, err = db.ExecContext(req.Context(),
+               `INSERT INTO api_client_authorizations
+(uuid, api_token, expires_at, scopes,
+user_id,
+api_client_id, created_at, updated_at)
+VALUES ($1, $2, now() + INTERVAL '2 weeks', $3,
+(SELECT id FROM users WHERE users.uuid=$4 LIMIT 1),
+0, now(), now())`,
+               uuid, token, string(scopesjson), userUUID)
+
+       if err != nil {
+               return nil, err
+       }
+
+       return &arvados.APIClientAuthorization{
+               UUID:      uuid,
+               APIToken:  token,
+               ExpiresAt: "",
+               Scopes:    scopes}, nil
 }
 
 // Extract the auth token supplied in req, and replace it with a
@@ -165,11 +240,10 @@ func (h *Handler) saltAuthToken(req *http.Request, remote string) (updatedReq *h
                // If the token exists in our own database, salt it
                // for the remote. Otherwise, assume it was issued by
                // the remote, and pass it through unmodified.
-               currentUser := CurrentUser{Authorization: arvados.APIClientAuthorization{APIToken: creds.Tokens[0]}}
-               err = h.validateAPItoken(req, &currentUser)
+               currentUser, err := h.validateAPItoken(req, creds.Tokens[0])
                if err == sql.ErrNoRows {
                        // Not ours; pass through unmodified.
-                       token = currentUser.Authorization.APIToken
+                       token = creds.Tokens[0]
                } else if err != nil {
                        return nil, err
                } else {
index 963fd1159415e16d93e676401021e974c287ad69..746b9242f2198ee3c3000c808771047d4aa1c77c 100644 (file)
@@ -130,3 +130,39 @@ func (s *HandlerSuite) TestProxyRedirect(c *check.C) {
        c.Check(resp.Code, check.Equals, http.StatusFound)
        c.Check(resp.Header().Get("Location"), check.Matches, `https://0.0.0.0:1/auth/joshid\?return_to=foo&?`)
 }
+
+func (s *HandlerSuite) TestValidateV1APIToken(c *check.C) {
+       req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
+       user, err := s.handler.(*Handler).validateAPItoken(req, arvadostest.ActiveToken)
+       c.Assert(err, check.IsNil)
+       c.Check(user.Authorization.UUID, check.Equals, arvadostest.ActiveTokenUUID)
+       c.Check(user.Authorization.APIToken, check.Equals, arvadostest.ActiveToken)
+       c.Check(user.Authorization.Scopes, check.DeepEquals, []string{"all"})
+       c.Check(user.UUID, check.Equals, arvadostest.ActiveUserUUID)
+}
+
+func (s *HandlerSuite) TestValidateV2APIToken(c *check.C) {
+       req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
+       user, err := s.handler.(*Handler).validateAPItoken(req, arvadostest.ActiveTokenV2)
+       c.Assert(err, check.IsNil)
+       c.Check(user.Authorization.UUID, check.Equals, arvadostest.ActiveTokenUUID)
+       c.Check(user.Authorization.APIToken, check.Equals, arvadostest.ActiveToken)
+       c.Check(user.Authorization.Scopes, check.DeepEquals, []string{"all"})
+       c.Check(user.UUID, check.Equals, arvadostest.ActiveUserUUID)
+       c.Check(user.Authorization.TokenV2(), check.Equals, arvadostest.ActiveTokenV2)
+}
+
+func (s *HandlerSuite) TestCreateAPIToken(c *check.C) {
+       req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
+       auth, err := s.handler.(*Handler).createAPItoken(req, arvadostest.ActiveUserUUID, nil)
+       c.Assert(err, check.IsNil)
+       c.Check(auth.Scopes, check.DeepEquals, []string{"all"})
+
+       user, err := s.handler.(*Handler).validateAPItoken(req, auth.TokenV2())
+       c.Assert(err, check.IsNil)
+       c.Check(user.Authorization.UUID, check.Equals, auth.UUID)
+       c.Check(user.Authorization.APIToken, check.Equals, auth.APIToken)
+       c.Check(user.Authorization.Scopes, check.DeepEquals, []string{"all"})
+       c.Check(user.UUID, check.Equals, arvadostest.ActiveUserUUID)
+       c.Check(user.Authorization.TokenV2(), check.Equals, auth.TokenV2())
+}
index ec0239eb37bf0a45bb715b35eab757c6c94850d5..17cff235db82fba55fa12c6ff08fe0a114dff27b 100644 (file)
@@ -6,8 +6,10 @@ package arvados
 
 // APIClientAuthorization is an arvados#apiClientAuthorization resource.
 type APIClientAuthorization struct {
-       UUID     string `json:"uuid"`
-       APIToken string `json:"api_token"`
+       UUID      string   `json:"uuid,omitempty"`
+       APIToken  string   `json:"api_token,omitempty"`
+       ExpiresAt string   `json:"expires_at,omitempty"`
+       Scopes    []string `json:"scopes,omitempty"`
 }
 
 // APIClientAuthorizationList is an arvados#apiClientAuthorizationList resource.
index cca9f9bf1be8e946b7b9594f1ed839e92aa73485..923cecdd598003789246bc48e1a66063379c1b37 100644 (file)
@@ -193,37 +193,62 @@ func anythingToValues(params interface{}) (url.Values, error) {
        return urlValues, nil
 }
 
-// RequestAndDecode performs an API request and unmarshals the
-// response (which must be JSON) into dst. Method and body arguments
-// are the same as for http.NewRequest(). The given path is added to
-// the server's scheme/host/port to form the request URL. The given
-// params are passed via POST form or query string.
-//
-// path must not contain a query string.
-func (c *Client) RequestAndDecode(dst interface{}, method, path string, body io.Reader, params interface{}) error {
-       if body, ok := body.(io.Closer); ok {
-               // Ensure body is closed even if we error out early
-               defer body.Close()
-       }
+func (c *Client) MakeRequest(method, path string, body io.Reader, params interface{}) (*http.Request, error) {
        urlString := c.apiURL(path)
        urlValues, err := anythingToValues(params)
        if err != nil {
-               return err
+               return nil, err
        }
        if (method == "GET" || body != nil) && urlValues != nil {
                // FIXME: what if params don't fit in URL
                u, err := url.Parse(urlString)
                if err != nil {
-                       return err
+                       return nil, err
                }
                u.RawQuery = urlValues.Encode()
                urlString = u.String()
        }
        req, err := http.NewRequest(method, urlString, body)
        if err != nil {
-               return err
+               return nil, err
        }
        req.Header.Set("Content-type", "application/x-www-form-urlencoded")
+
+       if c.AuthToken != "" {
+               req.Header.Add("Authorization", "OAuth2 "+c.AuthToken)
+       }
+
+       if req.Header.Get("X-Request-Id") == "" {
+               reqid, _ := c.context().Value(contextKeyRequestID).(string)
+               if reqid == "" {
+                       reqid = reqIDGen.Next()
+               }
+               if req.Header == nil {
+                       req.Header = http.Header{"X-Request-Id": {reqid}}
+               } else {
+                       req.Header.Set("X-Request-Id", reqid)
+               }
+       }
+
+       return req, nil
+}
+
+// RequestAndDecode performs an API request and unmarshals the
+// response (which must be JSON) into dst. Method and body arguments
+// are the same as for http.NewRequest(). The given path is added to
+// the server's scheme/host/port to form the request URL. The given
+// params are passed via POST form or query string.
+//
+// path must not contain a query string.
+func (c *Client) RequestAndDecode(dst interface{}, method, path string, body io.Reader, params interface{}) error {
+       if body, ok := body.(io.Closer); ok {
+               // Ensure body is closed even if we error out early
+               defer body.Close()
+       }
+       req, err := c.MakeRequest(method, path, body, params)
+       if err != nil {
+               return err
+       }
        return c.DoAndDecode(dst, req)
 }
 
index 114faf17b74e245aeaacf72aeaaf5bb6f8e5046a..e0f2483131a98a64856116bda8c14b4de7bd7051 100644 (file)
@@ -8,6 +8,7 @@ package arvadostest
 const (
        SpectatorToken          = "zw2f4gwx8hw8cjre7yp6v1zylhrhn3m5gvjq73rtpwhmknrybu"
        ActiveToken             = "3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi"
+       ActiveTokenUUID         = "zzzzz-gj3su-077z32aux8dg2s1"
        ActiveTokenV2           = "v2/zzzzz-gj3su-077z32aux8dg2s1/3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi"
        AdminToken              = "4axaw8zxe0qm22wa6urpp5nskcne8z88cvbupv653y1njyi05h"
        AnonymousToken          = "4kg6k6lzmp9kj4cpkcoxie964cmvjahbt4fod9zru44k4jqdmi"
index aa6b2d773dfb1e47969794dd816a81b179539163..9abb9bb15e0ae0824533c812f1302d93cf270722 100644 (file)
                        "revision": "d14ea06fba99483203c19d92cfcd13ebe73135f4",
                        "revisionTime": "2015-07-11T00:45:18Z"
                },
+               {
+                       "checksumSHA1": "khL6oKjx81rAZKW+36050b7f5As=",
+                       "path": "github.com/jmcvetta/randutil",
+                       "revision": "2bb1b664bcff821e02b2a0644cd29c7e824d54f8",
+                       "revisionTime": "2015-08-17T12:26:01Z"
+               },
                {
                        "checksumSHA1": "oX6jFQD74oOApvDIhOzW2dXpg5Q=",
                        "path": "github.com/kevinburke/ssh_config",