Merge branch '1692-redesign-dashboard'
authorPeter Amstutz <peter.amstutz@curoverse.com>
Tue, 31 Dec 2013 15:09:47 +0000 (10:09 -0500)
committerPeter Amstutz <peter.amstutz@curoverse.com>
Tue, 31 Dec 2013 15:09:47 +0000 (10:09 -0500)
37 files changed:
apps/admin/setup-new-user.rb
apps/workbench/app/assets/javascripts/user_agreements.js.coffee [new file with mode: 0644]
apps/workbench/app/assets/stylesheets/user_agreements.css.scss [new file with mode: 0644]
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/controllers/user_agreements_controller.rb [new file with mode: 0644]
apps/workbench/app/helpers/user_agreements_helper.rb [new file with mode: 0644]
apps/workbench/app/models/user.rb
apps/workbench/app/models/user_agreement.rb [new file with mode: 0644]
apps/workbench/app/views/layouts/application.html.erb
apps/workbench/app/views/user_agreements/index.html.erb [new file with mode: 0644]
apps/workbench/config/routes.rb
apps/workbench/test/functional/user_agreements_controller_test.rb [new file with mode: 0644]
apps/workbench/test/unit/helpers/user_agreements_helper_test.rb [new file with mode: 0644]
apps/workbench/test/unit/user_agreement_test.rb [new file with mode: 0644]
docker/Makefile
docker/api/Dockerfile
docker/base/Dockerfile
docker/doc/Dockerfile
docker/passenger/Dockerfile
docker/sso/Dockerfile
docker/workbench/Dockerfile
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/user_agreements_controller.rb [new file with mode: 0644]
services/api/app/controllers/arvados/v1/users_controller.rb
services/api/app/controllers/user_sessions_controller.rb
services/api/app/models/user.rb
services/api/app/models/user_agreement.rb [new file with mode: 0644]
services/api/config/routes.rb
services/api/test/fixtures/api_client_authorizations.yml
services/api/test/fixtures/collections.yml [new file with mode: 0644]
services/api/test/fixtures/groups.yml
services/api/test/fixtures/links.yml [new file with mode: 0644]
services/api/test/fixtures/users.yml
services/api/test/functional/arvados/v1/user_agreements_controller_test.rb [new file with mode: 0644]
services/api/test/functional/arvados/v1/users_controller_test.rb [new file with mode: 0644]
services/api/test/test_helper.rb

index f3758e05997a12f04047b049e8bda4b3930433b4..01fdfc6d8de5ab08605d38741af01c7aea4d9a28 100755 (executable)
@@ -5,7 +5,6 @@ abort 'Error: Ruby >= 1.9.3 required.' if RUBY_VERSION < '1.9.3'
 require 'logger'
 require 'trollop'
 log = Logger.new STDERR
-log.level = ENV['DEBUG'] ? Logger::DEBUG : Logger::WARN
 log.progname = $0.split('/').last
 
 opts = Trollop::options do
@@ -13,12 +12,26 @@ opts = Trollop::options do
   banner "Usage: #{log.progname} " +
     "{user_uuid_or_email} {user_and_repo_name} {vm_uuid}"
   banner ''
+  opt :debug, <<-eos
+Show debug messages.
+  eos
+  opt :create, <<-eos
+Create a new user with the given email address if an existing user \
+is not found.
+  eos
+  opt :openid_prefix, <<-eos, default: 'https://www.google.com/accounts/o8/id'
+If creating a new user record, require authentication from an OpenID \
+with this OpenID prefix *and* a matching email address in order to \
+claim the account.
+  eos
   opt :force, <<-eos
 Continue even if sanity checks raise flags: the given user is already \
 active, the given repository already exists, etc.
   eos
   opt :n, 'Do not change anything, just probe'
 end
+
+log.level = (ENV['DEBUG'] || opts.debug) ? Logger::DEBUG : Logger::WARN
     
 if ARGV.count != 3
   Trollop::die "required arguments are missing"
@@ -33,6 +46,30 @@ user = begin
          arv.user.get(uuid: user_arg)
        rescue Arvados::TransactionFailedError
          found = arv.user.list(where: {email: ARGV[0]})[:items]
+         if found.count == 0 and opts.create
+           if !opts.force and !user_arg.match(/\w\@\w+\.\w+/)
+             abort "About to create new user, but #{user_arg.inspect} " +
+               "does not look like an email address. Stop."
+           end
+           if opts.n
+             log.info "-n flag given. Stop before creating new user record."
+             exit 0
+           end
+           new_user = arv.user.create(user: {email: user_arg})
+           log.info { "created user: " + new_user[:uuid] }
+           login_perm_props = {identity_url_prefix: opts.openid_prefix }
+           oid_login_perm = arv.link.create(link: {
+                                              link_class: 'permission',
+                                              name: 'can_login',
+                                              tail_kind: 'email',
+                                              tail_uuid: user_arg,
+                                              head_kind: 'arvados#user',
+                                              head_uuid: new_user[:uuid],
+                                              properties: login_perm_props
+                                            })
+           log.info { "openid login permission: " + oid_login_perm[:uuid] }
+           found = [new_user]
+         end
          if found.count != 1
            abort "Found #{found.count} users " +
              "with uuid or email #{user_arg.inspect}. Stop."
