Merged master
authorPeter Amstutz <peter.amstutz@curoverse.com>
Fri, 11 Apr 2014 13:44:13 +0000 (09:44 -0400)
committerPeter Amstutz <peter.amstutz@curoverse.com>
Fri, 11 Apr 2014 13:44:13 +0000 (09:44 -0400)
14 files changed:
1  2 
apps/workbench/app/assets/javascripts/application.js
apps/workbench/app/controllers/users_controller.rb
apps/workbench/app/models/arvados_base.rb
doc/api/schema/Link.html.textile.liquid
doc/api/schema/Log.html.textile.liquid
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/jobs_controller.rb
services/api/app/controllers/arvados/v1/users_controller.rb
services/api/app/models/arvados_model.rb
services/api/app/models/job.rb
services/api/app/models/user.rb
services/api/db/schema.rb
services/api/test/fixtures/links.yml
services/api/test/integration/permissions_test.rb

index b49ffe4af1d01e94fd79dd291c52f9e3b8515fae,3ad7944385b82a6f2a7a668334b0cc41107a7411..6afc8c3b040c31cf83375ac7051dc63328851902
@@@ -18,6 -18,7 +18,7 @@@
  //= require bootstrap/tooltip
  //= require bootstrap/popover
  //= require bootstrap/collapse
+ //= require bootstrap/modal
  //= require bootstrap3-editable/bootstrap-editable
  //= require_tree .
  
@@@ -89,6 -90,7 +90,6 @@@ jQuery(function($)
                             {dataType: 'json',
                              type: $(this).attr('data-remote-method'),
                              data: {
 -                                'link[head_kind]': 'arvados#collection',
                                  'link[head_uuid]': tag_head_uuid,
                                  'link[link_class]': 'tag',
                                  'link[name]': new_tag
              });
          }
      }
 -    
 +
      var fixer = new HeaderRowFixer('.table-fixed-header-row');
      fixer.duplicateTheadTr();
      fixer.fixThead();
index 02f1d83cdcdcbe6f2d7bab117a0a2554976a9e3a,5ace8d68193d50cd278d87167fdfaede3194c8c5..0675625f70a6e4df204debca845c18ec02de0781
@@@ -1,7 -1,7 +1,7 @@@
  class UsersController < ApplicationController
-   skip_before_filter :find_object_by_uuid, :only => :welcome
+   skip_before_filter :find_object_by_uuid, :only => [:welcome, :activity]
    skip_around_filter :thread_with_mandatory_api_token, :only => :welcome
-   before_filter :ensure_current_user_is_admin, only: :sudo
+   before_filter :ensure_current_user_is_admin, only: [:sudo, :unsetup, :setup]
  
    def welcome
      if current_user
      end
    end
  
+   def activity
+     @breadcrumb_page_name = nil
+     @users = User.limit(params[:limit] || 1000).all
+     @user_activity = {}
+     @activity = {
+       logins: {},
+       jobs: {},
+       pipeline_instances: {}
+     }
+     @total_activity = {}
+     @spans = [['This week', Time.now.beginning_of_week, Time.now],
+               ['Last week',
+                Time.now.beginning_of_week.advance(weeks:-1),
+                Time.now.beginning_of_week],
+               ['This month', Time.now.beginning_of_month, Time.now],
+               ['Last month',
+                1.month.ago.beginning_of_month,
+                Time.now.beginning_of_month]]
+     @spans.each do |span, threshold_start, threshold_end|
+       @activity[:logins][span] = Log.
+         filter([[:event_type, '=', 'login'],
+                 [:object_kind, '=', 'arvados#user'],
+                 [:created_at, '>=', threshold_start],
+                 [:created_at, '<', threshold_end]])
+       @activity[:jobs][span] = Job.
+         filter([[:created_at, '>=', threshold_start],
+                 [:created_at, '<', threshold_end]])
+       @activity[:pipeline_instances][span] = PipelineInstance.
+         filter([[:created_at, '>=', threshold_start],
+                 [:created_at, '<', threshold_end]])
+       @activity.each do |type, act|
+         records = act[span]
+         @users.each do |u|
+           @user_activity[u.uuid] ||= {}
+           @user_activity[u.uuid][span + ' ' + type.to_s] ||= 0
+         end
+         records.each do |record|
+           @user_activity[record.modified_by_user_uuid] ||= {}
+           @user_activity[record.modified_by_user_uuid][span + ' ' + type.to_s] ||= 0
+           @user_activity[record.modified_by_user_uuid][span + ' ' + type.to_s] += 1
+           @total_activity[span + ' ' + type.to_s] ||= 0
+           @total_activity[span + ' ' + type.to_s] += 1
+         end
+       end
+     end
+     @users = @users.sort_by do |a|
+       [-@user_activity[a.uuid].values.inject(:+), a.full_name]
+     end
+     # Prepend a "Total" pseudo-user to the sorted list
+     @user_activity[nil] = @total_activity
+     @users = [OpenStruct.new(uuid: nil)] + @users
+   end
    def show_pane_list
      if current_user.andand.is_admin
        super | %w(Admin)
      end
    end
  
