19146: Add can_write and can_manage response fields.
authorTom Clegg <tom@curii.com>
Mon, 6 Jun 2022 15:27:52 +0000 (11:27 -0400)
committerTom Clegg <tom@curii.com>
Mon, 6 Jun 2022 15:27:52 +0000 (11:27 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

doc/api/methods/groups.html.textile.liquid
lib/controller/localdb/group_test.go
sdk/go/arvados/group.go
sdk/go/arvados/user.go
services/api/app/models/arvados_model.rb
services/api/app/models/group.rb
services/api/app/models/user.rb

index 2a762d92480955c0a55ed3e2e4fe11d7139b6664..db0aac3c7a3570fd0b3b5e9aa2c74dbe9874c196 100644 (file)
@@ -30,7 +30,9 @@ table(table table-bordered table-condensed).
 @"role"@|
 |description|text|||
 |properties|hash|User-defined metadata, may be used in queries using "subproperty filters":{{site.baseurl}}/api/methods.html#subpropertyfilters ||
-|writable_by|array|List of UUID strings identifying Users and other Groups that have write permission for this Group.  Only users who are allowed to administer the Group will receive a full list.  Other users will receive a partial list that includes the Group's owner_uuid and (if applicable) their own user UUID.||
+|writable_by|array|(Deprecated) List of UUID strings identifying Users and other Groups that have write permission for this Group.  Users who are allowed to administer the Group will receive a list of user/group UUIDs that have permission via explicit permission links; permissions via parent/ancestor groups are not taken into account.  Other users will receive a partial list including only the Group's owner_uuid and (if applicable) their own user UUID.||
+|can_write|boolean|True if the current user has write permission on this group.||
+|can_manage|boolean|True if the current user has manage permission on this group.||
 |trash_at|datetime|If @trash_at@ is non-null and in the past, this group and all objects directly or indirectly owned by the group will be hidden from API calls.  May be untrashed.||
 |delete_at|datetime|If @delete_at@ is non-null and in the past, the group and all objects directly or indirectly owned by the group may be permanently deleted.||
 |is_trashed|datetime|True if @trash_at@ is in the past, false if not.||
index 2d55def9f6cbba8c68d2520b6d629845204bb26f..1fde64d119a15892f560062895157238aa3a62e3 100644 (file)
@@ -24,14 +24,7 @@ type GroupSuite struct {
        railsSpy *arvadostest.Proxy
 }
 
-func (s *GroupSuite) TearDownSuite(c *check.C) {
-       // Undo any changes/additions to the user database so they
-       // don't affect subsequent tests.
-       arvadostest.ResetEnv()
-       c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
-}
-
-func (s *GroupSuite) SetUpTest(c *check.C) {
+func (s *GroupSuite) SetUpSuite(c *check.C) {
        cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
        c.Assert(err, check.IsNil)
        s.cluster, err = cfg.GetCluster("")
@@ -41,8 +34,12 @@ func (s *GroupSuite) SetUpTest(c *check.C) {
        *s.localdb.railsProxy = *rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider)
 }
 
-func (s *GroupSuite) TearDownTest(c *check.C) {
+func (s *GroupSuite) TearDownSuite(c *check.C) {
        s.railsSpy.Close()
+       // Undo any changes/additions to the user database so they
+       // don't affect subsequent tests.
+       arvadostest.ResetEnv()
+       c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
 }
 
 func (s *GroupSuite) setUpVocabulary(c *check.C, testVocabulary string) {
@@ -136,3 +133,99 @@ func (s *GroupSuite) TestGroupUpdateWithProperties(c *check.C) {
                }
        }
 }
+
+func (s *GroupSuite) TestCanWriteCanManageResponses(c *check.C) {
+       ctxUser1 := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
+       ctxUser2 := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.SpectatorToken}})
+       ctxAdmin := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.AdminToken}})
+       project, err := s.localdb.GroupCreate(ctxUser1, arvados.CreateOptions{
+               Attrs: map[string]interface{}{
+                       "group_class": "project",
+               },
+       })
+       c.Assert(err, check.IsNil)
+       c.Check(project.CanWrite, check.Equals, true)
+       c.Check(project.CanManage, check.Equals, true)
+
+       subproject, err := s.localdb.GroupCreate(ctxUser1, arvados.CreateOptions{
+               Attrs: map[string]interface{}{
+                       "owner_uuid":  project.UUID,
+                       "group_class": "project",
+               },
+       })
+       c.Assert(err, check.IsNil)
+       c.Check(subproject.CanWrite, check.Equals, true)
+       c.Check(subproject.CanManage, check.Equals, true)
+
+       // Give 2nd user permission to read
+       permlink, err := s.localdb.LinkCreate(ctxAdmin, arvados.CreateOptions{
+               Attrs: map[string]interface{}{
+                       "link_class": "permission",
+                       "name":       "can_read",
+                       "tail_uuid":  arvadostest.SpectatorUserUUID,
+                       "head_uuid":  project.UUID,
+               },
+       })
+       c.Assert(err, check.IsNil)
+
+       // As 2nd user: can read, cannot manage, cannot write
+       project2, err := s.localdb.GroupGet(ctxUser2, arvados.GetOptions{UUID: project.UUID})
+       c.Assert(err, check.IsNil)
+       c.Check(project2.CanWrite, check.Equals, false)
+       c.Check(project2.CanManage, check.Equals, false)
+
+       _, err = s.localdb.LinkUpdate(ctxAdmin, arvados.UpdateOptions{
+               UUID: permlink.UUID,
+               Attrs: map[string]interface{}{
+                       "name": "can_write",
+               },
+       })
+       c.Assert(err, check.IsNil)
+
+       // As 2nd user: cannot manage, can write
+       project2, err = s.localdb.GroupGet(ctxUser2, arvados.GetOptions{UUID: project.UUID})
+       c.Assert(err, check.IsNil)
+       c.Check(project2.CanWrite, check.Equals, true)
+       c.Check(project2.CanManage, check.Equals, false)
+
+       // As owner: after freezing, can manage (owner), cannot write (frozen)
+       project, err = s.localdb.GroupUpdate(ctxUser1, arvados.UpdateOptions{
+               UUID: project.UUID,
+               Attrs: map[string]interface{}{
+                       "frozen_by_uuid": arvadostest.ActiveUserUUID,
+               }})
+       c.Assert(err, check.IsNil)
+       c.Check(project.CanWrite, check.Equals, false)
+       c.Check(project.CanManage, check.Equals, true)
+
+       // As admin: can manage (admin), cannot write (frozen)
+       project, err = s.localdb.GroupGet(ctxAdmin, arvados.GetOptions{UUID: project.UUID})
+       c.Assert(err, check.IsNil)
+       c.Check(project.CanWrite, check.Equals, false)
+       c.Check(project.CanManage, check.Equals, true)
+
+       // As 2nd user: cannot manage (perm), cannot write (frozen)
+       project2, err = s.localdb.GroupGet(ctxUser2, arvados.GetOptions{UUID: project.UUID})
+       c.Assert(err, check.IsNil)
+       c.Check(project2.CanWrite, check.Equals, false)
+       c.Check(project2.CanManage, check.Equals, false)
+
+       // After upgrading perm to "manage", as 2nd user: can manage (perm), cannot write (frozen)
+       _, err = s.localdb.LinkUpdate(ctxAdmin, arvados.UpdateOptions{
+               UUID: permlink.UUID,
+               Attrs: map[string]interface{}{
+                       "name": "can_manage",
+               },
+       })
+       c.Assert(err, check.IsNil)
+       project2, err = s.localdb.GroupGet(ctxUser2, arvados.GetOptions{UUID: project.UUID})
+       c.Assert(err, check.IsNil)
+       c.Check(project2.CanWrite, check.Equals, false)
+       c.Check(project2.CanManage, check.Equals, true)
+
+       // 2nd user can also manage (but not write) the subject inside the frozen project
+       subproject2, err := s.localdb.GroupGet(ctxUser2, arvados.GetOptions{UUID: subproject.UUID})
+       c.Assert(err, check.IsNil)
+       c.Check(subproject2.CanWrite, check.Equals, false)
+       c.Check(subproject2.CanManage, check.Equals, true)
+}
index ad7ac1ee2b74a0d972b2adb4570a6ba417d455b6..0782bd43d154f1ada231307f6a85970c61a28b08 100644 (file)
@@ -27,6 +27,8 @@ type Group struct {
        WritableBy           []string               `json:"writable_by,omitempty"`
        Description          string                 `json:"description"`
        FrozenByUUID         string                 `json:"frozen_by_uuid"`
+       CanWrite             bool                   `json:"can_write"`
+       CanManage            bool                   `json:"can_manage"`
 }
 
 // GroupList is an arvados#groupList resource.
