2760: Assign a generic name if link_class=name and no name is given.
[arvados.git] / services / api / app / models / link.rb
1 class Link < ArvadosModel
2   include HasUuid
3   include KindAndEtag
4   include CommonApiTemplate
5   serialize :properties, Hash
6   before_create :permission_to_attach_to_objects
7   before_update :permission_to_attach_to_objects
8   before_create :assign_generic_name_if_none_given
9   after_update :maybe_invalidate_permissions_cache
10   after_create :maybe_invalidate_permissions_cache
11   after_destroy :maybe_invalidate_permissions_cache
12   attr_accessor :head_kind, :tail_kind
13   validate :name_link_has_valid_name
14
15   api_accessible :user, extend: :common do |t|
16     t.add :tail_uuid
17     t.add :link_class
18     t.add :name
19     t.add :head_uuid
20     t.add :head_kind
21     t.add :tail_kind
22     t.add :properties
23   end
24
25   def properties
26     @properties ||= Hash.new
27     super
28   end
29
30   def head_kind
31     if k = ArvadosModel::resource_class_for_uuid(head_uuid)
32       k.kind
33     end
34   end
35
36   def tail_kind
37     if k = ArvadosModel::resource_class_for_uuid(tail_uuid)
38       k.kind
39     end
40   end
41
42   protected
43
44   def assign_generic_name_if_none_given
45     if new_record? and link_class == 'name' and (!name or name.empty?)
46       # Creating a name link with no name means "invent a generic
47       # name, like New Foo (1)"
48
49       head_class = ArvadosModel::resource_class_for_uuid(head_uuid)
50       if !head_class
51         errors.add :name, 'cannot be automatically assigned for this uuid'
52         return
53       end
54       name_base = "New " + head_class.to_s.underscore.humanize.downcase
55       if not Link.where(link_class: link_class,
56                         tail_uuid: tail_uuid,
57                         name: name_base).any?
58         self.name = name_base
59       else
60         # Find how many digits the largest N has among "New model (N)" names
61         maxlen = ActiveRecord::Base.connection.
62           execute("SELECT max(length(name)) maxlen FROM links "\
63                   "WHERE link_class='name' "\
64                   "AND tail_uuid=#{Link.sanitize(tail_uuid)} "\
65                   "AND name~#{Link.sanitize "#{name_base} \\([0-9]+\\)"}")[0]
66         if maxlen and maxlen['maxlen']
67           # Find the largest N by sorting alphanumerically
68           maxname = ActiveRecord::Base.connection.
69             execute("SELECT max(name) maxname FROM links "\
70                     "WHERE link_class='name' "\
71                     "AND tail_uuid=#{Link.sanitize(tail_uuid)} "\
72                     "AND length(name)=#{maxlen['maxlen']} "\
73                     "AND name~#{Link.sanitize "#{name_base} \\([0-9]+\\)"}"
74                     )[0]['maxname']
75           n = maxname.match(/\(([0-9]+)\)$/)[1].to_i
76           n += 1
77         else
78           # "New foo" is taken, but "New foo (1)" isn't.
79           n = 1
80         end
81         self.name = name_base + " (#{n})"
82       end
83     end
84   end
85
86   def permission_to_attach_to_objects
87     # Anonymous users cannot write links
88     return false if !current_user
89
90     # All users can write links that don't affect permissions
91     return true if self.link_class != 'permission'
92
93     # Administrators can grant permissions
94     return true if current_user.is_admin
95
96     # All users can grant permissions on objects they own
97     head_obj = self.class.
98       resource_class_for_uuid(self.head_uuid).
99       where('uuid=?',head_uuid).
100       first
101     if head_obj
102       return true if head_obj.owner_uuid == current_user.uuid
103     end
104
105     # Users with "can_grant" permission on an object can grant
106     # permissions on that object
107     has_grant_permission = self.class.
108       where('link_class=? AND name=? AND tail_uuid=? AND head_uuid=?',
109             'permission', 'can_grant', current_user.uuid, self.head_uuid).
110       count > 0
111     return true if has_grant_permission
112
113     # Default = deny.
114     false
115   end
116
117   def maybe_invalidate_permissions_cache
118     if self.link_class == 'permission'
119       # Clearing the entire permissions cache can generate many
120       # unnecessary queries if many active users are not affected by
121       # this change. In such cases it would be better to search cached
122       # permissions for head_uuid and tail_uuid, and invalidate the
123       # cache for only those users. (This would require a browseable
124       # cache.)
125       User.invalidate_permissions_cache
126     end
127   end
128
129   def name_link_has_valid_name
130     if link_class == 'name'
131       if new_record? and (!name or name.empty?)
132         # Unique name will be assigned in before_create filter
133         true
134       else
135         unless name.is_a? String and !name.empty?
136           errors.add('name', 'must be a non-empty string')
137         end
138       end
139     else
140       true
141     end
142   end
143 end