+   def index_pane_list
+     if current_user.andand.is_admin
+       super | %w(Activity)
+     else
+       super
+     end
+   end
    def sudo
      resp = $arvados_api_client.api(ApiClientAuthorization, '', {
                                       api_client_authorization: {
@@@ -30,6 -91,9 +91,6 @@@
    def home
      @showallalerts = false
      @my_ssh_keys = AuthorizedKey.where(authorized_user_uuid: current_user.uuid)
 -    # @my_vm_perms = Link.where(tail_uuid: current_user.uuid, head_kind: 'arvados#virtual_machine', link_class: 'permission', name: 'can_login')
 -    # @my_repo_perms = Link.where(tail_uuid: current_user.uuid, head_kind: 'arvados#repository', link_class: 'permission', name: 'can_write')
 -
      @my_tag_links = {}
  
      @my_jobs = Job.
        f.html { render template: 'users/home' }
      end
    end
+   def unsetup
+     if current_user.andand.is_admin
+       @object.unsetup
+     end
+     show
+   end
+   def setup
+     respond_to do |format|
+       if current_user.andand.is_admin
+         setup_params = {}
+         if params['user_uuid'] && params['user_uuid'].size>0
+           setup_params[:uuid] = params['user_uuid']
+         end
+         if params['email'] && params['email'].size>0
+           user = {email: params['email']}
+           setup_params[:user] = user
+         end
+         if params['openid_prefix'] && params['openid_prefix'].size>0
+           setup_params[:openid_prefix] = params['openid_prefix']
+         end
+         if params['repo_name'] && params['repo_name'].size>0
+           setup_params[:repo_name] = params['repo_name']
+         end
+         if params['vm_uuid'] && params['vm_uuid'].size>0
+           setup_params[:vm_uuid] = params['vm_uuid']
+         end
+         if User.setup setup_params
+           format.js
+         else
+           self.render_error status: 422
+         end
+       else
+         self.render_error status: 422
+       end
+     end
+   end
+   def setup_popup
+     @vms = VirtualMachine.all.results
+     @current_selections = find_current_links @object
+     respond_to do |format|
+       format.html
+       format.js
+     end
+   end
+   protected
+   def find_current_links user
+     current_selections = {}
+     if !user
+       return current_selections
+     end
+     # oid login perm
+     oid_login_perms = Link.where(tail_uuid: user.email,
+                                    head_kind: 'arvados#user',
+                                    link_class: 'permission',
+                                    name: 'can_login')
+     if oid_login_perms.any?
+       prefix_properties = oid_login_perms.first.properties
+       current_selections[:identity_url_prefix] = prefix_properties[:identity_url_prefix]
+     end
+     # repo perm
+     repo_perms = Link.where(tail_uuid: user.uuid,
+                             head_kind: 'arvados#repository',
+                             link_class: 'permission',
+                             name: 'can_write')
+     if repo_perms.any?
+       repo_uuid = repo_perms.first.head_uuid
+       repos = Repository.where(head_uuid: repo_uuid)
+       if repos.any?
+         repo_name = repos.first.name
+         current_selections[:repo_name] = repo_name
+       end
+     end
+     # vm login perm
+     vm_login_perms = Link.where(tail_uuid: user.uuid,
+                               head_kind: 'arvados#virtualMachine',
+                               link_class: 'permission',
+                               name: 'can_login')
+     if vm_login_perms.any?
+       vm_uuid = vm_login_perms.first.head_uuid
+       current_selections[:vm_uuid] = vm_uuid
+     end
+     return current_selections
+   end
  end
index 15ba1e5f5a1262b049431a5c63c6dd1762305ef0,3c224aaba0560f492f923e138da7b421c3a17d59..55932213445febe52f3b17587e5fe274e24ea3c0
@@@ -32,7 -32,9 +32,7 @@@ class ArvadosBase < ActiveRecord::Bas
        'modified_by_user_uuid' => '004',
        'modified_by_client_uuid' => '005',
        'name' => '050',
 -      'tail_kind' => '100',
        'tail_uuid' => '100',
 -      'head_kind' => '101',
        'head_uuid' => '101',
        'info' => 'zzz-000',
        'updated_at' => 'zzz-999'
      ArvadosResourceList.new(self).order(*args)
    end
  
+   def self.filter(*args)
+     ArvadosResourceList.new(self).filter(*args)
+   end
    def self.where(*args)
      ArvadosResourceList.new(self).where(*args)
    end
        true
      end
    end
 -      
 +
    def links(*args)
      o = {}
      o.merge!(args.pop) if args[-1].is_a? Hash
      o[:link_class] ||= args.shift
      o[:name] ||= args.shift
 -    o[:head_kind] ||= args.shift
 -    o[:tail_kind] = self.kind
      o[:tail_uuid] = self.uuid
      if all_links
        return all_links.select do |m|
index 8ae43b34d9d0cfa0fc6f6e35bd8fd4b070837b09,355605578759982a245360a2f58b27118897e117..dec33bf83272c31f35294b4f1122b5af20fe244b
@@@ -16,20 -16,22 +16,20 @@@ For links that don't make sense to shar
  
  h2. Methods
  
- See "REST methods for working with Arvados resources":/api/methods.html
+ See "REST methods for working with Arvados resources":{{site.baseurl}}/api/methods.html
  
  API endpoint base: @https://{{ site.arvados_api_host }}/arvados/v1/links@
  
  h2. Resource
  
- Each link has, in addition to the usual "attributes of Arvados resources":/api/resources.html:
+ Each link has, in addition to the usual "attributes of Arvados resources":{{site.baseurl}}/api/resources.html:
  
  table(table table-bordered table-condensed).
  |_. Attribute|_. Type|_. Description|
  |tail_uuid|string|Object UUID at the tail (start, source, origin) of this link|
 -|tail_kind|string|Object kind at the tail (start, source, origin) of this link|
  |link_class|string|Class (see below)|
  |name|string|Link type (see below)|
  |head_uuid|string|Object UUID at the head (end, destination, target) of this link|
 -|head_kind|string|Object kind at the head (end, destination, target) of this link|
  |properties{}|list|Additional information, expressed as a key&rarr;value hash. Key: string. Value: string, number, array, or hash.|
  
  h2. Link classes
index aa5ddda81daa9084cba8a300cf495020e74dac70,3f00339a9c2570853a8083f46d0e3d64fcdde0f2..4d781dc3847b61bc9fcfea5bd32e3b4324842a46
@@@ -12,7 -12,7 +12,7 @@@ title: Lo
  
  h2. Methods
  
- See "REST methods for working with Arvados resources":/api/methods.html
+ See "REST methods for working with Arvados resources":{{site.baseurl}}/api/methods.html
  
  API endpoint base: @https://{{ site.arvados_api_host }}/arvados/v1/logs@
  
@@@ -26,10 -26,11 +26,10 @@@ At the time of this writing, the Arvado
  
  h2. Resources
  
- Each Log has, in addition to the usual "attributes of Arvados resources":/api/resources.html:
+ Each Log has, in addition to the usual "attributes of Arvados resources":{{site.baseurl}}/api/resources.html:
  
  table(table table-bordered table-condensed).
  |_. Attribute|_. Type|_. Description|_. Example|
 -|object_kind|string|||
  |object_uuid|string|||
  |event_at|datetime|||
  |event_type|string|A user-defined category or type for this event.|@LOGIN@|
index 70a2a4c9c0716108a95cbb2696dc370dc53d67f9,06e1838411b4a4d4171bf2884f323d61a9737ebf..94ef19c87948fbf4225ec065d5b4c2072ddb6e22
@@@ -36,16 -36,22 +36,16 @@@ class ApplicationController < ActionCon
  
    def create
      @object = model_class.new resource_attrs
 -    if @object.save
 -      show
 -    else
 -      raise "Save failed"
 -    end
 +    @object.save!
 +    show
    end
  
    def update
      attrs_to_update = resource_attrs.reject { |k,v|
        [:kind, :etag, :href].index k
      }
 -    if @object.update_attributes attrs_to_update
 -      show
 -    else
 -      raise "Update failed"
 -    end
 +    @object.update_attributes! attrs_to_update
 +    show
    end
  
    def destroy
    def load_filters_param
      if params[:filters].is_a? Array
        @filters = params[:filters]
-     elsif params[:filters].is_a? String
+     elsif params[:filters].is_a? String and !params[:filters].empty?
        begin
          @filters = Oj.load params[:filters]
          raise unless @filters.is_a? Array
        cond_out = []
        param_out = []
        @filters.each do |attr, operator, operand|
-         if !model_class.searchable_columns.index attr.to_s
+         if !model_class.searchable_columns(operator).index attr.to_s
            raise ArgumentError.new("Invalid attribute '#{attr}' in condition")
          end
          case operator.downcase
              cond_out << "#{table_name}.#{attr} IN (?)"
              param_out << operand
            end
 +        when 'is_a'
 +          operand = [operand] unless operand.is_a? Array
 +          cond = []
 +          operand.each do |op|
 +              cl = ArvadosModel::kind_class op
 +              if cl
 +                cond << "#{table_name}.#{attr} like ?"
 +                param_out << cl.uuid_like_pattern
 +              else
 +                cond << "1=0"
 +              end
 +          end
 +          cond_out << cond.join(' OR ')
          end
        end
        if cond_out.any?
      if @where.is_a? Hash and @where.any?
        conditions = ['1=1']
        @where.each do |attr,value|
-         if attr == :any
+         if attr.to_s == 'any'
            if value.is_a?(Array) and
                value.length == 2 and
-               value[0] == 'contains' and
-               model_class.columns.collect(&:name).index('name') then
+               value[0] == 'contains' then
              ilikes = []
-             model_class.searchable_columns.each do |column|
+             model_class.searchable_columns('ilike').each do |column|
                ilikes << "#{table_name}.#{column} ilike ?"
                conditions << "%#{value[1]}%"
              end
        :items => @objects.as_api_response(nil)
      }
      if @objects.respond_to? :except
-       @object_list[:items_available] = @objects.except(:limit).except(:offset).count
+       @object_list[:items_available] = @objects.
+         except(:limit).except(:offset).
+         count(:id, distinct: true)
      end
      render json: @object_list
    end
index 46dd0ecf8f368e7b6c71309800054f4d74b4be5b,178b48f173d58e47b5c590149d8b7f966b872dec..40f2def5dcbfd3a7546e55e68ebfcdc3547f4961
@@@ -5,46 -5,59 +5,59 @@@ class Arvados::V1::JobsController < App
    skip_before_filter :find_object_by_uuid, :only => :queue
    skip_before_filter :render_404_if_no_object, :only => :queue
  
-   def index
-     return super unless @where.is_a? Hash
-     want_ancestor = @where[:script_version_descends_from]
-     if want_ancestor
-       # Check for missing commit_ancestor rows, and create them if
-       # possible.
-       @objects.
-         dup.
-         includes(:commit_ancestors). # I wish Rails would let me
-                                      # specify here which
-                                      # commit_ancestors I am
-                                      # interested in.
-         each do |o|
-         if o.commit_ancestors.
-             select { |ca| ca.ancestor == want_ancestor }.
-             empty? and !o.script_version.nil?
-           begin
-             o.commit_ancestors << CommitAncestor.find_or_create_by_descendant_and_ancestor(o.script_version, want_ancestor)
-           rescue
+   def create
+     [:repository, :script, :script_version, :script_parameters].each do |r|
+       if !resource_attrs[r]
+         return render json: {
+           :error => "#{r} attribute must be specified"
+         }, status: :unprocessable_entity
+       end
+     end
+     r = Commit.find_commit_range(current_user,
+                                  resource_attrs[:repository],
+                                  resource_attrs[:minimum_script_version],
+                                  resource_attrs[:script_version],
+                                  resource_attrs[:exclude_script_versions])
+     if !resource_attrs[:nondeterministic] and !resource_attrs[:no_reuse]
+       # Search for jobs where the script_version is in the list of commits
+       # returned by find_commit_range
+       @object = nil
+       Job.readable_by(current_user).where(script: resource_attrs[:script],
+                                           script_version: r).
+         each do |j|
+         if j.nondeterministic != true and
+             j.success != false and
+             j.script_parameters == resource_attrs[:script_parameters]
+           # Record the first job in the list
+           if !@object
+             @object = j
+           end
+           # Ensure that all candidate jobs actually did produce the same output
+           if @object.output != j.output
+             @object = nil
+             break
            end
          end
-         o.commit_ancestors.
-           select { |ca| ca.ancestor == want_ancestor }.
-           select(&:is).
-           first
+         if @object
+           return show
+         end
        end
-       # Now it is safe to do an .includes().where() because we are no
-       # longer interested in jobs that have other ancestors but not
-       # want_ancestor.
-       @objects = @objects.
-         includes(:commit_ancestors).
-         where('commit_ancestors.ancestor = ? and commit_ancestors.is = ?',
-               want_ancestor, true)
      end
+     if r
+       resource_attrs[:script_version] = r[0]
+     end
+     # Don't pass these on to activerecord
+     resource_attrs.delete(:minimum_script_version)
+     resource_attrs.delete(:exclude_script_versions)
+     resource_attrs.delete(:no_reuse)
      super
    end
  
    def cancel
      reload_object_before_update
 -    @object.update_attributes cancelled_at: Time.now
 +    @object.update_attributes! cancelled_at: Time.now
      show
    end
  
index 6b35015d42448a946d129ed80fa16d9505458c46,58661a0e9a3c970712eccc054b8d2e9060dc249e..0934642261bb314fdfbf76175251a2d702fb3a6b
@@@ -1,8 -1,9 +1,9 @@@
  class Arvados::V1::UsersController < ApplicationController
    skip_before_filter :find_object_by_uuid, only:
-     [:activate, :event_stream, :current, :system]
+     [:activate, :event_stream, :current, :system, :setup]
    skip_before_filter :render_404_if_no_object, only:
-     [:activate, :event_stream, :current, :system]
+     [:activate, :event_stream, :current, :system, :setup]
+   before_filter :admin_required, only: [:setup, :unsetup]
  
    def current
      @object = current_user
@@@ -28,7 -29,7 +29,7 @@@
        end
      end
    end
-       
    def event_stream
      channel = current_user.andand.uuid
      if current_user.andand.is_admin
            "but is not invited"
          raise ArgumentError.new "Cannot activate without being invited."
        end
 -      act_as_system_user do
 -        required_uuids = Link.where(owner_uuid: system_user_uuid,
 -                                    link_class: 'signature',
 -                                    name: 'require',
 -                                    tail_uuid: system_user_uuid,
 -                                    head_kind: 'arvados#collection').
 +      act_as_system_user do       
 +        required_uuids = Link.where("owner_uuid = ? and link_class = ? and name = ? and tail_uuid = ? and head_uuid like ?",
 +                                    system_user_uuid,
 +                                    'signature',
 +                                    'require',
 +                                    system_user_uuid,
 +                                    Collection.uuid_like_pattern).
            collect(&:head_uuid)
          signed_uuids = Link.where(owner_uuid: system_user_uuid,
                                    link_class: 'signature',
                                    name: 'click',
 -                                  tail_kind: 'arvados#user',
                                    tail_uuid: @object.uuid,
 -                                  head_kind: 'arvados#collection',
                                    head_uuid: required_uuids).
            collect(&:head_uuid)
          todo_uuids = required_uuids - signed_uuids
      end
      show
    end
+   # create user object and all the needed links
+   def setup
+     @object = nil
+     if params[:uuid]
+       @object = User.find_by_uuid params[:uuid]
+       if !@object
+         return render_404_if_no_object
+       end
+       object_found = true
+     else
+       if !params[:user]
+         raise ArgumentError.new "Required uuid or user"
+       else
+         if params[:user]['uuid']
+           @object = User.find_by_uuid params[:user]['uuid']
+           if @object
+             object_found = true
+           end
+         end
+         if !@object
+           if !params[:user]['email']
+             raise ArgumentError.new "Require user email"
+           end
+           if !params[:openid_prefix]
+             raise ArgumentError.new "Required openid_prefix parameter is missing."
+           end
+           @object = model_class.create! resource_attrs
+         end
+       end
+     end
+     if object_found
+       @response = @object.setup_repo_vm_links params[:repo_name],
+                     params[:vm_uuid], params[:openid_prefix]
+     else
+       @response = User.setup @object, params[:openid_prefix],
+                     params[:repo_name], params[:vm_uuid]
+     end
+     render json: { kind: "arvados#HashList", items: @response }
+   end
+   # delete user agreements, vm, repository, login links; set state to inactive
+   def unsetup
+     reload_object_before_update
+     @object.unsetup
+     show
+   end
  end
index e2cc2f1a704f5207a24ecb55e03c3df7e0512fac,38c7a797d164d0a3b984246d89ae6a2f91263aa7..493cf828d165abaf6f6163daae6d714d0e8b8c8f
@@@ -11,11 -11,10 +11,11 @@@ class ArvadosModel < ActiveRecord::Bas
    before_create :ensure_permission_to_create
    before_update :ensure_permission_to_update
    before_destroy :ensure_permission_to_destroy
 -  before_create :update_modified_by_fields
 -  before_update :maybe_update_modified_by_fields
 +
 +  before_validation :maybe_update_modified_by_fields
    validate :ensure_serialized_attribute_type
    validate :normalize_collection_uuids
 +  validate :ensure_valid_uuids
  
    has_many :permissions, :foreign_key => :head_uuid, :class_name => 'Link', :primary_key => :uuid, :conditions => "link_class = 'permission'"
  
    end
  
    def self.kind_class(kind)
 -    kind.match(/^arvados\#(.+?)(_list|List)?$/)[1].pluralize.classify.constantize rescue nil
 +    kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
    end
  
    def href
      "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
    end
  
-   def self.searchable_columns
+   def self.searchable_columns operator
+     textonly_operator = !operator.match(/[<=>]/)
      self.columns.collect do |col|
-       if [:string, :text, :datetime, :integer].index(col.type) && col.name != 'owner_uuid'
+       if col.name == 'owner_uuid'
+         nil
+       elsif [:string, :text].index(col.type)
+         col.name
+       elsif !textonly_operator and [:datetime, :integer].index(col.type)
          col.name
        end
      end.compact
      self.columns.select { |col| col.name == attr.to_s }.first
    end
  
 -  def eager_load_associations
 -    self.class.columns.each do |col|
 -      re = col.name.match /^(.*)_kind$/
 -      if (re and
 -          self.respond_to? re[1].to_sym and
 -          (auuid = self.send((re[1] + '_uuid').to_sym)) and
 -          (aclass = self.class.kind_class(self.send(col.name.to_sym))) and
 -          (aobject = aclass.where('uuid=?', auuid).first))
 -        self.instance_variable_set('@'+re[1], aobject)
 -      end
 -    end
 -  end
 +  def eager_load_associations
 +    self.class.columns.each do |col|
 +      re = col.name.match /^(.*)_kind$/
 +      if (re and
 +          self.respond_to? re[1].to_sym and
 +          (auuid = self.send((re[1] + '_uuid').to_sym)) and
 +          (aclass = self.class.kind_class(self.send(col.name.to_sym))) and
 +          (aobject = aclass.where('uuid=?', auuid).first))
 +        self.instance_variable_set('@'+re[1], aobject)
 +      end
 +    end
 +  end
  
    def self.readable_by user
      uuid_list = [user.uuid, *user.groups_i_can(:read)]
    end
  
    def maybe_update_modified_by_fields
 -    update_modified_by_fields if self.changed?
 +    update_modified_by_fields if self.changed? or self.new_record?
    end
  
    def update_modified_by_fields
      attributes.keys.select { |a| a.match /_uuid$/ }
    end
  
 +  def skip_uuid_read_permission_check
 +    %w(modified_by_client_uuid)
 +  end
 +
    def normalize_collection_uuids
      foreign_key_attributes.each do |attr|
        attr_value = send attr
      end
    end
  
 +  @@UUID_REGEX = /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/
 +
 +  @@prefixes_hash = nil
 +  def self.uuid_prefixes
 +    unless @@prefixes_hash
 +      @@prefixes_hash = {}
 +      ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
 +        if k.respond_to?(:uuid_prefix)
 +          @@prefixes_hash[k.uuid_prefix] = k
 +        end
 +      end
 +    end
 +    @@prefixes_hash
 +  end
 +
 +  def self.uuid_like_pattern
 +    "_____-#{uuid_prefix}-_______________"
 +  end
 +
 +  def ensure_valid_uuids
 +    specials = [system_user_uuid, 'd41d8cd98f00b204e9800998ecf8427e+0']
 +
 +    foreign_key_attributes.each do |attr|
 +      begin
 +        if new_record? or send (attr + "_changed?")
 +          attr_value = send attr
 +          r = ArvadosModel::resource_class_for_uuid attr_value if attr_value
 +          r = r.readable_by(current_user) if r and not skip_uuid_read_permission_check.include? attr
 +          if r and r.where(uuid: attr_value).count == 0 and not specials.include? attr_value
 +            errors.add(attr, "'#{attr_value}' not found")
 +          end
 +        end
 +      rescue Exception => e
 +        bt = e.backtrace.join("\n")
 +        errors.add(attr, "'#{attr_value}' error '#{e}'\n#{bt}\n")
 +      end
 +    end
 +  end
 +
    def self.resource_class_for_uuid(uuid)
      if uuid.is_a? ArvadosModel
        return uuid.class
      resource_class = nil
  
      Rails.application.eager_load!
 -    uuid.match /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/ do |re|
 -      ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
 -        if k.respond_to?(:uuid_prefix)
 -          if k.uuid_prefix == re[1]
 -            return k
 -          end
 -        end
 -      end
 +    uuid.match @@UUID_REGEX do |re|
 +      return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
      end
      nil
    end
index e9ec10d53ea7808305b468cc0308c94af1e93230,e3881ba6e1f4bfdfc492975eac82bcccaeaba1e4..0b2247bc21d99196b839f58a0dca37a2d10c284f
@@@ -27,6 -27,7 +27,7 @@@ class Job < ArvadosMode
      t.add :started_at
      t.add :finished_at
      t.add :output
+     t.add :output_is_persistent
      t.add :success
      t.add :running
      t.add :is_locked_by_uuid
@@@ -36,6 -37,8 +37,8 @@@
      t.add :dependencies
      t.add :log_stream_href
      t.add :log_buffer
+     t.add :nondeterministic
+     t.add :repository
    end
  
    def assert_finished
        order('priority desc, created_at')
    end
  
+   def self.running
+     self.where('running = ?', true).
+       order('priority desc, created_at')
+   end
    protected
  
    def foreign_key_attributes
      super + %w(output log)
    end
  
 +  def skip_uuid_read_permission_check
 +    super + %w(cancelled_by_client_uuid)
 +  end
 +
    def ensure_script_version_is_commit
      if self.is_locked_by_uuid and self.started_at
        # Apparently client has already decided to go for it. This is
@@@ -74,7 -78,7 +82,7 @@@
        return true
      end
      if new_record? or script_version_changed?
-       sha1 = Commit.find_by_commit_ish(self.script_version) rescue nil
+       sha1 = Commit.find_commit_range(current_user, nil, nil, self.script_version, nil)[0] rescue nil
        if sha1
          self.script_version = sha1
        else
index 5c15fa2330e4286e7ca5c684e55854e32b73f40d,5c03eda519533d839abfca77b07b80f6342bfc2c..41858e8b3adb3cb2394c0eb55d5a3cfc24a1d42e
@@@ -7,6 -7,7 +7,7 @@@ class User < ArvadosMode
    before_update :prevent_privilege_escalation
    before_update :prevent_inactive_admin
    before_create :check_auto_admin
+   after_create :add_system_group_permission_link
    after_create AdminNotifier
  
    has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid
          Group.where('owner_uuid in (?)', lookup_uuids).each do |group|
            newgroups << [group.owner_uuid, group.uuid, 'can_manage']
          end
-         Link.where('tail_uuid in (?) and link_class = ? and head_uuid like ?',
 -        Link.where('tail_uuid in (?) and link_class = ? and head_kind in (?)',
++        Link.where('tail_uuid in (?) and link_class = ? and (head_uuid like ? or head_uuid like ?)',
                     lookup_uuids,
                     'permission',
-                    Group.uuid_like_pattern).each do |link|
 -                   ['arvados#group', 'arvados#user']).each do |link|
++                   Group.uuid_like_pattern,
++                   User.uuid_like_pattern).each do |link|
            newgroups << [link.tail_uuid, link.head_uuid, link.name]
          end
          newgroups.each do |tail_uuid, head_uuid, perm_name|
      end
    end
  
+   def self.setup(user, openid_prefix, repo_name=nil, vm_uuid=nil)
+     return user.setup_repo_vm_links(repo_name, vm_uuid, openid_prefix)
+   end
+   # create links
+   def setup_repo_vm_links(repo_name, vm_uuid, openid_prefix)
+     oid_login_perm = create_oid_login_perm openid_prefix
+     repo_perm = create_user_repo_link repo_name
+     vm_login_perm = create_vm_login_permission_link vm_uuid, repo_name
+     group_perm = create_user_group_link
+     return [oid_login_perm, repo_perm, vm_login_perm, group_perm, self].compact
+   end
+   # delete user signatures, login, repo, and vm perms, and mark as inactive
+   def unsetup
+     # delete oid_login_perms for this user
+     oid_login_perms = Link.where(tail_uuid: self.email,
+                                  head_kind: 'arvados#user',
+                                  link_class: 'permission',
+                                  name: 'can_login')
+     oid_login_perms.each do |perm|
+       Link.delete perm
+     end
+     # delete repo_perms for this user
+     repo_perms = Link.where(tail_uuid: self.uuid,
+                             head_kind: 'arvados#repository',
+                             link_class: 'permission',
+                             name: 'can_write')
+     repo_perms.each do |perm|
+       Link.delete perm
+     end
+     # delete vm_login_perms for this user
+     vm_login_perms = Link.where(tail_uuid: self.uuid,
+                                 head_kind: 'arvados#virtualMachine',
+                                 link_class: 'permission',
+                                 name: 'can_login')
+     vm_login_perms.each do |perm|
+       Link.delete perm
+     end
+     # delete "All users' group read permissions for this user
+     group = Group.where(name: 'All users').select do |g|
+       g[:uuid].match /-f+$/
+     end.first
+     group_perms = Link.where(tail_uuid: self.uuid,
+                              head_uuid: group[:uuid],
+                              head_kind: 'arvados#group',
+                              link_class: 'permission',
+                              name: 'can_read')
+     group_perms.each do |perm|
+       Link.delete perm
+     end
+     # delete any signatures by this user
+     signed_uuids = Link.where(link_class: 'signature',
+                               tail_kind: 'arvados#user',
+                               tail_uuid: self.uuid)
+     signed_uuids.each do |sign|
+       Link.delete sign
+     end
+     # mark the user as inactive
+     self.is_active = false
+     self.save!
+   end
    protected
  
    def permission_to_update
      upstream_path.delete start
      merged
    end
+   def create_oid_login_perm (openid_prefix)
+     login_perm_props = {identity_url_prefix: openid_prefix}
+     # Check oid_login_perm
+     oid_login_perms = Link.where(tail_uuid: self.email,
+                                    head_kind: 'arvados#user',
+                                    link_class: 'permission',
+                                    name: 'can_login')
+     if !oid_login_perms.any?
+       # create openid login permission
+       oid_login_perm = Link.create(link_class: 'permission',
+                                    name: 'can_login',
+                                    tail_kind: 'email',
+                                    tail_uuid: self.email,
+                                    head_kind: 'arvados#user',
+                                    head_uuid: self.uuid,
+                                    properties: login_perm_props
+                                   )
+       logger.info { "openid login permission: " + oid_login_perm[:uuid] }
+     else
+       oid_login_perm = oid_login_perms.first
+     end
+     return oid_login_perm
+   end
+   def create_user_repo_link(repo_name)
+     # repo_name is optional
+     if not repo_name
+       logger.warn ("Repository name not given for #{self.uuid}.")
+       return
+     end
+     # Check for an existing repository with the same name we're about to use.
+     repo = Repository.where(name: repo_name).first
+     if repo
+       logger.warn "Repository exists for #{repo_name}: #{repo[:uuid]}."
+       # Look for existing repository access for this repo
+       repo_perms = Link.where(tail_uuid: self.uuid,
+                               head_kind: 'arvados#repository',
+                               head_uuid: repo[:uuid],
+                               link_class: 'permission',
+                               name: 'can_write')
+       if repo_perms.any?
+         logger.warn "User already has repository access " +
+             repo_perms.collect { |p| p[:uuid] }.inspect
+         return repo_perms.first
+       end
+     end
+     # create repo, if does not already exist
+     repo ||= Repository.create(name: repo_name)
+     logger.info { "repo uuid: " + repo[:uuid] }
+     repo_perm = Link.create(tail_kind: 'arvados#user',
+                             tail_uuid: self.uuid,
+                             head_kind: 'arvados#repository',
+                             head_uuid: repo[:uuid],
+                             link_class: 'permission',
+                             name: 'can_write')
+     logger.info { "repo permission: " + repo_perm[:uuid] }
+     return repo_perm
+   end
+   # create login permission for the given vm_uuid, if it does not already exist
+   def create_vm_login_permission_link(vm_uuid, repo_name)
+     begin
+       # vm uuid is optional
+       if vm_uuid
+         vm = VirtualMachine.where(uuid: vm_uuid).first
+         if not vm
+           logger.warn "Could not find virtual machine for #{vm_uuid.inspect}"
+           raise "No vm found for #{vm_uuid}"
+         end
+       else
+         return
+       end
+       logger.info { "vm uuid: " + vm[:uuid] }
+       login_perms = Link.where(tail_uuid: self.uuid,
+                               head_uuid: vm[:uuid],
+                               head_kind: 'arvados#virtualMachine',
+                               link_class: 'permission',
+                               name: 'can_login')
+       perm_exists = false
+       login_perms.each do |perm|
+         if perm.properties[:username] == repo_name
+           perm_exists = true
+           break
+         end
+       end
+       if !perm_exists
+         login_perm = Link.create(tail_kind: 'arvados#user',
+                                  tail_uuid: self.uuid,
+                                  head_kind: 'arvados#virtualMachine',
+                                  head_uuid: vm[:uuid],
+                                  link_class: 'permission',
+                                  name: 'can_login',
+                                  properties: {username: repo_name})
+         logger.info { "login permission: " + login_perm[:uuid] }
+       else
+         login_perm = login_perms.first
+       end
+       return login_perm
+     end
+   end
+   # add the user to the 'All users' group
+   def create_user_group_link
+     # Look up the "All users" group (we expect uuid *-*-fffffffffffffff).
+     group = Group.where(name: 'All users').select do |g|
+       g[:uuid].match /-f+$/
+     end.first
+     if not group
+       logger.warn "No 'All users' group with uuid '*-*-fffffffffffffff'."
+       raise "No 'All users' group with uuid '*-*-fffffffffffffff' is found"
+     else
+       logger.info { "\"All users\" group uuid: " + group[:uuid] }
+       group_perms = Link.where(tail_uuid: self.uuid,
+                               head_uuid: group[:uuid],
+                               head_kind: 'arvados#group',
+                               link_class: 'permission',
+                               name: 'can_read')
+       if !group_perms.any?
+         group_perm = Link.create(tail_kind: 'arvados#user',
+                                  tail_uuid: self.uuid,
+                                  head_kind: 'arvados#group',
+                                  head_uuid: group[:uuid],
+                                  link_class: 'permission',
+                                  name: 'can_read')
+         logger.info { "group permission: " + group_perm[:uuid] }
+       else
+         group_perm = group_perms.first
+       end
+       return group_perm
+     end
+   end
+   # Give the special "System group" permission to manage this user and
+   # all of this user's stuff.
+   #
+   def add_system_group_permission_link
+     act_as_system_user do
+       Link.create(link_class: 'permission',
+                   name: 'can_manage',
+                   tail_kind: 'arvados#group',
+                   tail_uuid: system_group_uuid,
+                   head_kind: 'arvados#user',
+                   head_uuid: self.uuid)
+     end
+   end
  end
index 53655d2d0b349e40c3cccbbf3c75ac4416f49d80,fe31666694fee6a112b9c0f3af3febb92d3b9ceb..a63fdf4fe7a75cf2dad81bfa256ca68ae29b470a
@@@ -11,7 -11,7 +11,7 @@@
  #
  # It's strongly recommended to check this file into your version control system.
  
- ActiveRecord::Schema.define(:version => 20140325175653) do
+ ActiveRecord::Schema.define(:version => 20140402001908) do
  
    create_table "api_client_authorizations", :force => true do |t|
      t.string   "api_token",                                           :null => false
      t.boolean  "running"
      t.boolean  "success"
      t.string   "output"
-     t.datetime "created_at"
-     t.datetime "updated_at"
+     t.datetime "created_at",                                  :null => false
+     t.datetime "updated_at",                                  :null => false
      t.string   "priority"
      t.string   "is_locked_by_uuid"
      t.string   "log"
      t.text     "runtime_constraints"
      t.boolean  "nondeterministic"
      t.string   "repository"
+     t.boolean  "output_is_persistent",     :default => false, :null => false
    end
  
    add_index "jobs", ["created_at"], :name => "index_jobs_on_created_at"
      t.string   "modified_by_user_uuid"
      t.datetime "modified_at"
      t.string   "tail_uuid"
 -    t.string   "tail_kind"
      t.string   "link_class"
      t.string   "name"
      t.string   "head_uuid"
      t.text     "properties"
      t.datetime "updated_at"
 -    t.string   "head_kind"
    end
  
    add_index "links", ["created_at"], :name => "index_links_on_created_at"
 -  add_index "links", ["head_kind"], :name => "index_links_on_head_kind"
    add_index "links", ["head_uuid"], :name => "index_links_on_head_uuid"
    add_index "links", ["modified_at"], :name => "index_links_on_modified_at"
 -  add_index "links", ["tail_kind"], :name => "index_links_on_tail_kind"
    add_index "links", ["tail_uuid"], :name => "index_links_on_tail_uuid"
    add_index "links", ["uuid"], :name => "index_links_on_uuid", :unique => true
  
      t.string   "owner_uuid"
      t.string   "modified_by_client_uuid"
      t.string   "modified_by_user_uuid"
 -    t.string   "object_kind"
      t.string   "object_uuid"
      t.datetime "event_at"
      t.string   "event_type"
    add_index "logs", ["event_at"], :name => "index_logs_on_event_at"
    add_index "logs", ["event_type"], :name => "index_logs_on_event_type"
    add_index "logs", ["modified_at"], :name => "index_logs_on_modified_at"
 -  add_index "logs", ["object_kind"], :name => "index_logs_on_object_kind"
    add_index "logs", ["object_uuid"], :name => "index_logs_on_object_uuid"
    add_index "logs", ["summary"], :name => "index_logs_on_summary"
    add_index "logs", ["uuid"], :name => "index_logs_on_uuid", :unique => true
index 523c466f4eee5bc906560689fc82c95e56826ec3,0342d3d1aa654219c5315890ee688c7158de8b84..47de7e5abc7963333c5a7d42c63dbb9bab19edfd
@@@ -6,10 -6,12 +6,10 @@@ user_agreement_required
    modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
    modified_at: 2013-12-26T19:52:21Z
    updated_at: 2013-12-26T19:52:21Z
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-000000000000000
    link_class: signature
    name: require
-   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+93
 -  head_kind: arvados#collection
+   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+249025
    properties: {}
  
  user_agreement_readable:
    modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
    modified_at: 2014-01-24 20:42:26 -0800
    updated_at: 2014-01-24 20:42:26 -0800
 -  tail_kind: arvados#group
    tail_uuid: zzzzz-j7d0g-fffffffffffffff
    link_class: permission
    name: can_read
-   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+93
 -  head_kind: arvados#collection
+   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+249025
    properties: {}
  
  active_user_member_of_all_users_group:
    modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
    modified_at: 2014-01-24 20:42:26 -0800
    updated_at: 2014-01-24 20:42:26 -0800
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
    link_class: permission
    name: can_read
 -  head_kind: arvados#group
    head_uuid: zzzzz-j7d0g-fffffffffffffff
    properties: {}
  
@@@ -48,9 -54,11 +48,9 @@@ active_user_can_manage_system_owned_gro
    modified_by_user_uuid: zzzzz-tpzed-000000000000000
    modified_at: 2014-02-03 15:42:26 -0800
    updated_at: 2014-02-03 15:42:26 -0800
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
    link_class: permission
    name: can_manage
 -  head_kind: arvados#group
    head_uuid: zzzzz-j7d0g-8ulrifv67tve5sx
    properties: {}
  
@@@ -62,10 -70,12 +62,10 @@@ user_agreement_signed_by_active
    modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
    modified_at: 2013-12-26T20:52:21Z
    updated_at: 2013-12-26T20:52:21Z
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
    link_class: signature
    name: click
-   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+93
 -  head_kind: arvados#collection
+   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+249025
    properties: {}
  
  user_agreement_signed_by_inactive:
    modified_by_user_uuid: zzzzz-tpzed-7sg468ezxwnodxs
    modified_at: 2013-12-26T20:52:21Z
    updated_at: 2013-12-26T20:52:21Z
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-7sg468ezxwnodxs
    link_class: signature
    name: click
-   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+93
 -  head_kind: arvados#collection
+   head_uuid: b519d9cb706a29fc7ea24dbea2f05851+249025
    properties: {}
  
  spectator_user_member_of_all_users_group:
    modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
    modified_at: 2014-01-24 20:42:26 -0800
    updated_at: 2014-01-24 20:42:26 -0800
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-l1s2piq4t4mps8r
    link_class: permission
    name: can_read
 -  head_kind: arvados#group
    head_uuid: zzzzz-j7d0g-fffffffffffffff
    properties: {}
  
@@@ -104,9 -118,11 +104,9 @@@ inactive_user_member_of_all_users_group
    modified_by_user_uuid: zzzzz-tpzed-7sg468ezxwnodxs
    modified_at: 2013-12-26T20:52:21Z
    updated_at: 2013-12-26T20:52:21Z
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-x9kqpd79egh49c7
    link_class: permission
    name: can_read
 -  head_kind: arvados#group
    head_uuid: zzzzz-j7d0g-fffffffffffffff
    properties: {}
  
@@@ -118,9 -134,11 +118,9 @@@ inactive_signed_ua_user_member_of_all_u
    modified_by_user_uuid: zzzzz-tpzed-7sg468ezxwnodxs
    modified_at: 2013-12-26T20:52:21Z
    updated_at: 2013-12-26T20:52:21Z
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-7sg468ezxwnodxs
    link_class: permission
    name: can_read
 -  head_kind: arvados#group
    head_uuid: zzzzz-j7d0g-fffffffffffffff
    properties: {}
  
@@@ -132,12 -150,46 +132,44 @@@ foo_file_readable_by_active
    modified_by_user_uuid: zzzzz-tpzed-000000000000000
    modified_at: 2014-01-24 20:42:26 -0800
    updated_at: 2014-01-24 20:42:26 -0800
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
    link_class: permission
    name: can_read
 -  head_kind: arvados#collection
    head_uuid: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
    properties: {}
  
+ foo_file_readable_by_active_duplicate_permission:
+   uuid: zzzzz-o0j2j-2qlmhgothiur55r
+   owner_uuid: zzzzz-tpzed-000000000000000
+   created_at: 2014-01-24 20:42:26 -0800
+   modified_by_client_uuid: zzzzz-ozdt8-000000000000000
+   modified_by_user_uuid: zzzzz-tpzed-000000000000000
+   modified_at: 2014-01-24 20:42:26 -0800
+   updated_at: 2014-01-24 20:42:26 -0800
+   tail_kind: arvados#user
+   tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+   link_class: permission
+   name: can_read
+   head_kind: arvados#collection
+   head_uuid: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
+   properties: {}
+ foo_file_readable_by_active_redundant_permission_via_private_group:
+   uuid: zzzzz-o0j2j-5s8ry7sn6bwxb7w
+   owner_uuid: zzzzz-tpzed-000000000000000
+   created_at: 2014-01-24 20:42:26 -0800
+   modified_by_client_uuid: zzzzz-ozdt8-000000000000000
+   modified_by_user_uuid: zzzzz-tpzed-000000000000000
+   modified_at: 2014-01-24 20:42:26 -0800
+   updated_at: 2014-01-24 20:42:26 -0800
+   tail_kind: arvados#group
+   tail_uuid: zzzzz-j7d0g-22xp1wpjul508rk
+   link_class: permission
+   name: can_read
+   head_kind: arvados#collection
+   head_uuid: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
+   properties: {}
  bar_file_readable_by_active:
    uuid: zzzzz-o0j2j-8hppiuduf8eqdng
    owner_uuid: zzzzz-tpzed-000000000000000
    modified_by_user_uuid: zzzzz-tpzed-000000000000000
    modified_at: 2014-01-24 20:42:26 -0800
    updated_at: 2014-01-24 20:42:26 -0800
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
    link_class: permission
    name: can_read
 -  head_kind: arvados#collection
    head_uuid: fa7aeb5140e2848d39b416daeef4ffc5+45
    properties: {}
  
@@@ -160,9 -214,11 +192,9 @@@ bar_file_readable_by_spectator
    modified_by_user_uuid: zzzzz-tpzed-000000000000000
    modified_at: 2014-01-24 20:42:26 -0800
    updated_at: 2014-01-24 20:42:26 -0800
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-l1s2piq4t4mps8r
    link_class: permission
    name: can_read
 -  head_kind: arvados#collection
    head_uuid: fa7aeb5140e2848d39b416daeef4ffc5+45
    properties: {}
  
@@@ -174,9 -230,11 +206,9 @@@ baz_file_publicly_readable
    modified_by_user_uuid: zzzzz-tpzed-000000000000000
    modified_at: 2014-01-24 20:42:26 -0800
    updated_at: 2014-01-24 20:42:26 -0800
 -  tail_kind: arvados#group
    tail_uuid: zzzzz-j7d0g-fffffffffffffff
    link_class: permission
    name: can_read
 -  head_kind: arvados#collection
    head_uuid: ea10d51bcf88862dbcc36eb292017dfd+45
    properties: {}
  
@@@ -188,9 -246,74 +220,72 @@@ barbaz_job_readable_by_spectator
    modified_by_user_uuid: zzzzz-tpzed-000000000000000
    modified_at: 2014-01-24 20:42:26 -0800
    updated_at: 2014-01-24 20:42:26 -0800
 -  tail_kind: arvados#user
    tail_uuid: zzzzz-tpzed-l1s2piq4t4mps8r
    link_class: permission
    name: can_read
 -  head_kind: arvados#job
    head_uuid: zzzzz-8i9sb-cjs4pklxxjykyuq
    properties: {}
  
+ foo_repository_readable_by_spectator:
+   uuid: zzzzz-o0j2j-cpy7p41hpk5xxx
+   owner_uuid: zzzzz-tpzed-000000000000000
+   created_at: 2014-01-24 20:42:26 -0800
+   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+   modified_by_user_uuid: zzzzz-tpzed-000000000000000
+   modified_at: 2014-01-24 20:42:26 -0800
+   updated_at: 2014-01-24 20:42:26 -0800
+   tail_kind: arvados#user
+   tail_uuid: zzzzz-tpzed-l1s2piq4t4mps8r
+   link_class: permission
+   name: can_read
+   head_kind: arvados#repository
+   head_uuid: zzzzz-2x53u-382brsig8rp3666
+   properties: {}
+ miniadmin_user_is_a_testusergroup_admin:
+   uuid: zzzzz-o0j2j-38vvkciz7qc12j9
+   owner_uuid: zzzzz-tpzed-000000000000000
+   created_at: 2014-04-01 13:53:33 -0400
+   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+   modified_by_user_uuid: zzzzz-tpzed-000000000000000
+   modified_at: 2014-04-01 13:53:33 -0400
+   updated_at: 2014-04-01 13:53:33 -0400
+   tail_kind: arvados#user
+   tail_uuid: zzzzz-tpzed-2bg9x0oeydcw5hm
+   link_class: permission
+   name: can_manage
+   head_kind: arvados#group
+   head_uuid: zzzzz-j7d0g-48foin4vonvc2at
+   properties: {}
+ rominiadmin_user_is_a_testusergroup_admin:
+   uuid: zzzzz-o0j2j-6b0hz5hr107mc90
+   owner_uuid: zzzzz-tpzed-000000000000000
+   created_at: 2014-04-01 13:53:33 -0400
+   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+   modified_by_user_uuid: zzzzz-tpzed-000000000000000
+   modified_at: 2014-04-01 13:53:33 -0400
+   updated_at: 2014-04-01 13:53:33 -0400
+   tail_kind: arvados#user
+   tail_uuid: zzzzz-tpzed-4hvxm4n25emegis
+   link_class: permission
+   name: can_read
+   head_kind: arvados#group
+   head_uuid: zzzzz-j7d0g-48foin4vonvc2at
+   properties: {}
+ testusergroup_can_manage_active_user:
+   uuid: zzzzz-o0j2j-2vaqhxz6hsf4k1d
+   owner_uuid: zzzzz-tpzed-000000000000000
+   created_at: 2014-04-01 13:56:10 -0400
+   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+   modified_by_user_uuid: zzzzz-tpzed-000000000000000
+   modified_at: 2014-04-01 13:56:10 -0400
+   updated_at: 2014-04-01 13:56:10 -0400
+   tail_kind: arvados#group
+   tail_uuid: zzzzz-j7d0g-48foin4vonvc2at
+   link_class: permission
+   name: can_manage
+   head_kind: arvados#user
+   head_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+   properties: {}
index 8ae56b4606acbe14791b230bdab61143fa114c2a,40a77e72c56b3c814ec28c098b7501a319667bb9..9a190aab952f3a6b164f9d9d4ec6ef378b13a895
@@@ -3,224 -3,306 +3,288 @@@ require 'test_helper
  class PermissionsTest < ActionDispatch::IntegrationTest
    fixtures :users, :groups, :api_client_authorizations, :collections
  
-   test "adding and removing direct can_read links" do
-     auth = {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:spectator).api_token}"}
-     admin_auth = {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:admin).api_token}"}
+   def auth auth_fixture
+     {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(auth_fixture).api_token}"}
+   end
  
+   test "adding and removing direct can_read links" do
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response 404
  
      # try to add permission as spectator
      post "/arvados/v1/links", {
        :format => :json,
        :link => {
 -        tail_kind: 'arvados#user',
          tail_uuid: users(:spectator).uuid,
          link_class: 'permission',
          name: 'can_read',
 -        head_kind: 'arvados#collection',
          head_uuid: collections(:foo_file).uuid,
          properties: {}
        }
-     }, auth
+     }, auth(:spectator)
      assert_response 422
  
      # add permission as admin
      post "/arvados/v1/links", {
        :format => :json,
        :link => {
 -        tail_kind: 'arvados#user',
          tail_uuid: users(:spectator).uuid,
          link_class: 'permission',
          name: 'can_read',
 -        head_kind: 'arvados#collection',
          head_uuid: collections(:foo_file).uuid,
          properties: {}
        }
-     }, admin_auth
+     }, auth(:admin)
      u = jresponse['uuid']
      assert_response :success
  
      # read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response :success
  
      # try to delete permission as spectator