@@ -93,7 +130,7 @@ if opts.n
 end
 
 if need_force and not opts.force
-  abort "This does not seem to be a new user, and -f was not given. Stop."
+  abort "This does not seem to be a new user[name], and -f was not given. Stop."
 end
 
 # Everything seems to be in order. Create a repository (if needed) and
@@ -129,8 +166,3 @@ group_perm = arv.link.create(link: {
                                link_class: 'permission',
                                name: 'can_read'})
 log.info { "group permission: " + group_perm[:uuid] }
-
-user[:is_active] = true
-user.save
-
-log.info { "user saved with is_active=true" }
diff --git a/apps/workbench/app/assets/javascripts/user_agreements.js.coffee b/apps/workbench/app/assets/javascripts/user_agreements.js.coffee
new file mode 100644 (file)
index 0000000..7615679
--- /dev/null
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/apps/workbench/app/assets/stylesheets/user_agreements.css.scss b/apps/workbench/app/assets/stylesheets/user_agreements.css.scss
new file mode 100644 (file)
index 0000000..98edb29
--- /dev/null
@@ -0,0 +1,3 @@
+// Place all the styles related to the user_agreements controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
index 5b8276a9e01f9fca39f22ca60510a88879b82960..3dc54071fb3e64ddbca7356ea4155c74a3e387d9 100644 (file)
@@ -3,6 +3,7 @@ class ApplicationController < ActionController::Base
   around_filter :thread_clear
   around_filter :thread_with_api_token, :except => [:render_exception, :render_not_found]
   before_filter :find_object_by_uuid, :except => [:index, :render_exception, :render_not_found]
+  before_filter :check_user_agreements, :except => [:render_exception, :render_not_found]
 
   begin
     rescue_from Exception,
@@ -228,4 +229,28 @@ class ApplicationController < ActionController::Base
       self.render_error status: 401
     end
   end
+
+  def check_user_agreements
+    if current_user && !current_user.is_active && current_user.is_invited
+      signatures = UserAgreement.signatures
+      @signed_ua_uuids = UserAgreement.signatures.map &:head_uuid
+      @required_user_agreements = UserAgreement.all.map do |ua|
+        if not @signed_ua_uuids.index ua.uuid
+          Collection.find(ua.uuid)
+        end
+      end
+      if @required_user_agreements.empty?
+        # No agreements to sign. Perhaps we just need to ask?
+        current_user.activate
+        if !current_user.is_active
+          logger.warn "#{current_user.uuid.inspect}: " +
+            "No user agreements to sign, but activate failed!"
+        end
+      end
+      if !current_user.is_active
+        render 'user_agreements/index'
+      end
+    end
+    true
+  end
 end
index 71e882c5eebc46dc14f0ab6e579a6a26b04a6783..a5fed9d9a368cbc7194d4db2b50e7cd2de19de62 100644 (file)
@@ -41,6 +41,16 @@ class CollectionsController < ApplicationController
     end
   end
 
+  def show_file
+    opts = params.merge(arvados_api_token: Thread.current[:arvados_api_token])
+    if r = params[:file].match(/(\.\w+)/)
+      ext = r[1]
+    end
+    self.response.headers['Content-Type'] =
+      Rack::Mime::MIME_TYPES[ext] || 'application/octet-stream'
+    self.response_body = FileStreamer.new opts
+  end
+
   def show
     return super if !@object
     @provenance = []
@@ -86,4 +96,32 @@ class CollectionsController < ApplicationController
       end
     end
   end
