[ARVADOS] updated: 1.2.0-190-g1312335b9

Git user git at public.curoverse.com
Mon Oct 22 12:13:54 EDT 2018


Summary of changes:
 lib/controller/fed_generic.go              | 22 ++++++-
 lib/controller/federation.go               | 98 ++++++++++++++++++++++++++----
 lib/controller/federation_test.go          | 67 ++++++++++++++++++++
 lib/controller/handler_test.go             | 36 +++++++++++
 sdk/go/arvados/api_client_authorization.go |  6 +-
 sdk/go/arvados/client.go                   | 55 ++++++++++++-----
 sdk/go/arvados/container.go                |  1 +
 sdk/go/arvadostest/fixtures.go             |  1 +
 vendor/vendor.json                         |  6 ++
 9 files changed, 260 insertions(+), 32 deletions(-)

       via  1312335b91f3ad1958941d401afe7a244c90b275 (commit)
       via  333f7d9b45e0418418220a888a17d0958e05805f (commit)
      from  d59ed50f6c87d8ce9545df786d506337b74ebafd (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 1312335b91f3ad1958941d401afe7a244c90b275
Author: Peter Amstutz <pamstutz at veritasgenetics.com>
Date:   Mon Oct 22 12:13:38 2018 -0400

    14262: Tests for setting and checking container tokens.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz at veritasgenetics.com>

diff --git a/lib/controller/federation.go b/lib/controller/federation.go
index 18f3e4479..dc0aa908c 100644
--- a/lib/controller/federation.go
+++ b/lib/controller/federation.go
@@ -185,9 +185,9 @@ func (h *Handler) createAPItoken(req *http.Request, userUUID string, scopes []st
 (uuid, api_token, expires_at, scopes,
 user_id,
 api_client_id, created_at, updated_at)
-VALUES ($1, $2, now() + INTERVAL '2 weeks', $3,
+VALUES ($1, $2, CURRENT_TIMESTAMP + INTERVAL '2 weeks', $3,
 (SELECT id FROM users WHERE users.uuid=$4 LIMIT 1),
-0, now(), now())`,
+0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
 		uuid, token, string(scopesjson), userUUID)
 
 	if err != nil {
diff --git a/lib/controller/federation_test.go b/lib/controller/federation_test.go
index 23d5d7ca7..7842ad05d 100644
--- a/lib/controller/federation_test.go
+++ b/lib/controller/federation_test.go
@@ -5,8 +5,10 @@
 package controller
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -90,6 +92,10 @@ func (s *FederationSuite) SetUpTest(c *check.C) {
 }
 
 func (s *FederationSuite) remoteMockHandler(w http.ResponseWriter, req *http.Request) {
+	b := &bytes.Buffer{}
+	io.Copy(b, req.Body)
+	req.Body = ioutil.NopCloser(b)
+	req.Body.Close()
 	s.remoteMockRequests = append(s.remoteMockRequests, *req)
 }
 
@@ -567,6 +573,67 @@ func (s *FederationSuite) TestCreateRemoteContainerRequest(c *check.C) {
 	c.Check(strings.HasPrefix(cr.UUID, "zzzzz-"), check.Equals, true)
 }
 
+func (s *FederationSuite) TestCreateRemoteContainerRequestCheckRuntimeToken(c *check.C) {
+	// Send request to zmock and check that outgoing request has
+	// runtime_token sent (because runtime_token isn't returned in
+	// the response).
+
+	defer s.localServiceReturns404(c).Close()
+	// pass cluster_id via query parameter, this allows arvados-controller
+	// to avoid parsing the body
+	req := httptest.NewRequest("POST", "/arvados/v1/container_requests?cluster_id=zmock",
+		strings.NewReader(`{
+  "container_request": {
+    "name": "hello world",
+    "state": "Uncommitted",
+    "output_path": "/",
+    "container_image": "123",
+    "command": ["abc"]
+  }
+}
+`))
+	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
+	req.Header.Set("Content-type", "application/json")
+	resp := s.testRequest(req)
+	c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+	var cr struct {
+		arvados.ContainerRequest `json:"container_request"`
+	}
+	c.Check(json.NewDecoder(s.remoteMockRequests[0].Body).Decode(&cr), check.IsNil)
+	c.Check(strings.HasPrefix(cr.ContainerRequest.RuntimeToken, "v2/"), check.Equals, true)
+}
+
+func (s *FederationSuite) TestCreateRemoteContainerRequestCheckSetRuntimeToken(c *check.C) {
+	// Send request to zmock and check that outgoing request has
+	// runtime_token sent (because runtime_token isn't returned in
+	// the response).
+
+	defer s.localServiceReturns404(c).Close()
+	// pass cluster_id via query parameter, this allows arvados-controller
+	// to avoid parsing the body
+	req := httptest.NewRequest("POST", "/arvados/v1/container_requests?cluster_id=zmock",
+		strings.NewReader(`{
+  "container_request": {
+    "name": "hello world",
+    "state": "Uncommitted",
+    "output_path": "/",
+    "container_image": "123",
+    "command": ["abc"],
+    "runtime_token": "xyz"
+  }
+}
+`))
+	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
+	req.Header.Set("Content-type", "application/json")
+	resp := s.testRequest(req)
+	c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+	var cr struct {
+		arvados.ContainerRequest `json:"container_request"`
+	}
+	c.Check(json.NewDecoder(s.remoteMockRequests[0].Body).Decode(&cr), check.IsNil)
+	c.Check(cr.ContainerRequest.RuntimeToken, check.Equals, "xyz")
+}
+
 func (s *FederationSuite) TestCreateRemoteContainerRequestError(c *check.C) {
 	defer s.localServiceReturns404(c).Close()
 	// pass cluster_id via query parameter, this allows arvados-controller
diff --git a/sdk/go/arvados/container.go b/sdk/go/arvados/container.go
index 2622c1370..b70b4ac91 100644
--- a/sdk/go/arvados/container.go
+++ b/sdk/go/arvados/container.go
@@ -56,6 +56,7 @@ type ContainerRequest struct {
 	UseExisting             bool                   `json:"use_existing"`
 	LogUUID                 string                 `json:"log_uuid"`
 	OutputUUID              string                 `json:"output_uuid"`
+	RuntimeToken            string                 `json:"runtime_token"`
 }
 
 // Mount is special behavior to attach to a filesystem path or device.

commit 333f7d9b45e0418418220a888a17d0958e05805f
Author: Peter Amstutz <pamstutz at veritasgenetics.com>
Date:   Mon Oct 22 11:02:02 2018 -0400

    14262: Add createAPIToken, with test
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz at veritasgenetics.com>

diff --git a/lib/controller/fed_generic.go b/lib/controller/fed_generic.go
index 0630217b6..63e61e690 100644
--- a/lib/controller/fed_generic.go
+++ b/lib/controller/fed_generic.go
@@ -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 {
diff --git a/lib/controller/federation.go b/lib/controller/federation.go
index 03d2f3fab..18f3e4479 100644
--- a/lib/controller/federation.go
+++ b/lib/controller/federation.go
@@ -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 {
diff --git a/lib/controller/handler_test.go b/lib/controller/handler_test.go
index 963fd1159..746b9242f 100644
--- a/lib/controller/handler_test.go
+++ b/lib/controller/handler_test.go
@@ -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())
+}
diff --git a/sdk/go/arvados/api_client_authorization.go b/sdk/go/arvados/api_client_authorization.go
index ec0239eb3..17cff235d 100644
--- a/sdk/go/arvados/api_client_authorization.go
+++ b/sdk/go/arvados/api_client_authorization.go
@@ -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.
diff --git a/sdk/go/arvados/client.go b/sdk/go/arvados/client.go
index cca9f9bf1..923cecdd5 100644
--- a/sdk/go/arvados/client.go
+++ b/sdk/go/arvados/client.go
@@ -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)
 }
 
diff --git a/sdk/go/arvadostest/fixtures.go b/sdk/go/arvadostest/fixtures.go
index eb79b5b7d..4fdeb0dbe 100644
--- a/sdk/go/arvadostest/fixtures.go
+++ b/sdk/go/arvadostest/fixtures.go
@@ -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"
diff --git a/vendor/vendor.json b/vendor/vendor.json
index aa6b2d773..9abb9bb15 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -313,6 +313,12 @@
 			"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",
 			"revision": "802051befeb51da415c46972b5caf36e7c33c53d",

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list