-     delete "/arvados/v1/links/#{u}", {:format => :json}, auth
+     delete "/arvados/v1/links/#{u}", {:format => :json}, auth(:spectator)
      assert_response 403
  
      # delete permission as admin
-     delete "/arvados/v1/links/#{u}", {:format => :json}, admin_auth
+     delete "/arvados/v1/links/#{u}", {:format => :json}, auth(:admin)
      assert_response :success
  
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response 404
    end
  
  
    test "adding can_read links from user to group, group to collection" do
-     auth = {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:spectator).api_token}"}
-     admin_auth = {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:admin).api_token}"}
-     
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response 404
  
      # add permission for spectator to read group
      post "/arvados/v1/links", {
        :format => :json,
        :link => {
 -        tail_kind: 'arvados#user',
          tail_uuid: users(:spectator).uuid,
          link_class: 'permission',
          name: 'can_read',
 -        head_kind: 'arvados#group',
          head_uuid: groups(:private).uuid,
          properties: {}
        }
-     }, admin_auth
+     }, auth(:admin)
      assert_response :success
  
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response 404
  
      # add permission for group to read collection
      post "/arvados/v1/links", {
        :format => :json,
        :link => {
 -        tail_kind: 'arvados#group',
          tail_uuid: groups(:private).uuid,
          link_class: 'permission',
          name: 'can_read',
 -        head_kind: 'arvados#collection',
          head_uuid: collections(:foo_file).uuid,
          properties: {}
        }
-     }, admin_auth
+     }, auth(:admin)
      u = jresponse['uuid']
      assert_response :success
  
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response :success
  
      # delete permission for group to read collection