+
+  protected
+  class FileStreamer
+    def initialize(opts={})
+      @opts = opts
+    end
+    def each
+      return unless @opts[:uuid] && @opts[:file]
+      env = Hash[ENV].
+        merge({
+                'ARVADOS_API_HOST' =>
+                $arvados_api_client.arvados_v1_base.
+                sub(/\/arvados\/v1/, '').
+                sub(/^https?:\/\//, ''),
+                'ARVADOS_API_TOKEN' =>
+                @opts[:arvados_api_token],
+                'ARVADOS_API_HOST_INSECURE' =>
+                Rails.configuration.arvados_insecure_https ? 'true' : 'false'
+              })
+      IO.popen([env, 'arv-get', "#{@opts[:uuid]}/#{@opts[:file]}"],
+               'rb') do |io|
+        while buf = io.read(2**20)
+          yield buf
+        end
+      end
+      Rails.logger.warn("#{@opts[:uuid]}/#{@opts[:file]}: $?") if $? != 0
+    end
+  end
 end
diff --git a/apps/workbench/app/controllers/user_agreements_controller.rb b/apps/workbench/app/controllers/user_agreements_controller.rb
new file mode 100644 (file)
index 0000000..cc0d3c8
--- /dev/null
@@ -0,0 +1,18 @@
+class UserAgreementsController < ApplicationController
+  skip_before_filter :check_user_agreements
+  skip_before_filter :find_object_by_uuid
+
+  def model_class
+    Collection
+  end
+
+  def sign
+    params[:checked].each do |checked|
+      if r = checked.match(/^([0-9a-f]+)/)
+        UserAgreement.sign uuid: r[1]
+      end
+    end
+    current_user.activate
+    redirect_to(params[:return_to] || :back)
+  end
+end
diff --git a/apps/workbench/app/helpers/user_agreements_helper.rb b/apps/workbench/app/helpers/user_agreements_helper.rb
new file mode 100644 (file)
index 0000000..ab8d3d3
--- /dev/null
@@ -0,0 +1,2 @@
+module UserAgreementsHelper
+end
index 782385fb0d88240211b02793cf9aa299249c45cd..bc1f4763cc90ffa29dc7327b44cfefdd173df0fb 100644 (file)
@@ -21,6 +21,12 @@ class User < ArvadosBase
     (self.first_name || "") + " " + (self.last_name || "")
   end
 
+  def activate
+    self.private_reload($arvados_api_client.api(self.class,
+                                                "/#{self.uuid}/activate",
+                                                {}))
+  end
+
   def attribute_editable?(attr)
     (not (self.uuid.andand.match(/000000000000000$/) and self.is_admin)) and super(attr)
   end
diff --git a/apps/workbench/app/models/user_agreement.rb b/apps/workbench/app/models/user_agreement.rb
new file mode 100644 (file)
index 0000000..63b8452
--- /dev/null
@@ -0,0 +1,10 @@
+class UserAgreement < ArvadosBase
+  def self.signatures
+    res = $arvados_api_client.api self, '/signatures'
+    $arvados_api_client.unpack_api_response(res)
+  end
+  def self.sign(params)
+    res = $arvados_api_client.api self, '/sign', params
+    $arvados_api_client.unpack_api_response(res)
+  end
+end
index e7c7d5d712cd8eae4fe74570b902186d8ad40d74..f92b32c801bf1112548501611ee1d61dfdb09870 100644 (file)
   </div> <!-- /container -->
 
   <%= piwik_tracking_tag %>
+  <%= javascript_tag do %>
+  <%= yield :footer_js %>
+  <% end %>
 </body>
 </html>
diff --git a/apps/workbench/app/views/user_agreements/index.html.erb b/apps/workbench/app/views/user_agreements/index.html.erb
new file mode 100644 (file)
index 0000000..59d9282
--- /dev/null
@@ -0,0 +1,45 @@
+<h1>User agreements</h1>
+
+<p>You must read and sign all applicable user agreements before continuing.</p>
+
+<button data-toggle="modal" href="#open_user_agreement" class="btn btn-primary">Show details</button>
+
+<div id="open_user_agreement" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="uaModalLabel" aria-hidden="true" data-show="true">
+  <%= form_for(@required_user_agreements.first, {url: {action: 'sign', controller: 'user_agreements'}}) do |f| %>
+    <%= hidden_field_tag :return_to, request.url %>
+  <% n_files = @required_user_agreements.collect(&:files).flatten(1).count %>
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+    <h3 id="uaModalLabel">User agreement<%= 's' if n_files != 1 %></h3>
+  </div>
+  <div class="modal-body">
+    <p>Please check <%= n_files > 1 ? 'each' : 'the' %> box to indicate that you have read and accepted the agreement.</p>
+    <% @required_user_agreements.each do |ua| %>
+    <% ua.files.each do |file| %>
+    <%= f.label 'checked[]', class: 'checkbox inline' do %>
+    <%= check_box_tag 'checked[]', "#{ua.uuid}/#{file[0]}/#{file[1]}", false %>
+    <%= link_to 'view', {controller: 'collections', action: 'show_file', uuid: ua.uuid, file: "#{file[0]}/#{file[1]}"}, {target: '_blank', class: 'label label-info'} %>
+    <%= file[1] %>
+    <% end %>
+    <% end %>
+    <% end %>
+  </div>
+  <div class="modal-footer">
+    <button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
+    <%= f.submit 'Accept', {class: 'btn btn-primary', disabled: true} %>
+  </div>
+  <% end %>
+</div>
+
+<% content_for :footer_js do %>
+$('#open_user_agreement').modal();
+$('#open_user_agreement input[name="checked[]"]').on('click', function() {
+    var dialog = $('#open_user_agreement')[0]
+    $('input[type=submit]', dialog).prop('disabled',false);
+    $('input[name="checked[]"]', dialog).each(function(){
+        if(!this.checked) {
+            $('input[type=submit]', dialog).prop('disabled',true);
+        }
+    });
+});
+<% end %>
index 9f8486d755ff09c06ecb4c931d75649e67cdd40b..409058cf3aa2b764814eeb82de06151aa3ac4d06 100644 (file)
@@ -1,13 +1,10 @@
 ArvadosWorkbench::Application.routes.draw do
+  resources :user_agreements
+  post '/user_agreements/sign' => 'user_agreements#sign'
+  get '/user_agreements/signatures' => 'user_agreements#signatures'
   resources :nodes
-
-
   resources :humans
-
-
   resources :traits
-
-
   resources :api_client_authorizations
   resources :repositories
   resources :virtual_machines
@@ -30,6 +27,7 @@ ArvadosWorkbench::Application.routes.draw do
   resources :links
   match '/collections/graph' => 'collections#graph'
   resources :collections
+  get '/collections/:uuid/*file' => 'collections#show_file', :format => false
   root :to => 'users#welcome'
 
   # Send unroutable requests to an arbitrary controller
diff --git a/apps/workbench/test/functional/user_agreements_controller_test.rb b/apps/workbench/test/functional/user_agreements_controller_test.rb
new file mode 100644 (file)
index 0000000..898ac63
--- /dev/null
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class UserAgreementsControllerTest < ActionController::TestCase
+end
diff --git a/apps/workbench/test/unit/helpers/user_agreements_helper_test.rb b/apps/workbench/test/unit/helpers/user_agreements_helper_test.rb
new file mode 100644 (file)
index 0000000..2577870
--- /dev/null
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class UserAgreementsHelperTest < ActionView::TestCase
+end
diff --git a/apps/workbench/test/unit/user_agreement_test.rb b/apps/workbench/test/unit/user_agreement_test.rb
new file mode 100644 (file)
index 0000000..7c0ac65
--- /dev/null
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class UserAgreementTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
index 3b21b43c65095dd36fac54c98db18ba27f110852..45ffe5e7b0d31594cee556fc39a5607d93d2a3ac 100644 (file)
@@ -1,7 +1,17 @@
 all: api-image doc-image workbench-image warehouse-image sso-image
 
+# `make clean' removes the files generated in the build directory
+# but does not remove any docker images generated in previous builds
 clean:
        -rm *-image */generated/*
+       -@rmdir */generated
+
+# `make realclean' will also remove the docker images and force
+# subsequent makes to build the entire chain from the ground up
+realclean: clean
+       -[ -n "`docker ps -q`" ] && docker stop `docker ps -q`
+       -docker rm `docker ps -a -q`
+       -docker rmi `docker images -q`
 
 # ============================================================
 # Dependencies for */generated files which are prerequisites
index 4401ac0e99eb211b1810c7c4d3792dc337a7f694..93944146cabf7e2e8a6986b95a611d0d9db8d4e9 100644 (file)
@@ -40,10 +40,10 @@ RUN sh /tmp/config_databases.sh && \
 
 # Configure Apache and Passenger.
 ADD passenger.conf /etc/apache2/conf.d/passenger
-RUN a2dissite default ; \
-    a2ensite arvados ; \
-    a2enmod rewrite ; \
-    a2enmod ssl ; \
+RUN a2dissite default && \
+    a2ensite arvados && \
+    a2enmod rewrite && \
+    a2enmod ssl && \
     /bin/mkdir /var/run/apache2
 
 # Supervisor.
index fe6844f3ae596531400f69a41a1a1e497ea5584a..5bbee240110e457d188b2a731e02289ca3895b92 100644 (file)
@@ -10,12 +10,12 @@ ENV DEBIAN_FRONTEND noninteractive
 #   * git, curl, rvm
 #   * Arvados source code in /usr/src/arvados-upstream, for preseeding gem installation
 
-RUN apt-get update ;\
-    apt-get -q -y install -q -y openssh-server apt-utils git curl locales postgresql-server-dev-9.1 ;\
-    /bin/mkdir -p /root/.ssh ;\
-    /bin/sed -ri 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen ;\
-    /usr/sbin/locale-gen ;\
-    curl -L https://get.rvm.io | bash -s stable --ruby=2.0.0 ;\
+RUN apt-get update && \
+    apt-get -q -y install -q -y openssh-server apt-utils git curl locales postgresql-server-dev-9.1 && \
+    /bin/mkdir -p /root/.ssh && \
+    /bin/sed -ri 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
+    /usr/sbin/locale-gen && \
+    curl -L https://get.rvm.io | bash -s stable --ruby=2.0.0 && \
     git clone https://github.com/clinicalfuture/arvados.git /usr/src/arvados-upstream
 
 # Set up RVM environment. These are just the env variables created by
@@ -27,11 +27,10 @@ ENV PATH /usr/local/rvm/gems/ruby-2.0.0-p353/bin:/usr/local/rvm/gems/ruby-2.0.0-
 
 # Update gem. This (hopefully) fixes
 # https://github.com/rubygems/rubygems.org/issues/613.
-RUN gem update --system 
-
-RUN gem install bundler ;\
-    bundle install --gemfile=/usr/src/arvados-upstream/apps/workbench/Gemfile ;\
-    bundle install --gemfile=/usr/src/arvados-upstream/services/api/Gemfile ;\
+RUN gem update --system && \
+    gem install bundler && \
+    bundle install --gemfile=/usr/src/arvados-upstream/apps/workbench/Gemfile && \
+    bundle install --gemfile=/usr/src/arvados-upstream/services/api/Gemfile && \
     bundle install --gemfile=/usr/src/arvados-upstream/doc/Gemfile
 
 ADD generated/id_rsa.pub /root/.ssh/authorized_keys
index 0473e87a658fe5555be2e9d6cc6eefca21649790..5ae4bb273b34cbae22f63f3a1c005bb0fb05c6c0 100644 (file)
@@ -4,20 +4,21 @@ FROM arvados/base
 maintainer Ward Vandewege <ward@clinicalfuture.com>
 
 # Install packages
-RUN /bin/mkdir -p /usr/src/arvados ;\
-    apt-get update ;\
+RUN /bin/mkdir -p /usr/src/arvados && \
+    apt-get update && \
     apt-get install -q -y curl procps apache2-mpm-worker
 
 ADD generated/doc.tar.gz /usr/src/arvados/
 
 # Build static site
-RUN /bin/sed -ri 's/^baseurl: .*$/baseurl: /' /usr/src/arvados/doc/_config.yml ;\
-    cd /usr/src/arvados/doc; LANG="en_US.UTF-8" LC_ALL="en_US.UTF-8" jekyll build
+RUN /bin/sed -ri 's/^baseurl: .*$/baseurl: /' /usr/src/arvados/doc/_config.yml && \
+    cd /usr/src/arvados/doc && \
+    LANG="en_US.UTF-8" LC_ALL="en_US.UTF-8" jekyll build
 
 # Configure Apache
 ADD apache2_vhost /etc/apache2/sites-available/doc
 RUN \
-  a2dissite default ;\
+  a2dissite default && \
   a2ensite doc
 
 ADD apache2_foreground.sh /etc/apache2/foreground.sh
index 8bb017c673846613bff60da1de8e376e4d1c6adc..45ef9ae50bf0123b7c490e2be07701b2eb1bba49 100644 (file)
@@ -4,8 +4,9 @@ FROM arvados/base
 MAINTAINER Ward Vandewege <ward@clinicalfuture.com>
 
 # Install packages and build the passenger apache module
-RUN apt-get update ;\
+RUN apt-get update && \
     apt-get install -q -y apt-utils git curl procps apache2-mpm-worker \
-                          libcurl4-openssl-dev apache2-threaded-dev libapr1-dev libaprutil1-dev ;\
+                          libcurl4-openssl-dev apache2-threaded-dev \
+                          libapr1-dev libaprutil1-dev && \
     passenger-install-apache2-module --auto
 
index f5dc2afc89189ad1165c004688cdd711107f8022..2933788ac49ca968904e1ff7e7144f12225ce152 100644 (file)
@@ -3,7 +3,7 @@
 FROM arvados/passenger
 MAINTAINER Ward Vandewege <ward@clinicalfuture.com>
 
-RUN git clone git://github.com/clinicalfuture/sso-devise-omniauth-provider.git /usr/src/sso-provider ;\
+RUN git clone git://github.com/clinicalfuture/sso-devise-omniauth-provider.git /usr/src/sso-provider && \
     bundle install --gemfile=/usr/src/sso-provider/Gemfile
 
 # Install generated config files
@@ -14,13 +14,15 @@ ADD generated/apache2_vhost /etc/apache2/sites-available/sso-provider
 
 # Configure Apache and Passenger.
 ADD passenger.conf /etc/apache2/conf.d/passenger
-RUN a2dissite default ; \
-    a2ensite sso-provider ; \
-    a2enmod rewrite ; \
-    a2enmod ssl ; \
-    cd /usr/src/sso-provider; RAILS_ENV=production rake db:setup ; rake assets:precompile ; \
-    chown www-data:www-data tmp_omniauth log config.ru -R ; \
-    chown www-data:www-data db db/production.sqlite3 ; \
+RUN a2dissite default && \
+    a2ensite sso-provider && \
+    a2enmod rewrite && \
+    a2enmod ssl && \
+    cd /usr/src/sso-provider && \
+    RAILS_ENV=production rake db:setup && \
+    rake assets:precompile && \
+    chown www-data:www-data tmp_omniauth log config.ru -R && \
+    chown www-data:www-data db db/production.sqlite3 && \
     /bin/mkdir /var/run/apache2
 
 ADD apache2_foreground.sh /etc/apache2/foreground.sh
index fbf18216aed8bcb52b98a7916e9c2a394c0c6f28..7bbc24508ab58909654172436737a1a66fdc7c21 100644 (file)
@@ -7,17 +7,18 @@ MAINTAINER Ward Vandewege <ward@clinicalfuture.com>
 RUN /bin/mkdir -p /usr/src/arvados/apps
 ADD generated/workbench.tar.gz /usr/src/arvados/apps/
 
-RUN touch /usr/src/arvados/apps/workbench/log/production.log ;\
-    chmod 666 /usr/src/arvados/apps/workbench/log/production.log ;\
-    touch /usr/src/arvados/apps/workbench/db/production.sqlite3 ;\
-    bundle install --gemfile=/usr/src/arvados/apps/workbench/Gemfile ;\
-    cd /usr/src/arvados/apps/workbench; rake assets:precompile
+RUN touch /usr/src/arvados/apps/workbench/log/production.log && \
+    chmod 666 /usr/src/arvados/apps/workbench/log/production.log && \
+    touch /usr/src/arvados/apps/workbench/db/production.sqlite3 && \
+    bundle install --gemfile=/usr/src/arvados/apps/workbench/Gemfile && \
+    cd /usr/src/arvados/apps/workbench && \
+    rake assets:precompile
 
 # Configure Apache
 ADD generated/apache2_vhost /etc/apache2/sites-available/workbench
 RUN \
-  a2dissite default ;\
-  a2ensite workbench ;\
+  a2dissite default && \
+  a2ensite workbench && \
   a2enmod rewrite
 
 # Set up the production environment
index 434b09572b40b64af026e27a43962b422aeea7c6..de0c50c57849fd2ae4e50ab434fc83facdad696a 100644 (file)
@@ -328,7 +328,7 @@ class ApplicationController < ActionController::Base
 
   def render_list
     @object_list = {
-      :kind  => "arvados##{resource_name}List",
+      :kind  => "arvados##{(@response_resource_name || resource_name).camelize(:lower)}List",
       :etag => "",
       :self_link => "",
       :next_page_token => "",
diff --git a/services/api/app/controllers/arvados/v1/user_agreements_controller.rb b/services/api/app/controllers/arvados/v1/user_agreements_controller.rb
new file mode 100644 (file)
index 0000000..990c1d6
--- /dev/null
@@ -0,0 +1,81 @@
+class Arvados::V1::UserAgreementsController < ApplicationController
+  before_filter :admin_required, except: [:index, :sign, :signatures]
+  skip_before_filter :find_object, only: [:sign, :signatures]
+
+  def model_class
+    Link
+  end
+
+  def index
+    if not current_user.is_invited
+      # New users cannot see user agreements until/unless invited to
+      # use this installation.
+      @objects = []
+    else
+      current_user_uuid = current_user.uuid
+      act_as_system_user do
+        uuids = Link.where(owner_uuid: system_user_uuid,
+                           link_class: 'signature',
+                           name: 'require',
+                           tail_kind: 'arvados#user',
+                           tail_uuid: system_user_uuid,
+                           head_kind: 'arvados#collection').
+          collect &:head_uuid
+        @objects = Collection.where('uuid in (?)', uuids)
+      end
+    end
+    @response_resource_name = 'collection'
+    super
+  end
+
+  def signatures
+    current_user_uuid = (current_user.andand.is_admin && params[:uuid]) ||
+      current_user.uuid
+    act_as_system_user do
+      @objects = Link.where(owner_uuid: system_user_uuid,
+                            link_class: 'signature',
+                            name: 'click',
+                            tail_kind: 'arvados#user',
+                            tail_uuid: current_user_uuid,
+                            head_kind: 'arvados#collection')
+    end
+    @response_resource_name = 'link'
+    render_list
+  end
+
+  def sign
+    current_user_uuid = current_user.uuid
+    act_as_system_user do
+      @object = Link.create(link_class: 'signature',
+                            name: 'click',
+                            tail_kind: 'arvados#user',
+                            tail_uuid: current_user_uuid,
+                            head_kind: 'arvados#collection',
+                            head_uuid: params[:uuid])
+    end
+    show
+  end
+
+  def create
+    usage_error
+  end
+  
+  def new
+    usage_error
+  end
+
+  def update
+    usage_error
+  end
+
+  def destroy
+    usage_error
+  end
+
+  protected
+  def usage_error
+    raise ArgumentError.new \
+    "Manage user agreements via Collections and Links instead."
+  end
+  
+end
index a0d2f54792d6a34f5bb0d9d05cd962495ccfccb6..441db9947e88480f5bc4de3968f889164601302b 100644 (file)
@@ -40,4 +40,46 @@ class Arvados::V1::UsersController < ApplicationController
       }
     end
   end
+
+  def activate
+    if current_user.andand.is_admin && params[:uuid]
+      @object = User.find params[:uuid]
+    else
+      @object = current_user
+    end
+    if not @object.is_active
+      if not (current_user.is_admin or @object.is_invited)
+        logger.warn "User #{@object.uuid} called users.activate " +
+          "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').
+          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
+        if todo_uuids == []
+          @object.update_attributes is_active: true
+          logger.info "User #{@object.uuid} activated"
+        else
+          logger.warn "User #{@object.uuid} called users.activate " +
+            "before signing agreements #{todo_uuids.inspect}"
+          raise ArgumentError.new \
+          "Cannot activate without user agreements #{todo_uuids.inspect}."
+        end
+      end
+    end
+    show
+  end
 end
index eaaf7b5b93598a0d8a173998e7867ff67933a570..3ac47d46cf221bd7bf9b549281892e2f6e9326d7 100644 (file)
@@ -20,6 +20,22 @@ class UserSessionsController < ApplicationController
     end
 
     user = User.find_by_identity_url(omniauth['info']['identity_url'])
+    if not user
+      # Check for permission to log in to an existing User record with
+      # a different identity_url
+      Link.where(link_class: 'permission',
+                 name: 'can_login',
+                 tail_kind: 'email',
+                 tail_uuid: omniauth['info']['email'],
+                 head_kind: 'arvados#user').each do |link|
+        if prefix = link.properties[:identity_url_prefix]
+          if prefix == omniauth['info']['identity_url'][0..prefix.size-1]
+            user = User.find_by_uuid(link.head_uuid)
+            break if user
+          end
+        end
+      end
+    end
     if not user
       # New user registration
       user = User.new(:email => omniauth['info']['email'],
@@ -31,6 +47,10 @@ class UserSessionsController < ApplicationController
       user.email = omniauth['info']['email']
       user.first_name = omniauth['info']['first_name']
       user.last_name = omniauth['info']['last_name']
+      if user.identity_url.nil?
+        # First login to a pre-activated account
+        user.identity_url = omniauth['info']['identity_url']
+      end
     end
 
     # prevent ArvadosModel#before_create and _update from throwing
index e628edaa3fab292e21ade5c5f24903001aa54fb7..0364c08f5024060c2e3584d7c7047111914d8156 100644 (file)
@@ -19,6 +19,7 @@ class User < ArvadosModel
     t.add :identity_url
     t.add :is_active
     t.add :is_admin
+    t.add :is_invited
     t.add :prefs
   end
 
@@ -28,6 +29,12 @@ class User < ArvadosModel
     "#{first_name} #{last_name}"
   end
 
+  def is_invited
+    (self.is_active ||
+     Rails.configuration.new_users_are_active ||
+     self.groups_i_can(:read).select { |x| x.match /-f+$/ }.first)
+  end
+
   def groups_i_can(verb)
     self.group_permissions.select { |uuid, mask| mask[verb] }.keys
   end
diff --git a/services/api/app/models/user_agreement.rb b/services/api/app/models/user_agreement.rb
new file mode 100644 (file)
index 0000000..1790dea
--- /dev/null
@@ -0,0 +1,4 @@
+class UserAgreement < Collection
+  # This class exists so that Arvados::V1::SchemaController includes
+  # UserAgreementsController's methods in the discovery document.
+end
index 5f9900d62d6ab8d3e56cd8554611aedea9da6b72..65b6a17587e4a0490e417d4d8b04120f98186934 100644 (file)
@@ -89,10 +89,13 @@ Server::Application.routes.draw do
       match '/jobs/:uuid/log_tail_follow' => 'jobs#log_tail_follow'
       post '/jobs/:uuid/cancel' => 'jobs#cancel'
       match '/users/:uuid/event_stream' => 'users#event_stream'
+      post '/users/:uuid/activate' => 'users#activate'
       match '/virtual_machines/get_all_logins' => 'virtual_machines#get_all_logins'
       match '/virtual_machines/:uuid/logins' => 'virtual_machines#logins'
       post '/api_client_authorizations/create_system_auth' => 'api_client_authorizations#create_system_auth'
       match '/repositories/get_all_permissions' => 'repositories#get_all_permissions'
+      get '/user_agreements/signatures' => 'user_agreements#signatures'
+      post '/user_agreements/sign' => 'user_agreements#sign'
       resources :collections
       resources :links
       resources :nodes
@@ -112,6 +115,7 @@ Server::Application.routes.draw do
       resources :repositories
       resources :traits
       resources :humans
+      resources :user_agreements
     end
   end
 
index 7231b146917edeaf8b21e48d6a638ca7a4296bf4..94dabf99cfebd4a9359f92e2b37b573417857ad2 100644 (file)
@@ -30,6 +30,18 @@ inactive:
   api_token: 5s29oj2hzmcmpq80hx9cta0rl5wuf3xfd6r7disusaptz7h9m0
   expires_at: 2038-01-01 00:00:00
 
+inactive_uninvited:
+  api_client: untrusted
+  user: inactive_uninvited
+  api_token: 62mhllc0otp78v08e3rpa3nsmf8q8ogk47f7u5z4erp5gpj9al
+  expires_at: 2038-01-01 00:00:00
+
+inactive_but_signed_user_agreement:
+  api_client: untrusted
+  user: inactive_but_signed_user_agreement
+  api_token: 64k3bzw37iwpdlexczj02rw3m333rrb8ydvn2qq99ohv68so5k
+  expires_at: 2038-01-01 00:00:00
+
 expired:
   api_client: untrusted
   user: active
diff --git a/services/api/test/fixtures/collections.yml b/services/api/test/fixtures/collections.yml
new file mode 100644 (file)
index 0000000..8cbaea5
--- /dev/null
@@ -0,0 +1,9 @@
+user_agreement:
+  uuid: b519d9cb706a29fc7ea24dbea2f05851
+  owner_uuid: qr1hi-tpzed-tpj2ff66551eyym
+  created_at: 2013-12-26T19:22:54Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2013-12-26T19:22:54Z
+  updated_at: 2013-12-26T19:22:54Z
+  manifest_text: ". 6a4ff0499484c6c79c95cd8c566bd25f+249025 0:249025:GNU_General_Public_License,_version_3.pdf\n"
index ebf22341a6febe7d2052083e868a0983b0debc1f..c9b52dcfc7c3337d169fd604413135ec366a7a4b 100644 (file)
@@ -3,3 +3,8 @@ public:
   owner_uuid: zzzzz-tpzed-d9tiejq69daie8f
   name: Public
   description: Public Group
+
+all_users:
+  uuid: zzzzz-j7d0g-fffffffffffffff
+  owner_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  name: All users
diff --git a/services/api/test/fixtures/links.yml b/services/api/test/fixtures/links.yml
new file mode 100644 (file)
index 0000000..e871c9b
--- /dev/null
@@ -0,0 +1,79 @@
+user_agreement_required:
+  uuid: zzzzz-o0j2j-j2qe76q7s3c8aro
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2013-12-26T19:52:21Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  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_kind: arvados#collection
+  head_uuid: b519d9cb706a29fc7ea24dbea2f05851
+  properties: {}
+
+user_agreement_signed_by_active:
+  uuid: zzzzz-o0j2j-4x85a69tqlrud1z
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2013-12-26T20:52:21Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  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_kind: arvados#collection
+  head_uuid: b519d9cb706a29fc7ea24dbea2f05851
+  properties: {}
+
+user_agreement_signed_by_inactive:
+  uuid: zzzzz-o0j2j-lh7er2o3k6bmetw
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2013-12-26T20:52:21Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  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_kind: arvados#collection
+  head_uuid: b519d9cb706a29fc7ea24dbea2f05851
+  properties: {}
+
+inactive_user_member_of_all_users_group:
+  uuid: zzzzz-o0j2j-osckxpy5hl5fjk5
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2013-12-26T20:52:21Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  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: {}
+
+inactive_signed_ua_user_member_of_all_users_group:
+  uuid: zzzzz-o0j2j-qkhyjcr6tidk652
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2013-12-26T20:52:21Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  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: {}
index b4f8fec3b8a79f2ad150333cf6456611af9d6ad6..ab43907da00a62cac8089aa18b6cfd45535c48d8 100644 (file)
@@ -20,9 +20,19 @@ active:
   is_admin: false
   prefs: {}
 
+inactive_uninvited:
+  uuid: zzzzz-tpzed-rf2ec3ryh4vb5ma
+  email: inactive-uninvited-user@arvados.local
+  first_name: Inactive and Uninvited
+  last_name: User
+  identity_url: https://inactive-uninvited-user.openid.local
+  is_active: false
+  is_admin: false
+  prefs: {}
+
 inactive:
   uuid: zzzzz-tpzed-x9kqpd79egh49c7
-  email: active-user@arvados.local
+  email: inactive-user@arvados.local
   first_name: Inactive
   last_name: User
   identity_url: https://inactive-user.openid.local
@@ -30,3 +40,13 @@ inactive:
   is_admin: false
   prefs: {}
 
+inactive_but_signed_user_agreement:
+  uuid: zzzzz-tpzed-7sg468ezxwnodxs
+  email: inactive-user-signed-ua@arvados.local
+  first_name: Inactive But Agreeable
+  last_name: User
+  identity_url: https://inactive-but-agreeable-user.openid.local
+  is_active: false
+  is_admin: false
+  prefs: {}
+
diff --git a/services/api/test/functional/arvados/v1/user_agreements_controller_test.rb b/services/api/test/functional/arvados/v1/user_agreements_controller_test.rb
new file mode 100644 (file)
index 0000000..05bdef5
--- /dev/null
@@ -0,0 +1,46 @@
+require 'test_helper'
+
+class Arvados::V1::UserAgreementsControllerTest < ActionController::TestCase
+
+  test "active user get user agreements" do
+    authorize_with :active
+    get :index
+    assert_response :success
+    assert_not_nil assigns(:objects)
+    agreements_list = JSON.parse(@response.body)
+    assert_not_nil agreements_list['items']
+    assert_not_nil agreements_list['items'][0]
+  end
+
+  test "active user get user agreement signatures" do
+    authorize_with :active
+    get :signatures
+    assert_response :success
+    assert_not_nil assigns(:objects)
+    agreements_list = JSON.parse(@response.body)
+    assert_not_nil agreements_list['items']
+    assert_not_nil agreements_list['items'][0]
+    assert_equal 1, agreements_list['items'].count
+  end
+
+  test "inactive user get user agreements" do
+    authorize_with :inactive
+    get :index
+    assert_response :success
+    assert_not_nil assigns(:objects)
+    agreements_list = JSON.parse(@response.body)
+    assert_not_nil agreements_list['items']
+    assert_not_nil agreements_list['items'][0]
+  end
+
+  test "uninvited user receives empty list of user agreements" do
+    authorize_with :inactive_uninvited
+    get :index
+    assert_response :success
+    assert_not_nil assigns(:objects)
+    agreements_list = JSON.parse(@response.body)
+    assert_not_nil agreements_list['items']
+    assert_nil agreements_list['items'][0]
+  end
+
+end
diff --git a/services/api/test/functional/arvados/v1/users_controller_test.rb b/services/api/test/functional/arvados/v1/users_controller_test.rb
new file mode 100644 (file)
index 0000000..4b52c9b
--- /dev/null
@@ -0,0 +1,41 @@
+require 'test_helper'
+
+class Arvados::V1::UsersControllerTest < ActionController::TestCase
+
+  test "activate a user after signing UA" do
+    authorize_with :inactive_but_signed_user_agreement
+    get :current
+    assert_response :success
+    me = JSON.parse(@response.body)
+    post :activate, uuid: me['uuid']
+    assert_response :success
+    assert_not_nil assigns(:object)
+    me = JSON.parse(@response.body)
+    assert_equal true, me['is_active']
+  end
+
+  test "refuse to activate a user before signing UA" do
+    authorize_with :inactive
+    get :current
+    assert_response :success
+    me = JSON.parse(@response.body)
+    post :activate, uuid: me['uuid']
+    assert_response 422
+    get :current
+    assert_response :success
+    me = JSON.parse(@response.body)
+    assert_equal false, me['is_active']
+  end
+
+  test "activate an already-active user" do
+    authorize_with :active
+    get :current
+    assert_response :success
+    me = JSON.parse(@response.body)
+    post :activate, uuid: me['uuid']
+    assert_response :success
+    me = JSON.parse(@response.body)
+    assert_equal true, me['is_active']
+  end
+
+end
index 9955a3500ffe9830c41974679954e6956c6f4f21..8c12ffb5df4718df4c5d19dbd5eae5d109da764a 100644 (file)
@@ -19,3 +19,6 @@ class ActiveSupport::TestCase
 
   # Add more helper methods to be used by all tests here...
 end
+
+# Ensure permissions are computed from the test fixtures.
+User.invalidate_permissions_cache