[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