-     delete "/arvados/v1/links/#{u}", {:format => :json}, admin_auth
+     delete "/arvados/v1/links/#{u}", {:format => :json}, auth(:admin)
      assert_response :success
  
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response 404
      
    end
  
  
    test "adding can_read links from group to collection, user to group" do
-     auth = {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:spectator).api_token}"}
-     admin_auth = {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:admin).api_token}"}
-     
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response 404
  
      # add permission for group to read collection
      post "/arvados/v1/links", {
        :format => :json,
        :link => {
 -        tail_kind: 'arvados#group',
          tail_uuid: groups(:private).uuid,
          link_class: 'permission',
          name: 'can_read',
 -        head_kind: 'arvados#collection',
          head_uuid: collections(:foo_file).uuid,
          properties: {}
        }
-     }, admin_auth
+     }, auth(:admin)
      assert_response :success
  
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response 404
  
      # add permission for spectator to read group
      post "/arvados/v1/links", {
        :format => :json,
        :link => {
 -        tail_kind: 'arvados#user',
          tail_uuid: users(:spectator).uuid,
          link_class: 'permission',
          name: 'can_read',
 -        head_kind: 'arvados#group',
          head_uuid: groups(:private).uuid,
          properties: {}
        }
-     }, admin_auth
+     }, auth(:admin)
      u = jresponse['uuid']
      assert_response :success
  
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response :success
  
      # delete permission for spectator to read group