index 68960144a8a3dae092c604bfa4a256efcc8a669b..2fb061e7fb818840fc8e1c42ae10e771c45e1ee5 100644 (file)
@@ -26,6 +26,8 @@ type User struct {
        ModifiedByClientUUID string                 `json:"modified_by_client_uuid"`
        Prefs                map[string]interface{} `json:"prefs"`
        WritableBy           []string               `json:"writable_by,omitempty"`
+       CanWrite             bool                   `json:"can_write"`
+       CanManage            bool                   `json:"can_manage"`
 }
 
 // UserList is an arvados#userList resource.
index 07a31d81a8a129dc67acb4aa1a7fa8f39e253ea1..e7ffe740b13ae622200e4832d1c3e9530264359e 100644 (file)
@@ -273,6 +273,22 @@ class ArvadosModel < ApplicationRecord
     end.compact.uniq
   end
 
+  def can_write
+    if respond_to?(:frozen_by_uuid) && frozen_by_uuid
+      return false
+    else
+      return owner_uuid == current_user.uuid ||
+             current_user.is_admin ||
+             current_user.can?(write: uuid)
+    end
+  end
+
+  def can_manage
+    return owner_uuid == current_user.uuid ||
+           current_user.is_admin ||
+           current_user.can?(manage: uuid)
+  end
+
   # Return a query with read permissions restricted to the union of the
   # permissions of the members of users_list, i.e. if something is readable by
   # any user in users_list, it will be readable in the query returned by this
index b1b2e942c60c4bc79c13dbe731ad9bc1112684d2..e18ee5ef38748fa66c71fc1132af6d6e8cb6199f 100644 (file)
@@ -44,6 +44,8 @@ class Group < ArvadosModel
     t.add :is_trashed
     t.add :properties
     t.add :frozen_by_uuid
+    t.add :can_write
+    t.add :can_manage
   end
 
   def ensure_filesystem_compatible_name
index bbb2378f5c56becac22646212beb343549da5170..52b96f9c512699c8cc7f7f71b6bdd31804fd5902 100644 (file)
@@ -72,6 +72,8 @@ class User < ArvadosModel
     t.add :is_invited
     t.add :prefs
     t.add :writable_by
+    t.add :can_write
+    t.add :can_manage
   end
 
   ALL_PERMISSIONS = {read: true, write: true, manage: true}