[ARVADOS] created: db783c021cbad2dd4ab4a4db2783000430e292b2

git at public.curoverse.com git at public.curoverse.com
Tue Aug 5 14:15:27 EDT 2014


        at  db783c021cbad2dd4ab4a4db2783000430e292b2 (commit)


commit db783c021cbad2dd4ab4a4db2783000430e292b2
Author: Tim Pierce <twp at curoverse.com>
Date:   Tue Aug 5 14:00:01 2014 -0400

    2769: ask API server for user's admin status
    
    api_client.go implements new methods IsAdmin and HasUnlimitedScope,
    which use the sdk.ArvadosClient to consult the API server.
    
    * IsAdmin(api_token): returns true if is_admin=true in the user record
      associated with this api_token, false otherwise.
    
    * HasUnlimitedScope(api_token): returns true if the "scopes" field in
      this token's api_client_authorizations includes the string "all",
      false otherwise.
    
    * CanDelete now checks any non-DataManager token and returns true if
      both IsAdmin and HasUnlimitedScope return true.
    
    api_client_test.go tests the methods in api_client.go by calling them
    against a fake API server in a goroutine.
    
    Refs #2769.

diff --git a/services/keep/src/keep/api_client.go b/services/keep/src/keep/api_client.go
new file mode 100644
index 0000000..90b947a
--- /dev/null
+++ b/services/keep/src/keep/api_client.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+	"arvados.org/sdk"
+	"crypto/tls"
+	"log"
+	"net/http"
+	"os"
+	_ "time"
+)
+
+// MakeApiClient returns a sdk.ArvadosClient suitable for connecting to the
+// requested API server.
+//
+func MakeApiClient(api_token string) sdk.ArvadosClient {
+	// Make an API client.
+	// TODO(twp): use command line flags for ARVADOS_API_HOST, etc.
+	var api_host string = os.Getenv("ARVADOS_API_HOST")
+	var api_insecure bool = (os.Getenv("ARVADOS_API_HOST_INSECURE") == "true")
+	return sdk.ArvadosClient{
+		ApiServer:   api_host,
+		ApiToken:    api_token,
+		ApiInsecure: api_insecure,
+		Client: &http.Client{Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: api_insecure}}},
+		External: false}
+}
+
+// IsAdmin returns true if the user who submitted this request has an
+// administrator's token (the "is_admin" field of their User record is
+// "true").
+//
+func IsAdmin(api_token string) bool {
+	var api_client = MakeApiClient(api_token)
+	// Ask the API server whether this user is an admin.
+	var userinfo sdk.Dict
+	if err := api_client.List("users/current", nil, &userinfo); err != nil {
+		log.Printf("IsAdmin: %s\n", err)
+		return false
+	}
+	return userinfo["is_admin"].(bool)
+}
+
+// HasUnlimitedScope returns true if the scopes attached to this
+// token's ApiClientAuthorization record include "all".
+func HasUnlimitedScope(api_token string) bool {
+	var api_client = MakeApiClient(api_token)
+	var auth sdk.Dict
+	req := "api_client_authorizations/" + api_token
+	if err := api_client.List(req, nil, &auth); err != nil {
+		log.Printf("HasUnlimitedScope: %s\n", err)
+		return false
+	}
+
+	var scopes []interface{} = auth["scopes"].([]interface{})
+	for _, s := range scopes {
+		if s.(string) == "all" {
+			return true
+		}
+	}
+	return false
+}
diff --git a/services/keep/src/keep/api_client_test.go b/services/keep/src/keep/api_client_test.go
new file mode 100644
index 0000000..4c4fe8a
--- /dev/null
+++ b/services/keep/src/keep/api_client_test.go
@@ -0,0 +1,187 @@
+package main
+
+// Test methods defined in api_client.go.
+//
+// These tests launch a fake API server in a goroutine. The fake API
+// server only knows how to return a few predefined responses.
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+)
+
+// Define API tokens for testing:
+//   * an administrator token with scope "all"
+//   * an administrator token without scope "all"
+//   * an unprivileged token with scope "all"
+//   * an unprivileged token without scope "all"
+//
+var api_token = map[string]string{
+	"admin_allscope": "admin_with_all_scopes",
+	"admin_badscope": "admin_without_all_scope",
+	"admin_noscope":  "admin_with_no_scope",
+	"user_allscope":  "unprivileged_user_with_all_scope",
+	"user_badscope":  "unprivileged_user_with_bad_scope",
+	"user_noscope":   "unprivileged_user_with_no_scope",
+}
+
+// Canned responses for the fake API server.  If the
+// token and path match a request's Authorization header
+// and URI path, then the response from the server will
+// use the corresponding HTTP status and response body.
+//
+var apiserver_responses = []struct {
+	token    string
+	path     string
+	status   int
+	response string
+}{
+	// /users/current requests
+	// admin_* tokens return {"is_admin":true}
+	// user_* tokens return {"is_admin":false}
+	{
+		token:    api_token["admin_allscope"],
+		path:     "/arvados/v1/users/current",
+		status:   http.StatusOK,
+		response: `{"is_admin":true}`,
+	},
+	{
+		token:    api_token["admin_badscope"],
+		path:     "/arvados/v1/users/current",
+		status:   http.StatusOK,
+		response: `{"is_admin":true}`,
+	},
+	{
+		token:    api_token["admin_noscope"],
+		path:     "/arvados/v1/users/current",
+		status:   http.StatusOK,
+		response: `{"is_admin":true}`,
+	},
+	{
+		token:    api_token["user_allscope"],
+		path:     "/arvados/v1/users/current",
+		status:   http.StatusOK,
+		response: `{"is_admin":false}`,
+	},
+	{
+		token:    api_token["user_badscope"],
+		path:     "/arvados/v1/users/current",
+		status:   http.StatusOK,
+		response: `{"is_admin":false}`,
+	},
+	{
+		token:    api_token["user_noscope"],
+		path:     "/arvados/v1/users/current",
+		status:   http.StatusOK,
+		response: `{"is_admin":false}`,
+	},
+	// api_client_authorizations
+	// *_allscope tokens get a response with "scopes":["all"].
+	// *_badscope tokens get a response with "scopes" including something other than "all".
+	// *_noscope tokens have no "scopes" field in the response at all.
+	{
+		token:    api_token["admin_allscope"],
+		path:     "/arvados/v1/api_client_authorizations/" + api_token["admin_allscope"],
+		status:   http.StatusOK,
+		response: `{"uuid":"admin_allscope","scopes":["all"]}`,
+	},
+	{
+		token:    api_token["admin_badscope"],
+		path:     "/arvados/v1/api_client_authorizations/" + api_token["admin_badscope"],
+		status:   http.StatusOK,
+		response: `{"uuid":"admin_badscope","scopes":["GET /arvados/v1/collections/"]}`,
+	},
+	{
+		token:    api_token["admin_noscope"],
+		path:     "/arvados/v1/api_client_authorizations/" + api_token["admin_badscope"],
+		status:   http.StatusOK,
+		response: `{"uuid":"admin_noscope"}`,
+	},
+	{
+		token:    api_token["user_allscope"],
+		path:     "/arvados/v1/api_client_authorizations/" + api_token["user_allscope"],
+		status:   http.StatusOK,
+		response: `{"uuid":"user_allscope","scopes":["all"]}`,
+	},
+	{
+		token:    api_token["user_badscope"],
+		path:     "/arvados/v1/api_client_authorizations/" + api_token["user_badscope"],
+		status:   http.StatusOK,
+		response: `{"uuid":"user_badscope","scopes":["GET /arvados/v1/collections/"]}`,
+	},
+	{
+		token:    api_token["user_noscope"],
+		path:     "/arvados/v1/api_client_authorizations/" + api_token["user_noscope"],
+		status:   http.StatusOK,
+		response: `{"uuid":"user_noscope"}`,
+	},
+}
+
+// FakeAPIServer is the http.HandlerFunc implementing the test API
+// server.
+//
+var FakeAPIServer = http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+	tok := GetApiToken(req)
+	for _, test := range apiserver_responses {
+		if test.token == tok && test.path == req.URL.Path {
+			resp.WriteHeader(test.status)
+			resp.Write([]byte(test.response))
+			return
+		}
+	}
+	http.Error(resp, "Internal server error", http.StatusInternalServerError)
+})
+
+func TestIsAdmin(t *testing.T) {
+	ts := httptest.NewUnstartedServer(FakeAPIServer)
+	ts.StartTLS()
+	defer ts.Close()
+
+	os.Setenv("ARVADOS_API_HOST", ts.Listener.Addr().String())
+	os.Setenv("ARVADOS_API_HOST_INSECURE", "true")
+
+	expected_results := map[string]bool{
+		"admin_allscope": true,
+		"admin_badscope": true,
+		"admin_noscope":  true,
+		"user_allscope":  false,
+		"user_badscope":  false,
+		"user_noscope":   false,
+	}
+
+	for test, token := range api_token {
+		result := IsAdmin(token)
+		if result != expected_results[test] {
+			t.Errorf("%s: expected %v, got %v\n",
+				token, expected_results[test], result)
+		}
+	}
+}
+
+func TestHasUnlimitedScope(t *testing.T) {
+	ts := httptest.NewUnstartedServer(FakeAPIServer)
+	ts.StartTLS()
+	defer ts.Close()
+
+	os.Setenv("ARVADOS_API_HOST", ts.Listener.Addr().String())
+	os.Setenv("ARVADOS_API_HOST_INSECURE", "true")
+
+	expected_results := map[string]bool{
+		"admin_allscope": true,
+		"admin_badscope": false,
+		"admin_noscope":  false,
+		"user_allscope":  true,
+		"user_badscope":  false,
+		"user_noscope":   false,
+	}
+
+	for test, token := range api_token {
+		result := HasUnlimitedScope(token)
+		if result != expected_results[test] {
+			t.Errorf("%s: expected %v, got %v\n",
+				token, expected_results[test], result)
+		}
+	}
+}
diff --git a/services/keep/src/keep/handlers.go b/services/keep/src/keep/handlers.go
index dc0f517..88954f9 100644
--- a/services/keep/src/keep/handlers.go
+++ b/services/keep/src/keep/handlers.go
@@ -569,8 +569,10 @@ func CanDelete(api_token string) bool {
 	if api_token == data_manager_token {
 		return true
 	}
-	// TODO(twp): look up api_token with the API server
-	// return true if is_admin is true and if the token
-	// has unlimited scope
+	// Tokens belonging to an Arvados administrator with unlimited
+	// scope may also delete blocks.
+	if IsAdmin(api_token) && HasUnlimitedScope(api_token) {
+		return true
+	}
 	return false
 }

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list