-     delete "/arvados/v1/links/#{u}", {:format => :json}, admin_auth
+     delete "/arvados/v1/links/#{u}", {:format => :json}, auth(:admin)
      assert_response :success
  
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response 404
      
    end
  
    test "adding can_read links from user to group, group to group, group to collection" do
-     auth = {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:spectator).api_token}"}
-     admin_auth = {'HTTP_AUTHORIZATION' => "OAuth2 #{api_client_authorizations(:admin).api_token}"}
-     
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response 404
  
      # add permission for user to read group
      post "/arvados/v1/links", {
        :format => :json,
        :link => {
 -        tail_kind: 'arvados#user',
          tail_uuid: users(:spectator).uuid,
          link_class: 'permission',
          name: 'can_read',
 -        head_kind: 'arvados#group',
          head_uuid: groups(:private).uuid,
          properties: {}
        }
-     }, admin_auth
+     }, auth(:admin)
      assert_response :success
  
      # add permission for group to read group
      post "/arvados/v1/links", {
        :format => :json,
        :link => {
 -        tail_kind: 'arvados#group',
          tail_uuid: groups(:private).uuid,
          link_class: 'permission',
          name: 'can_read',
 -        head_kind: 'arvados#group',
          head_uuid: groups(:empty_lonely_group).uuid,
          properties: {}
        }
-     }, admin_auth
+     }, auth(:admin)
      assert_response :success
  
      # add permission for group to read collection
      post "/arvados/v1/links", {
        :format => :json,
        :link => {
 -        tail_kind: 'arvados#group',
          tail_uuid: groups(:empty_lonely_group).uuid,
          link_class: 'permission',
          name: 'can_read',
 -        head_kind: 'arvados#collection',
          head_uuid: collections(:foo_file).uuid,
          properties: {}
        }
-     }, admin_auth
+     }, auth(:admin)
      u = jresponse['uuid']
      assert_response :success
  
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
      assert_response :success
  
      # delete permission for group to read collection
