Merge branch 'master' into 14874-protected-collection-properties
authorLucas Di Pentima <ldipentima@veritasgenetics.com>
Mon, 24 Jun 2019 13:22:52 +0000 (10:22 -0300)
committerLucas Di Pentima <ldipentima@veritasgenetics.com>
Mon, 24 Jun 2019 13:22:52 +0000 (10:22 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima@veritasgenetics.com>

20 files changed:
apps/workbench/app/assets/javascripts/components/edit_tags.js
doc/_config.yml
doc/_includes/_admin_list_collections_without_property_py.liquid [new file with mode: 0644]
doc/_includes/_admin_set_property_to_collections_under_project_py.liquid [new file with mode: 0644]
doc/_includes/_admin_update_collection_property_py.liquid [new file with mode: 0644]
doc/_includes/_install_compute_docker.liquid
doc/_includes/_mount_types.liquid
doc/admin/collection-managed-properties.html.textile.liquid [new file with mode: 0644]
doc/install/install-postgresql.html.textile.liquid
doc/sdk/index.html.textile.liquid
doc/user/topics/collection-versioning.html.textile.liquid
lib/config/config.default.yml
lib/config/generated_config.go
lib/controller/federation.go
sdk/java/src/main/java/org/arvados/sdk/Arvados.java
services/api/app/models/arvados_model.rb
services/api/app/models/collection.rb
services/api/test/unit/collection_test.rb
services/keep-balance/balance_test.go
tools/arvbox/lib/arvbox/docker/runit-docker/runit-docker.c

index 1fddb2651ef96a2cbec2e5dff1da030a0f33c3eb..5e02279ea19ca8b1ef3721d7b56078e09d9f4096 100644 (file)
@@ -182,6 +182,24 @@ window.TagEditorApp = {
         tag.value.map(function() { vnode.state.dirty(true) })
         tag.name.map(m.redraw)
     },
+    fixTag: function(vnode, tagName) {
+        // Recover tag if deleted, recover its value if modified
+        savedTagValue = vnode.state.saved_tags[tagName]
+        if (savedTagValue === undefined) {
+            return
+        }
+        found = false
+        vnode.state.tags.forEach(function(tag) {
+            if (tag.name == tagName) {
+                tag.value = vnode.state.saved_tags[tagName]
+                found = true
+            }
+        })
+        if (!found) {
+            vnode.state.tags.pop() // Remove the last empty row
+            vnode.state.appendTag(vnode, tagName, savedTagValue)
+        }
+    },
     oninit: function(vnode) {
         vnode.state.sessionDB = new SessionDB()
         // Get vocabulary
@@ -190,8 +208,10 @@ window.TagEditorApp = {
         m.request('/vocabulary.json?v=' + vocabularyTimestamp).then(vnode.state.vocabulary)
         vnode.state.editMode = vnode.attrs.targetEditable
         vnode.state.tags = []
+        vnode.state.saved_tags = {}
         vnode.state.dirty = m.stream(false)
         vnode.state.dirty.map(m.redraw)
+        vnode.state.error = m.stream('')
         vnode.state.objPath = 'arvados/v1/' + vnode.attrs.targetController + '/' + vnode.attrs.targetUuid
         // Get tags
         vnode.state.sessionDB.request(
@@ -213,6 +233,7 @@ window.TagEditorApp = {
                     }
                     // Data synced with server, so dirty state should be false
                     vnode.state.dirty(false)
+                    vnode.state.saved_tags = o.properties
                     // Add new tag row when the last one is completed
                     vnode.state.dirty.map(function() {
                         if (!vnode.state.editMode) { return }
@@ -249,9 +270,37 @@ window.TagEditorApp = {
                             }
                         ).then(function(v) {
                             vnode.state.dirty(false)
+                            vnode.state.error('')
+                            vnode.state.saved_tags = tags
+                        }).catch(function(err) {
+                            if (err.errors !== undefined) {
+                                var re = /protected\ property/i
+                                var protected_props = []
+                                err.errors.forEach(function(error) {
+                                    if (re.test(error)) {
+                                        prop = error.split(':')[1].trim()
+                                        vnode.state.fixTag(vnode, prop)
+                                        protected_props.push(prop)
+                                    }
+                                })
+                                if (protected_props.length > 0) {
+                                    errMsg = "Protected properties cannot be updated: " + protected_props.join(', ')
+                                } else {
+                                    errMsg = errors.join(', ')
+                                }
+                            } else {
+                                errMsg = err
+                            }
+                            vnode.state.error(errMsg)
                         })
                     }
-                }, vnode.state.dirty() ? ' Save changes ' : ' Saved ')
+                }, vnode.state.dirty() ? ' Save changes ' : ' Saved '),
+                m('span', {
+                    style: {
+                        color: '#ff0000',
+                        margin: '0px 10px'
+                    }
+                }, [ vnode.state.error() ])
             ]),
             // Tags table
             m(TagEditorTable, {
index 21c4257a90cd9915aa27ec09aff912c9587e5690..20a2085c11b3403071c83d75ed12a4fc8068d119 100644 (file)
@@ -180,8 +180,10 @@ navbar:
     - Cloud:
       - admin/storage-classes.html.textile.liquid
       - admin/spot-instances.html.textile.liquid
-    - Other:
+    - Data Management:
       - admin/collection-versioning.html.textile.liquid
+      - admin/collection-managed-properties.html.textile.liquid
+    - Other:
       - admin/federation.html.textile.liquid
       - admin/controlling-container-reuse.html.textile.liquid
       - admin/logs-table-management.html.textile.liquid
diff --git a/doc/_includes/_admin_list_collections_without_property_py.liquid b/doc/_includes/_admin_list_collections_without_property_py.liquid
new file mode 100644 (file)
index 0000000..a65aec9
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+import arvados
+import arvados.util as util
+
+filters = [['properties.responsible_person_uuid', 'exists', False]]
+cols = util.list_all(arvados.api().collections().list, filters=filters, select=['uuid', 'name'])
+
+print("Found {} collections:".format(len(cols)))
+for c in cols:
+    print('{}, "{}"'.format(c['uuid'], c['name']))
\ No newline at end of file
diff --git a/doc/_includes/_admin_set_property_to_collections_under_project_py.liquid b/doc/_includes/_admin_set_property_to_collections_under_project_py.liquid
new file mode 100644 (file)
index 0000000..06ef6f0
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+import arvados
+import arvados.util as util
+
+def get_subproject_uuids(api, root_uuid):
+    uuids = []
+    groups = util.list_all(api.groups().list, filters=[['owner_uuid', '=', '{}'.format(root_uuid)]], select=['uuid'])
+    for g in groups:
+        uuids += ([g['uuid']] + get_subproject_uuids(api, g['uuid']))
+    return uuids
+
+def get_cols(api, filters):
+    cols = util.list_all(api.collections().list, filters=filters, select=['uuid', 'properties'])
+    return cols
+
+# Search for collections on project hierarchy rooted at root_uuid
+root_uuid = 'zzzzz-j7d0g-ppppppppppppppp'
+# Set the property to the UUID below
+responsible_uuid = 'zzzzz-tpzed-xxxxxxxxxxxxxxx'
+
+api = arvados.api()
+for p_uuid in [root_uuid] + get_subproject_uuids(api, root_uuid):
+    f = [['properties.responsible_person_uuid', 'exists', False],
+         ['owner_uuid', '=', p_uuid]]
+    cols = get_cols(api, f)
+    print("Found {} collections owned by {}".format(len(cols), p_uuid))
+    for c in cols:
+        print(" - Updating collection {}".format(c["uuid"]))
+        props = c['properties']
+        props['responsible_person_uuid'] = responsible_uuid
+        api.collections().update(uuid=c['uuid'], body={'properties': props}).execute()
\ No newline at end of file
diff --git a/doc/_includes/_admin_update_collection_property_py.liquid b/doc/_includes/_admin_update_collection_property_py.liquid
new file mode 100644 (file)
index 0000000..a2a8f9d
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+import arvados
+import arvados.util as util
+
+old_uuid = "zzzzz-tpzed-xxxxxxxxxxxxxxx"
+new_uuid = "zzzzz-tpzed-yyyyyyyyyyyyyyy"
+
+api = arvados.api()
+filters = [['properties.responsible_person_uuid', '=', '{}'.format(old_uuid)]]
+cols = util.list_all(api.collections().list, filters=filters, select=['uuid', 'properties'])
+
+print("Found {} collections".format(len(cols)))
+for c in cols:
+    print("Updating collection {}".format(c["uuid"]))
+    props = c['properties']
+    props['responsible_person_uuid'] = new_uuid
+    api.collections().update(uuid=c['uuid'], body={'properties': props}).execute()
\ No newline at end of file
index ea3640e52a077ba0d5ce626740af691f701f4439..69b49e83cd827bc48e45c5b3ff164985377b594f 100644 (file)
@@ -70,7 +70,7 @@ EOF</span>
 
 h2. Download and tag the latest arvados/jobs docker image
 
-In order to start workflows from workbench, there needs to be Docker image tagged @arvados/jobs:latest@. The following command downloads the latest arvados/jobs image from Docker Hub, loads it into Keep, and tags it as 'latest'.  In this example @$project_uuid@ should be the the UUID of the "Arvados Standard Docker Images" project.
+In order to start workflows from workbench, there needs to be Docker image tagged @arvados/jobs:latest@. The following command downloads the latest arvados/jobs image from Docker Hub, loads it into Keep, and tags it as 'latest'.  In this example @$project_uuid@ should be the UUID of the "Arvados Standard Docker Images" project.
 
 <notextile>
 <pre><code>~$ <span class="userinput">arv-keepdocker --pull arvados/jobs latest --project-uuid $project_uuid</span>
index edf8edfd4ab278ee6b134f3ba8dbe773487f3442..de417e14880a4163074f5cbf468b244ea4fac574 100644 (file)
@@ -115,4 +115,4 @@ table(table table-bordered table-condensed).
 
 h2(#symlinks-in-output). Symlinks in output
 
-When a container's output_path is a tmp mount backed by local disk, this output directory can contain symlinks to other files in the the output directory, or to collection mount points.  If the symlink leads to a collection mount, efficiently copy the collection into the output collection.  Symlinks leading to files or directories are expanded and created as regular files in the output collection.  Further, whether symlinks are relative or absolute, every symlink target (even targets that are symlinks themselves) must point to a path in either the output directory or a collection mount.
+When a container's output_path is a tmp mount backed by local disk, this output directory can contain symlinks to other files in the output directory, or to collection mount points.  If the symlink leads to a collection mount, efficiently copy the collection into the output collection.  Symlinks leading to files or directories are expanded and created as regular files in the output collection.  Further, whether symlinks are relative or absolute, every symlink target (even targets that are symlinks themselves) must point to a path in either the output directory or a collection mount.
diff --git a/doc/admin/collection-managed-properties.html.textile.liquid b/doc/admin/collection-managed-properties.html.textile.liquid
new file mode 100644 (file)
index 0000000..c6943ac
--- /dev/null
@@ -0,0 +1,85 @@
+---
+layout: default
+navsection: admin
+title: Configuring collection's managed properties
+...
+
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+Collection's managed properties allow a cluster administrator to enable some special behaviors regarding properties at creation & update times.
+This page describes how to enable and configure these behaviors on the API server.
+
+h3. API Server configuration
+
+The @Collections.ManagedProperties@ setting from the @config.yml@ file is used for enabling any of the following behaviors:
+
+h4. Pre-assigned property key & value
+
+For every newly created collection, assign a predefined key/value pair if it isn't already passed at creation time:
+
+<pre>
+Collections:
+  ManagedProperties:
+    foo: {value: bar}
+</pre>
+
+h4. Original owner UUID
+
+This behavior will assign to a property key the UUID of the user who owns the collection's contaning project.
+
+<pre>
+Collections:
+  ManagedProperties:
+    responsible_person_uuid: {function: original_owner}
+</pre>
+
+h4. Protected properties
+
+If there's a need to prevent a non-admin user from modifying a specific property, even by its owner, the @protected@ attribute can be set to @true@, like so:
+
+<pre>
+Collections:
+  ManagedProperties:
+    responsible_person_uuid: {function: original_owner, protected: true}
+</pre>
+
+This property can be applied to any of the defined managed properties. If missing, it's assumed as being @false@ by default.
+
+h3. Supporting example scripts
+
+When enabling this feature, there may be pre-existing collections that won't have the managed properties just configured. The following script examples may be helpful to sync these older collections.
+
+For the following examples we assume that the @responsible_person_uuid@ property is set as @{function: original_owner, protected: true}@.
+
+h4. List uuid/names of collections without @responsible_person_uuid@ property
+
+The collection's managed properties feature assigns the configured properties to newly created collections. This means that previously existing collections won't get the default properties and if needed, they should be assigned manually.
+
+The following example script outputs a listing of collection UUIDs and names of those collections that don't include the @responsible_person_uuid@ property.
+
+{% codeblock as python %}
+{% include 'admin_list_collections_without_property_py' %}
+{% endcodeblock %}
+
+h4. Update the @responsible_person_uuid@ property from nil to X in the project hierarchy rooted at P
+
+When enabling @responsible_person_uuid@, new collections will get this property's value set to the user who owns the root project where the collection is placed, but older collections won't have the property set. The following example script allows an administrator to set the @responsible_person_uuid@ property to collections below a certaing project hierarchy.
+
+{% codeblock as python %}
+{% include 'admin_set_property_to_collections_under_project_py' %}
+{% endcodeblock %}
+
+h4. Update the @responsible_person_uuid@ property from X to Y on all collections
+
+This example can be useful to change responsibility from one user to another.
+
+Please note that the following code should run with admin privileges, assuming that the managed property is @protected@.
+
+{% codeblock as python %}
+{% include 'admin_update_collection_property_py' %}
+{% endcodeblock %}
+
index 5e638ff850c93949f466dc6bea2a7e6d8951a100..7324bb30ff2d91e6c13ff720173e7326772ad4e3 100644 (file)
@@ -11,7 +11,7 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 
 Two Arvados Rails servers store data in a PostgreSQL database: the SSO server, and the API server.  The API server requires at least version *9.4* of PostgreSQL.  Beyond that, you have the flexibility to deploy PostgreSQL any way that the Rails servers will be able to connect to it.  Our recommended deployment strategy is:
 
-* Install PostgreSQL on the the same host as the SSO server, and dedicate that install to hosting the SSO database.  This provides the best security for the SSO server, because the database does not have to accept any client connections over the network.  Typical load on the SSO server is light enough that deploying both it and its database on the same host does not compromise performance.
+* Install PostgreSQL on the same host as the SSO server, and dedicate that install to hosting the SSO database.  This provides the best security for the SSO server, because the database does not have to accept any client connections over the network.  Typical load on the SSO server is light enough that deploying both it and its database on the same host does not compromise performance.
 * If you want to provide the most scalability for your Arvados cluster, install PostgreSQL for the API server on a dedicated host.  This gives you the most flexibility to avoid resource contention, and tune performance separately for the API server and its database.  If performance is less of a concern for your installation, you can install PostgreSQL on the API server host directly, as with the SSO server.
 
 Find the section for your distribution below, and follow it to install PostgreSQL on each host where you will deploy it.  Then follow the steps in the later section(s) to set up PostgreSQL for the Arvados service(s) that need it.
index 8ff5ddc0994537981d83983756f2dc231ba0d769..5fbc3d5dd2d0848cd735c58e5900cac71deadd92 100644 (file)
@@ -20,4 +20,4 @@ This section documents language bindings for the "Arvados API":{{site.baseurl}}/
 * "Java SDK v2":{{site.baseurl}}/sdk/java-v2/index.html
 * "Java SDK v1":{{site.baseurl}}/sdk/java/index.html
 
-Many Arvados Workbench pages, under the the *Advanced* tab, provide examples of API and SDK use for accessing the current resource .
+Many Arvados Workbench pages, under the *Advanced* tab, provide examples of API and SDK use for accessing the current resource .
index 01670d88ff90f8f88fa5ee49f0bac27f5763a252..9a32de0d0b35ba335b9890ce918188ecf987b866 100644 (file)
@@ -12,7 +12,7 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 
 When collection versioning is enabled, updating certain collection attributes (@name@, @description@, @properties@, @manifest_text@) will save a copy of the collection state, previous to the update. This copy (a new collection record) will have its own @uuid@, and a @current_version_uuid@ attribute pointing to the current version's @uuid@.
 
-Every collection has a @version@ attribute that indicates its version number, starting from 1 on new collections and incrementing by 1 with every versionable update. All collections point to their most current version via the @current_version_uuid@ attribute, being @uuid@ and @current_version_uuid@ equal on those collection records that are the the current version of themselves. Note that the "current version" collection record doesn't change its @uuid@, "past versions" are saved as new records every time it's needed, pointing to the current collection record.
+Every collection has a @version@ attribute that indicates its version number, starting from 1 on new collections and incrementing by 1 with every versionable update. All collections point to their most current version via the @current_version_uuid@ attribute, being @uuid@ and @current_version_uuid@ equal on those collection records that are the current version of themselves. Note that the "current version" collection record doesn't change its @uuid@, "past versions" are saved as new records every time it's needed, pointing to the current collection record.
 
 A version will be saved when one of the following conditions is true:
 
index dc128e56b5aef01d90531317ee61c498b394aa92..aaf88a0d70bbe4c8948d93c5aa63fb12b3af5491 100644 (file)
@@ -279,6 +279,19 @@ Clusters:
       # > 0s = auto-create a new version when older than the specified number of seconds.
       PreserveVersionIfIdle: -1s
 
+      # Managed collection properties. At creation time, if the client didn't
+      # provide the listed keys, they will be automatically populated following
+      # one of the following behaviors:
+      #
+      # * UUID of the user who owns the containing project.
+      #   responsible_person_uuid: {function: original_owner, protected: true}
+      #
+      # * Default concrete value.
+      #   foo_bar: {value: baz, protected: false}
+      #
+      # If protected is true, only an admin user can modify its value.
+      ManagedProperties: {}
+
     Login:
       # These settings are provided by your OAuth2 provider (e.g.,
       # sso-provider).
index 98cd343bd1698980901cd3ec55461bd3e4953755..15004ca98638c48be9d289bd39b68f18b6d1cb5a 100644 (file)
@@ -285,6 +285,19 @@ Clusters:
       # > 0s = auto-create a new version when older than the specified number of seconds.
       PreserveVersionIfIdle: -1s
 
+      # Managed collection properties. At creation time, if the client didn't
+      # provide the listed keys, they will be automatically populated following
+      # one of the following behaviors:
+      #
+      # * UUID of the user who owns the containing project.
+      #   responsible_person_uuid: {function: original_owner, protected: true}
+      #
+      # * Default concrete value.
+      #   foo_bar: {value: baz, protected: false}
+      #
+      # If protected is true, only an admin user can modify its value.
+      ManagedProperties: {}
+
     Login:
       # These settings are provided by your OAuth2 provider (e.g.,
       # sso-provider).
index 557c7c3563d59c23644370765f466e63517f4d5a..ed2eb31c7830db992d0e86b42b6de68c275a428f 100644 (file)
@@ -275,7 +275,7 @@ func (h *Handler) saltAuthToken(req *http.Request, remote string) (updatedReq *h
        }
        updatedReq.Header.Set("Authorization", "Bearer "+token)
 
-       // Remove api_token=... from the the query string, in case we
+       // Remove api_token=... from the query string, in case we
        // end up forwarding the request.
        if values, err := url.ParseQuery(updatedReq.URL.RawQuery); err != nil {
                return nil, err
index 2b8bbee6721ffd3f47e0304d81ed4f1f2a51a7da..102c2a3c27ead04b3eef3f8664a58a116d1ce4ef 100644 (file)
@@ -459,7 +459,7 @@ public class Arvados {
 
   public static void main(String[] args){
     System.out.println("Welcome to Arvados Java SDK.");
-    System.out.println("Please refer to http://doc.arvados.org/sdk/java/index.html to get started with the the SDK.");
+    System.out.println("Please refer to http://doc.arvados.org/sdk/java/index.html to get started with the SDK.");
   }
 
 }
index 91c5a1923c95beaa674dc255835dda50153b5661..8c8ad8e254467cadbb5874c3f7519a574ccfe21c 100644 (file)
@@ -409,6 +409,18 @@ class ArvadosModel < ApplicationRecord
     end
   end
 
+  def user_owner_uuid
+    if self.owner_uuid.nil?
+      return current_user.uuid
+    end
+    owner_class = ArvadosModel.resource_class_for_uuid(self.owner_uuid)
+    if owner_class == User
+      self.owner_uuid
+    else
+      owner_class.find_by_uuid(self.owner_uuid).user_owner_uuid
+    end
+  end
+
   def logged_attributes
     attributes.except(*Rails.configuration.AuditLogs.UnloggedAttributes)
   end
index 775ebdb49486861d73f20b97ba562d77656de765..1ffaa6a319c449a4d2f65980a0848a035b8ffc86 100644 (file)
@@ -22,6 +22,7 @@ class Collection < ArvadosModel
 
   before_validation :default_empty_manifest
   before_validation :default_storage_classes, on: :create
+  before_validation :default_properties, on: :create
   before_validation :check_encoding
   before_validation :check_manifest_validity
   before_validation :check_signatures
@@ -31,6 +32,7 @@ class Collection < ArvadosModel
   validate :ensure_storage_classes_contain_non_empty_strings
   validate :versioning_metadata_updates, on: :update
   validate :past_versions_cannot_be_updated, on: :update
+  validate :protected_default_properties_updates, on: :update
   after_validation :set_file_count_and_total_size
   before_save :set_file_names
   around_update :manage_versioning, unless: :is_past_version?
@@ -606,6 +608,23 @@ class Collection < ArvadosModel
     self.storage_classes_confirmed ||= []
   end
 
+  # Sets default properties at creation time
+  def default_properties
+    default_props = Rails.configuration.Collections.ManagedProperties.with_indifferent_access
+    if default_props.empty?
+      return
+    end
+    (default_props.keys - self.properties.keys).each do |key|
+      if default_props[key].has_key?('value')
+        self.properties[key] = default_props[key]['value']
+      elsif default_props[key]['function'].andand == 'original_owner'
+        self.properties[key] = self.user_owner_uuid
+      else
+        logger.warn "Unidentified default property definition '#{key}': #{default_props[key].inspect}"
+      end
+    end
+  end
+
   def portable_manifest_text
     self.class.munge_manifest_locators(manifest_text) do |match|
       if match[2] # size
@@ -667,6 +686,25 @@ class Collection < ArvadosModel
     end
   end
 
+  def protected_default_properties_updates
+    default_properties = Rails.configuration.Collections.ManagedProperties.with_indifferent_access
+    if default_properties.empty? || !properties_changed? || current_user.is_admin
+      return true
+    end
+    protected_props = default_properties.keys.select do |p|
+      Rails.configuration.Collections.ManagedProperties[p]['protected']
+    end
+    # Pre-existent protected properties can't be updated
+    invalid_updates = properties_was.keys.select{|p| properties_was[p] != properties[p]} & protected_props
+    if !invalid_updates.empty?
+      invalid_updates.each do |p|
+        errors.add("protected property cannot be updated:", p)
+      end
+      raise PermissionDeniedError.new
+    end
+    true
+  end
+
   def versioning_metadata_updates
     valid = true
     if !is_past_version? && current_version_uuid_changed?
index 08d5b1fb72cb9544ba8ae651e0936826462703d3..4790501ddd5aaa875f5c716d88b9ac0464cec607 100644 (file)
@@ -1012,4 +1012,65 @@ class CollectionTest < ActiveSupport::TestCase
     SweepTrashedObjects.sweep_now
     assert_empty Collection.where(uuid: uuid)
   end
+
+  test "create collections with default properties" do
+    Rails.configuration.Collections.ManagedProperties = {
+      'default_prop1' => {'value' => 'prop1_value'},
+      'responsible_person_uuid' => {'function' => 'original_owner'}
+    }
+    # Test collection without initial properties
+    act_as_user users(:active) do
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      assert_not_empty c.properties
+      assert_equal 'prop1_value', c.properties['default_prop1']
+      assert_equal users(:active).uuid, c.properties['responsible_person_uuid']
+    end
+    # Test collection with default_prop1 property already set
+    act_as_user users(:active) do
+      c = Collection.create(manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n",
+                            properties: {'default_prop1' => 'custom_value'})
+      assert c.valid?
+      assert_not_empty c.properties
+      assert_equal 'custom_value', c.properties['default_prop1']
+      assert_equal users(:active).uuid, c.properties['responsible_person_uuid']
+    end
+    # Test collection inside a sub project
+    act_as_user users(:active) do
+      c = Collection.create(manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n",
+                            owner_uuid: groups(:asubproject).uuid)
+      assert c.valid?
+      assert_not_empty c.properties
+      assert_equal users(:active).uuid, c.properties['responsible_person_uuid']
+    end
+  end
+
+  test "update collection with protected default properties" do
+    Rails.configuration.Collections.ManagedProperties = {
+      'default_prop1' => {'value' => 'prop1_value', 'protected' => true},
+    }
+    act_as_user users(:active) do
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      assert_not_empty c.properties
+      assert_equal 'prop1_value', c.properties['default_prop1']
+      # Add new property
+      c.properties['prop2'] = 'value2'
+      c.save!
+      c.reload
+      assert_equal 'value2', c.properties['prop2']
+      # Try to change protected property's value
+      c.properties['default_prop1'] = 'new_value'
+      assert_raises(ArvadosModel::PermissionDeniedError) do
+        c.save!
+      end
+      # Admins are allowed to change protected properties
+      act_as_system_user do
+        c.properties['default_prop1'] = 'new_value'
+        c.save!
+        c.reload
+        assert_equal 'new_value', c.properties['default_prop1']
+      end
+    end
+  end
 end
index 423546c46a9c179aab3b15522912667c72cdbb8f..2259b3d8cf8e87ac83d08df9a15dfdd17c8d02c6 100644 (file)
@@ -13,7 +13,6 @@ import (
        "time"
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
-
        check "gopkg.in/check.v1"
 )
 
@@ -694,7 +693,7 @@ func (bal *balancerSuite) try(c *check.C, t tester) {
 
 // srvList returns the KeepServices, sorted in rendezvous order and
 // then selected by idx. For example, srvList(3, slots{0, 1, 4})
-// returns the the first-, second-, and fifth-best servers for storing
+// returns the first-, second-, and fifth-best servers for storing
 // bal.knownBlkid(3).
 func (bal *balancerSuite) srvList(knownBlockID int, order slots) (srvs []*KeepService) {
        for _, i := range order {
index 825a35fd0b8ed724fb56a40a05e2e92d2a6b4268..43d1e0e5c83c6a101c3723a8bd2d5c8f279acbbe 100644 (file)
@@ -25,7 +25,7 @@ int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
     real_sigaction(SIGINT, act, oldact);
   }
 
-  // Forward the call the the real sigaction.
+  // Forward the call to the real sigaction.
   return real_sigaction(signum, act, oldact);
 }