-     delete "/arvados/v1/links/#{u}", {:format => :json}, admin_auth
+     delete "/arvados/v1/links/#{u}", {:format => :json}, auth(:admin)
      assert_response :success
  
      # try to read collection as spectator
-     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth
+     get "/arvados/v1/collections/#{collections(:foo_file).uuid}", {:format => :json}, auth(:spectator)
+     assert_response 404
+   end
+   test "read-only group-admin sees correct subset of user list" do
+     get "/arvados/v1/users", {:format => :json}, auth(:rominiadmin)
+     assert_response :success
+     resp_uuids = jresponse['items'].collect { |i| i['uuid'] }
+     [[true, users(:rominiadmin).uuid],
+      [true, users(:active).uuid],
+      [false, users(:miniadmin).uuid],
+      [false, users(:spectator).uuid]].each do |should_find, uuid|
+       assert_equal should_find, !resp_uuids.index(uuid).nil?, "rominiadmin should #{'not ' if !should_find}see #{uuid} in user list"
+     end
+   end
+   test "read-only group-admin cannot modify administered user" do
+     put "/arvados/v1/users/#{users(:active).uuid}", {
+       :user => {
+         first_name: 'KilroyWasHere'
+       },
+       :format => :json
+     }, auth(:rominiadmin)
+     assert_response 403
+   end
+   test "read-only group-admin cannot read or update non-administered user" do
+     get "/arvados/v1/users/#{users(:spectator).uuid}", {
+       :format => :json
+     }, auth(:rominiadmin)
+     assert_response 404
+     put "/arvados/v1/users/#{users(:spectator).uuid}", {
+       :user => {
+         first_name: 'KilroyWasHere'
+       },
+       :format => :json
+     }, auth(:rominiadmin)
      assert_response 404
    end
+   test "RO group-admin finds user's specimens, RW group-admin can update" do
+     [[:rominiadmin, false],
+      [:miniadmin, true]].each do |which_user, update_should_succeed|
+       get "/arvados/v1/specimens", {:format => :json}, auth(which_user)
+       assert_response :success
+       resp_uuids = jresponse['items'].collect { |i| i['uuid'] }
+       [[true, specimens(:owned_by_active_user).uuid],
+        [true, specimens(:owned_by_private_group).uuid],
+        [false, specimens(:owned_by_spectator).uuid],
+       ].each do |should_find, uuid|
+         assert_equal(should_find, !resp_uuids.index(uuid).nil?,
+                      "%s should%s see %s in specimen list" %
+                      [which_user.to_s,
+                       should_find ? '' : 'not ',
+                       uuid])
+         put "/arvados/v1/specimens/#{uuid}", {
+           :specimen => {
+             properties: {
+               miniadmin_was_here: true
+             }
+           },
+           :format => :json
+         }, auth(which_user)
+         if !should_find
+           assert_response 404
+         elsif !update_should_succeed
+           assert_response 403
+         else
+           assert_response :success
+         end
+       end
+     end
+   end
  end