From 3f9ab9bc91d9e6acab651cb842d35a2477a2bade Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 20 Mar 2017 18:40:28 +0530 Subject: [PATCH 01/54] Protect tag partial --- .../_create_protected_tag.html.haml | 32 +++++++++++++++++++ .../protected_tags/_dropdown.html.haml | 15 +++++++++ .../projects/protected_tags/_index.html.haml | 18 +++++++++++ .../protected_tags/_tags_list.html.haml | 6 ++++ 4 files changed, 71 insertions(+) create mode 100644 app/views/projects/protected_tags/_create_protected_tag.html.haml create mode 100644 app/views/projects/protected_tags/_dropdown.html.haml create mode 100644 app/views/projects/protected_tags/_index.html.haml create mode 100644 app/views/projects/protected_tags/_tags_list.html.haml diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml new file mode 100644 index 00000000000..6ffe7b4c24f --- /dev/null +++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml @@ -0,0 +1,32 @@ += form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| + .panel.panel-default + .panel-heading + %h3.panel-title + Protect a tag + .panel-body + .form-horizontal + = form_errors(@protected_branch) + .form-group + = f.label :name, class: 'col-md-2 text-right' do + Tag: + .col-md-10 + = render partial: "projects/protected_tags/dropdown", locals: { f: f } + .help-block + = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches') + such as + %code *-stable + or + %code production/* + are supported + .form-group + %label.col-md-2.text-right{ for: 'push_access_levels_attributes' } + Allowed to push: + .col-md-10 + .push_access_levels-container + = dropdown_tag('Select', + options: { toggle_class: 'js-allowed-to-push wide', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) + + .panel-footer + = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/protected_tags/_dropdown.html.haml b/app/views/projects/protected_tags/_dropdown.html.haml new file mode 100644 index 00000000000..3da153bc521 --- /dev/null +++ b/app/views/projects/protected_tags/_dropdown.html.haml @@ -0,0 +1,15 @@ += f.hidden_field(:name) + += dropdown_tag('Select tag or create wildcard', + options: { toggle_class: 'js-protected-tag-select js-filter-submit wide', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected tag", + footer_content: true, + data: { show_no: true, show_any: true, show_upcoming: true, + selected: params[:protected_branch_name], + project_id: @project.try(:id) } }) do + + %ul.dropdown-footer-list + %li + = link_to '#', title: "New Protected Tag", class: "create-new-protected-tag" do + Create wildcard + %code diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml new file mode 100644 index 00000000000..3a1da1202e9 --- /dev/null +++ b/app/views/projects/protected_tags/_index.html.haml @@ -0,0 +1,18 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('protected_branches') + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + Protected tags + %p.prepend-top-20 + By default, Protected branches are designed to: + %ul + %li Prevent tag pushes from everybody except Masters + %li Prevent anyone from force pushing to the tag + %li Prevent anyone from deleting the tag + .col-lg-9 + - if can? current_user, :admin_project, @project + = render 'projects/protected_tags/create_protected_tag' + + = render "projects/protected_tags/tags_list" diff --git a/app/views/projects/protected_tags/_tags_list.html.haml b/app/views/projects/protected_tags/_tags_list.html.haml new file mode 100644 index 00000000000..e7ce90393e9 --- /dev/null +++ b/app/views/projects/protected_tags/_tags_list.html.haml @@ -0,0 +1,6 @@ +.panel.panel-default.protected-tags-list + .panel-heading + %h3.panel-title + Protected tag (0) + %p.settings-message.text-center + There are currently no protected tags, protect a tag with the form above. From 421a386d8c76d9947bdfc007b890c853313f0c46 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 20 Mar 2017 18:40:55 +0530 Subject: [PATCH 02/54] Apply styles to protected tag same as protected branch --- app/assets/stylesheets/pages/projects.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index efa47be9a73..f7ef9275560 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -745,7 +745,8 @@ pre.light-well { } } -.protected-branches-list { +.protected-branches-list, +.protected-tags-list { margin-bottom: 30px; a { From d20c5427af94c7043c3cc7c04288a056921b812c Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 20 Mar 2017 18:41:19 +0530 Subject: [PATCH 03/54] Render protected tags within Project Settings > Repository --- app/views/projects/settings/repository/show.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 4c02302e161..5402320cb66 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -3,3 +3,4 @@ = render @deploy_keys = render "projects/protected_branches/index" += render "projects/protected_tags/index" From 97a02fca6bf24a2e6e5feb4b164a9265511903c2 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 21 Mar 2017 21:21:56 +0530 Subject: [PATCH 04/54] Protected Tags Classes --- .../protected_tag_access_dropdown.js | 29 +++++++ .../protected_tags/protected_tag_create.js | 45 +++++++++++ .../protected_tags/protected_tag_dropdown.js | 79 +++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js create mode 100644 app/assets/javascripts/protected_tags/protected_tag_create.js create mode 100644 app/assets/javascripts/protected_tags/protected_tag_dropdown.js diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js new file mode 100644 index 00000000000..b85c2991dd9 --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js @@ -0,0 +1,29 @@ +/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, max-len */ + +(global => { + global.gl = global.gl || {}; + + gl.ProtectedTagAccessDropdown = class { + constructor(options) { + const { $dropdown, data, onSelect } = options; + + $dropdown.glDropdown({ + data: data, + selectable: true, + inputId: $dropdown.data('input-id'), + fieldName: $dropdown.data('field-name'), + toggleLabel(item, el) { + if (el.is('.is-active')) { + return item.text; + } else { + return 'Select'; + } + }, + clicked(item, $el, e) { + e.preventDefault(); + onSelect(); + } + }); + } + }; +})(window); diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js new file mode 100644 index 00000000000..4c652e7747f --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -0,0 +1,45 @@ +/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */ +/* global ProtectedTagDropdown */ + +(global => { + global.gl = global.gl || {}; + + gl.ProtectedTagCreate = class { + constructor() { + this.$wrap = this.$form = $('.new_protected_tag'); + this.buildDropdowns(); + } + + buildDropdowns() { + const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + + // Cache callback + this.onSelectCallback = this.onSelect.bind(this); + + // Allowed to Push dropdown + new gl.ProtectedTagAccessDropdown({ + $dropdown: $allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelectCallback + }); + + // Select default + $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); + + // Protected tag dropdown + new ProtectedTagDropdown({ + $dropdown: this.$wrap.find('.js-protected-tag-select'), + onSelect: this.onSelectCallback + }); + } + + // This will run after clicked callback + onSelect() { + // Enable submit button + const $tagInput = this.$wrap.find('input[name="protected_tag[name]"]'); + const $allowedToPushInput = this.$wrap.find('input[name="protected_tag[push_access_levels_attributes][0][access_level]"]'); + + this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToPushInput.length)); + } + }; +})(window); diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js new file mode 100644 index 00000000000..5a0356f502c --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js @@ -0,0 +1,79 @@ +/* eslint-disable comma-dangle, no-unused-vars */ + +class ProtectedTagDropdown { + constructor(options) { + this.onSelect = options.onSelect; + this.$dropdown = options.$dropdown; + this.$dropdownContainer = this.$dropdown.parent(); + this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); + this.$protectedTag = this.$dropdownContainer.find('.create-new-protected-tag'); + + this.buildDropdown(); + this.bindEvents(); + + // Hide footer + this.$dropdownFooter.addClass('hidden'); + } + + buildDropdown() { + this.$dropdown.glDropdown({ + data: this.getProtectedTags.bind(this), + filterable: true, + remote: false, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel(selected) { + return (selected && 'id' in selected) ? selected.title : 'Protected Tag'; + }, + fieldName: 'protected_tag[name]', + text(protectedTag) { + return _.escape(protectedTag.title); + }, + id(protectedTag) { + return _.escape(protectedTag.id); + }, + onFilter: this.toggleCreateNewButton.bind(this), + clicked: (item, $el, e) => { + e.preventDefault(); + this.onSelect(); + } + }); + } + + bindEvents() { + this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this)); + } + + onClickCreateWildcard() { + this.$dropdown.data('glDropdown').remote.execute(); + this.$dropdown.data('glDropdown').selectRowAtIndex(); + } + + getProtectedTags(term, callback) { + if (this.selectedTag) { + callback(gon.open_branches.concat(this.selectedTag)); + } else { + callback(gon.open_branches); + } + } + + toggleCreateNewButton(tagName) { + this.selectedTag = { + title: tagName, + id: tagName, + text: tagName + }; + + if (tagName) { + this.$dropdownContainer + .find('.create-new-protected-tag code') + .text(tagName); + } + + this.$dropdownFooter.toggleClass('hidden', !tagName); + } +} + +window.ProtectedTagDropdown = ProtectedTagDropdown; From c5c4370a7bb71130c1fcca57a069e3e8626d9108 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 21 Mar 2017 21:22:12 +0530 Subject: [PATCH 05/54] Re-export protected tag classes --- app/assets/javascripts/protected_tags/protected_tags_bundle.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 app/assets/javascripts/protected_tags/protected_tags_bundle.js diff --git a/app/assets/javascripts/protected_tags/protected_tags_bundle.js b/app/assets/javascripts/protected_tags/protected_tags_bundle.js new file mode 100644 index 00000000000..d84d2e1ef70 --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tags_bundle.js @@ -0,0 +1,3 @@ +require('./protected_tag_access_dropdown'); +require('./protected_tag_create'); +require('./protected_tag_dropdown'); From 167b86000838aab32b13e19f4213805f460b7d46 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 21 Mar 2017 21:22:38 +0530 Subject: [PATCH 06/54] Initialize Protected tags feature under Repository Settings page --- app/assets/javascripts/dispatcher.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 3557f6f617e..6234092bdc2 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -320,6 +320,7 @@ const UserCallout = require('./user_callout'); case 'projects:repository:show': new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); + new gl.ProtectedTagCreate(); break; case 'projects:ci_cd:show': new gl.ProjectVariables(); From 0fd2fd652a18ccceb725ce2f8f7683ad6ad3c224 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 21 Mar 2017 21:23:17 +0530 Subject: [PATCH 07/54] Update property names for tags --- .../projects/protected_tags/_create_protected_tag.html.haml | 4 ++-- app/views/projects/protected_tags/_dropdown.html.haml | 2 +- app/views/projects/protected_tags/_index.html.haml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml index 6ffe7b4c24f..110c24ac9e4 100644 --- a/app/views/projects/protected_tags/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| += form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'new_protected_tag' } do |f| .panel.panel-default .panel-heading %h3.panel-title @@ -26,7 +26,7 @@ = dropdown_tag('Select', options: { toggle_class: 'js-allowed-to-push wide', dropdown_class: 'dropdown-menu-selectable', - data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) + data: { field_name: 'protected_tag[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) .panel-footer = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/protected_tags/_dropdown.html.haml b/app/views/projects/protected_tags/_dropdown.html.haml index 3da153bc521..74851519077 100644 --- a/app/views/projects/protected_tags/_dropdown.html.haml +++ b/app/views/projects/protected_tags/_dropdown.html.haml @@ -5,7 +5,7 @@ filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected tag", footer_content: true, data: { show_no: true, show_any: true, show_upcoming: true, - selected: params[:protected_branch_name], + selected: params[:protected_tag_name], project_id: @project.try(:id) } }) do %ul.dropdown-footer-list diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml index 3a1da1202e9..0965bf75eae 100644 --- a/app/views/projects/protected_tags/_index.html.haml +++ b/app/views/projects/protected_tags/_index.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('protected_branches') + = page_specific_javascript_bundle_tag('protected_tags') .row.prepend-top-default.append-bottom-default .col-lg-3 From 99859b01f4dad72dc51c0765db89800915a94f36 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Tue, 21 Mar 2017 21:23:31 +0530 Subject: [PATCH 08/54] Add protected tags to bundle config --- config/webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/config/webpack.config.js b/config/webpack.config.js index c6794d6b944..d861fa0c7a4 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -39,6 +39,7 @@ var config = { network: './network/network_bundle.js', profile: './profile/profile_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js', + protected_tags: './protected_tags/protected_tags_bundle.js', snippet: './snippet/snippet_bundle.js', terminal: './terminal/terminal_bundle.js', u2f: ['vendor/u2f'], From 1a416a42f1c1b876ecd96687e41696bc915cc2c2 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Wed, 15 Mar 2017 22:26:48 +0000 Subject: [PATCH 09/54] Database migrations for protected tags Added schema.rb for protected tags --- .../20170309173138_create_protected_tags.rb | 39 +++++++++++++++++++ db/schema.rb | 30 +++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20170309173138_create_protected_tags.rb diff --git a/db/migrate/20170309173138_create_protected_tags.rb b/db/migrate/20170309173138_create_protected_tags.rb new file mode 100644 index 00000000000..c69ef970410 --- /dev/null +++ b/db/migrate/20170309173138_create_protected_tags.rb @@ -0,0 +1,39 @@ +class CreateProtectedTags < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + GitlabAccessMaster = 40 + + def change + create_table :protected_tags do |t| + t.integer :project_id, null: false + t.string :name, null: false + t.string :timestamps #TODO: `null: false`? Missing from protected_branches + end + + add_index :protected_tags, :project_id + + create_table :protected_tag_merge_access_levels do |t| + t.references :protected_tag, index: { name: "index_protected_tag_merge_access" }, foreign_key: true, null: false + + t.integer :access_level, default: GitlabAccessMaster, null: true #TODO: was false, check schema + t.integer :group_id #TODO: check why group/user id missing from CE + t.integer :user_id + t.timestamps null: false + end + + create_table :protected_tag_push_access_levels do |t| + t.references :protected_tag, index: { name: "index_protected_tag_push_access" }, foreign_key: true, null: false + t.integer :access_level, default: GitlabAccessMaster, null: true #TODO: was false, check schema + t.integer :group_id + t.integer :user_id + t.timestamps null: false + end + + #TODO: These had rubocop set to disable Migration/AddConcurrentForeignKey + # add_foreign_key :protected_tag_merge_access_levels, :namespaces, column: :group_id + # add_foreign_key :protected_tag_push_access_levels, :namespaces, column: :group_id + end +end diff --git a/db/schema.rb b/db/schema.rb index f96a7d21890..05b28b6a63b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170315194013) do +ActiveRecord::Schema.define(version: 20170317203554) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -963,6 +963,32 @@ ActiveRecord::Schema.define(version: 20170315194013) do add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree + create_table "protected_tag_merge_access_levels", force: :cascade do |t| + t.integer "protected_tag_id", null: false + t.integer "access_level", default: 40, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "protected_tag_merge_access_levels", ["protected_tag_id"], name: "index_protected_tag_merge_access", using: :btree + + create_table "protected_tag_push_access_levels", force: :cascade do |t| + t.integer "protected_tag_id", null: false + t.integer "access_level", default: 40, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "protected_tag_push_access_levels", ["protected_tag_id"], name: "index_protected_tag_push_access", using: :btree + + create_table "protected_tags", force: :cascade do |t| + t.integer "project_id", null: false + t.string "name", null: false + t.string "timestamps" + end + + add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree + create_table "releases", force: :cascade do |t| t.string "tag" t.text "description" @@ -1305,6 +1331,8 @@ ActiveRecord::Schema.define(version: 20170315194013) do add_foreign_key "project_statistics", "projects", on_delete: :cascade add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_push_access_levels", "protected_branches" + add_foreign_key "protected_tag_merge_access_levels", "protected_tags" + add_foreign_key "protected_tag_push_access_levels", "protected_tags" add_foreign_key "subscriptions", "projects", on_delete: :cascade add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade From 91ed8ed687ee9edbda0098475e66ad41f886d7a5 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Wed, 15 Mar 2017 22:29:07 +0000 Subject: [PATCH 10/54] Protected tags copy/paste from protected branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Should provide basic CRUD backend for frontend to work from. Doesn’t include frontend, API, or the internal API used from gitlab-shell --- .../projects/protected_tags_controller.rb | 58 ++++++++++++ app/models/concerns/protected_ref_access.rb | 21 +++++ app/models/concerns/protected_tag_access.rb | 21 +++++ app/models/project.rb | 1 + app/models/protected_branch.rb | 39 ++------ app/models/protected_ref_matcher.rb | 52 ++++++++++ app/models/protected_tag.rb | 39 ++++++++ app/models/protected_tag/push_access_level.rb | 21 +++++ app/services/protected_tags/create_service.rb | 11 +++ app/services/protected_tags/update_service.rb | 13 +++ config/routes/project.rb | 2 + .../protected_tags_controller_spec.rb | 10 ++ spec/factories/protected_tags.rb | 22 +++++ .../protected_tags/access_control_ce_spec.rb | 79 ++++++++++++++++ spec/features/protected_tags_spec.rb | 94 +++++++++++++++++++ spec/models/protected_tag_spec.rb | 14 +++ .../protected_tags/create_service_spec.rb | 23 +++++ 17 files changed, 488 insertions(+), 32 deletions(-) create mode 100644 app/controllers/projects/protected_tags_controller.rb create mode 100644 app/models/concerns/protected_ref_access.rb create mode 100644 app/models/concerns/protected_tag_access.rb create mode 100644 app/models/protected_ref_matcher.rb create mode 100644 app/models/protected_tag.rb create mode 100644 app/models/protected_tag/push_access_level.rb create mode 100644 app/services/protected_tags/create_service.rb create mode 100644 app/services/protected_tags/update_service.rb create mode 100644 spec/controllers/projects/protected_tags_controller_spec.rb create mode 100644 spec/factories/protected_tags.rb create mode 100644 spec/features/protected_tags/access_control_ce_spec.rb create mode 100644 spec/features/protected_tags_spec.rb create mode 100644 spec/models/protected_tag_spec.rb create mode 100644 spec/services/protected_tags/create_service_spec.rb diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb new file mode 100644 index 00000000000..5ab5d1d997b --- /dev/null +++ b/app/controllers/projects/protected_tags_controller.rb @@ -0,0 +1,58 @@ +class Projects::ProtectedTagsController < Projects::ApplicationController + include RepositorySettingsRedirect + # Authorize + before_action :require_non_empty_project + before_action :authorize_admin_project! + before_action :load_protected_tag, only: [:show, :update, :destroy] + + layout "project_settings" + + def index + redirect_to_repository_settings(@project) + end + + def create + @protected_tag = ::ProtectedTags::CreateService.new(@project, current_user, protected_tag_params).execute + unless @protected_tag.persisted? + flash[:alert] = @protected_tags.errors.full_messages.join(', ').html_safe + end + redirect_to_repository_settings(@project) + end + + def show + @matching_tags = @protected_tag.matching(@project.repository.tags) + end + + def update + @protected_tag = ::ProtectedTags::UpdateService.new(@project, current_user, protected_tag_params).execute(@protected_tag) + + if @protected_tag.valid? + respond_to do |format| + format.json { render json: @protected_tag, status: :ok } + end + else + respond_to do |format| + format.json { render json: @protected_tag.errors, status: :unprocessable_entity } + end + end + end + + def destroy + @protected_tag.destroy + + respond_to do |format| + format.html { redirect_to_repository_settings(@project) } + format.js { head :ok } + end + end + + private + + def load_protected_tag + @protected_tag = @project.protected_tags.find(params[:id]) + end + + def protected_tag_params + params.require(:protected_tag).permit(:name, push_access_levels_attributes: [:access_level, :id]) + end +end diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb new file mode 100644 index 00000000000..9dd4d9c6f24 --- /dev/null +++ b/app/models/concerns/protected_ref_access.rb @@ -0,0 +1,21 @@ +module ProtectedBranchAccess + extend ActiveSupport::Concern + + included do + belongs_to :protected_branch + delegate :project, to: :protected_branch + + scope :master, -> { where(access_level: Gitlab::Access::MASTER) } + scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } + end + + def humanize + self.class.human_access_levels[self.access_level] + end + + def check_access(user) + return true if user.is_admin? + + project.team.max_member_access(user.id) >= access_level + end +end diff --git a/app/models/concerns/protected_tag_access.rb b/app/models/concerns/protected_tag_access.rb new file mode 100644 index 00000000000..cf66a6434b5 --- /dev/null +++ b/app/models/concerns/protected_tag_access.rb @@ -0,0 +1,21 @@ +module ProtectedTagAccess + extend ActiveSupport::Concern + + included do + belongs_to :protected_tag + delegate :project, to: :protected_tag + + scope :master, -> { where(access_level: Gitlab::Access::MASTER) } + scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } + end + + def humanize + self.class.human_access_levels[self.access_level] + end + + def check_access(user) + return true if user.is_admin? + + project.team.max_member_access(user.id) >= access_level + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 4a3faff7d5b..3f1a8a1a1e1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -132,6 +132,7 @@ class Project < ActiveRecord::Base has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet' has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy + has_many :protected_tags, dependent: :destroy has_many :project_authorizations has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 39e979ef15b..7681d5b5112 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -18,50 +18,25 @@ class ProtectedBranch < ActiveRecord::Base project.commit(self.name) end - # Returns all protected branches that match the given branch name. - # This realizes all records from the scope built up so far, and does - # _not_ return a relation. - # - # This method optionally takes in a list of `protected_branches` to search - # through, to avoid calling out to the database. def self.matching(branch_name, protected_branches: nil) - (protected_branches || all).select { |protected_branch| protected_branch.matches?(branch_name) } + ProtectedRefMatcher.matching(ProtectedBranch, branch_name, protected_refs: protected_branches) end - # Returns all branches (among the given list of branches [`Gitlab::Git::Branch`]) - # that match the current protected branch. def matching(branches) - branches.select { |branch| self.matches?(branch.name) } + ref_matcher.matching(branches) end - # Checks if the protected branch matches the given branch name. def matches?(branch_name) - return false if self.name.blank? - - exact_match?(branch_name) || wildcard_match?(branch_name) + ref_matcher.matches?(branch_name) end - # Checks if this protected branch contains a wildcard def wildcard? - self.name && self.name.include?('*') + ref_matcher.wildcard? end - protected + private - def exact_match?(branch_name) - self.name == branch_name - end - - def wildcard_match?(branch_name) - wildcard_regex === branch_name - end - - def wildcard_regex - @wildcard_regex ||= begin - name = self.name.gsub('*', 'STAR_DONT_ESCAPE') - quoted_name = Regexp.quote(name) - regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') - /\A#{regex_string}\z/ - end + def ref_matcher + @ref_matcher ||= ProtectedRefMatcher.new(self) end end diff --git a/app/models/protected_ref_matcher.rb b/app/models/protected_ref_matcher.rb new file mode 100644 index 00000000000..83f44240259 --- /dev/null +++ b/app/models/protected_ref_matcher.rb @@ -0,0 +1,52 @@ +class ProtectedRefMatcher + def initialize(protected_ref) + @protected_ref = protected_ref + end + + # Returns all protected refs that match the given ref name. + # This realizes all records from the scope built up so far, and does + # _not_ return a relation. + # + # This method optionally takes in a list of `protected_refs` to search + # through, to avoid calling out to the database. + def self.matching(type, ref_name, protected_refs: nil) + (protected_refs || type.all).select { |protected_ref| protected_ref.matches?(ref_name) } + end + + # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`]) + # that match the current protected ref. + def matching(refs) + refs.select { |ref| @protected_ref.matches?(ref.name) } + end + + # Checks if the protected ref matches the given ref name. + def matches?(ref_name) + return false if @protected_ref.name.blank? + + exact_match?(ref_name) || wildcard_match?(ref_name) + end + + # Checks if this protected ref contains a wildcard + def wildcard? + @protected_ref.name && @protected_ref.name.include?('*') + end + + protected + + def exact_match?(ref_name) + @protected_ref.name == ref_name + end + + def wildcard_match?(ref_name) + wildcard_regex === ref_name + end + + def wildcard_regex + @wildcard_regex ||= begin + name = @protected_ref.name.gsub('*', 'STAR_DONT_ESCAPE') + quoted_name = Regexp.quote(name) + regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') + /\A#{regex_string}\z/ + end + end +end diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb new file mode 100644 index 00000000000..d307549aa49 --- /dev/null +++ b/app/models/protected_tag.rb @@ -0,0 +1,39 @@ +class ProtectedTag < ActiveRecord::Base + include Gitlab::ShellAdapter + + belongs_to :project + validates :name, presence: true + validates :project, presence: true + + has_many :push_access_levels, dependent: :destroy + + validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." } + + accepts_nested_attributes_for :push_access_levels + + def commit + project.commit(self.name) + end + + def self.matching(tag_name, protected_tags: nil) + ProtectedRefMatcher.matching(ProtectedTag, tag_name, protected_refs: protected_tags) + end + + def matching(branches) + ref_matcher.matching(branches) + end + + def matches?(tag_name) + ref_matcher.matches?(tag_name) + end + + def wildcard? + ref_matcher.wildcard? + end + + private + + def ref_matcher + @ref_matcher ||= ProtectedRefMatcher.new(self) + end +end diff --git a/app/models/protected_tag/push_access_level.rb b/app/models/protected_tag/push_access_level.rb new file mode 100644 index 00000000000..9282af841ce --- /dev/null +++ b/app/models/protected_tag/push_access_level.rb @@ -0,0 +1,21 @@ +class ProtectedTag::PushAccessLevel < ActiveRecord::Base + include ProtectedTagAccess + + validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, + Gitlab::Access::DEVELOPER, + Gitlab::Access::NO_ACCESS] } + + def self.human_access_levels + { + Gitlab::Access::MASTER => "Masters", + Gitlab::Access::DEVELOPER => "Developers + Masters", + Gitlab::Access::NO_ACCESS => "No one" + }.with_indifferent_access + end + + def check_access(user) + return false if access_level == Gitlab::Access::NO_ACCESS + + super + end +end diff --git a/app/services/protected_tags/create_service.rb b/app/services/protected_tags/create_service.rb new file mode 100644 index 00000000000..faba7865a17 --- /dev/null +++ b/app/services/protected_tags/create_service.rb @@ -0,0 +1,11 @@ +module ProtectedTags + class CreateService < BaseService + attr_reader :protected_tag + + def execute + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) + + project.protected_tags.create(params) + end + end +end diff --git a/app/services/protected_tags/update_service.rb b/app/services/protected_tags/update_service.rb new file mode 100644 index 00000000000..8a2419efd7b --- /dev/null +++ b/app/services/protected_tags/update_service.rb @@ -0,0 +1,13 @@ +module ProtectedTags + class UpdateService < BaseService + attr_reader :protected_tag + + def execute(protected_tag) + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) + + @protected_tag = protected_tag + @protected_tag.update(params) + @protected_tag + end + end +end diff --git a/config/routes/project.rb b/config/routes/project.rb index 44b8ae7aedd..f0735bf73e8 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -134,6 +134,8 @@ constraints(ProjectUrlConstrainer.new) do end resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :protected_tags, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :variables, only: [:index, :show, :update, :create, :destroy] resources :triggers, only: [:index, :create, :edit, :update, :destroy] do member do diff --git a/spec/controllers/projects/protected_tags_controller_spec.rb b/spec/controllers/projects/protected_tags_controller_spec.rb new file mode 100644 index 00000000000..729017c1483 --- /dev/null +++ b/spec/controllers/projects/protected_tags_controller_spec.rb @@ -0,0 +1,10 @@ +require('spec_helper') + +describe Projects::ProtectedTagsController do + # describe "GET #index" do + # let(:project) { create(:project_empty_repo, :public) } + # it "redirects empty repo to projects page" do + # get(:index, namespace_id: project.namespace.to_param, project_id: project) + # end + # end +end diff --git a/spec/factories/protected_tags.rb b/spec/factories/protected_tags.rb new file mode 100644 index 00000000000..f0016b37d66 --- /dev/null +++ b/spec/factories/protected_tags.rb @@ -0,0 +1,22 @@ +FactoryGirl.define do + factory :protected_tag do + name + project + + after(:build) do |protected_tag| + protected_tag.push_access_levels.new(access_level: Gitlab::Access::MASTER) + end + + trait :developers_can_push do + after(:create) do |protected_tag| + protected_tag.push_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER) + end + end + + trait :no_one_can_push do + after(:create) do |protected_tag| + protected_tag.push_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS) + end + end + end +end diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb new file mode 100644 index 00000000000..545d3bca74d --- /dev/null +++ b/spec/features/protected_tags/access_control_ce_spec.rb @@ -0,0 +1,79 @@ +# RSpec.shared_examples "protected tags > access control > CE" do +# ProtectedTag::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| +# it "allows creating protected tags that #{access_type_name} can push to" do +# visit namespace_project_protected_tags_path(project.namespace, project) +# set_protected_tag_name('master') +# within('.new_protected_tag') do +# allowed_to_push_button = find(".js-allowed-to-push") + +# unless allowed_to_push_button.text == access_type_name +# allowed_to_push_button.click +# within(".dropdown.open .dropdown-menu") { click_on access_type_name } +# end +# end +# click_on "Protect" + +# expect(ProtectedTag.count).to eq(1) +# expect(ProtectedTag.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) +# end + +# it "allows updating protected tags so that #{access_type_name} can push to them" do +# visit namespace_project_protected_tags_path(project.namespace, project) +# set_protected_tag_name('master') +# click_on "Protect" + +# expect(ProtectedTag.count).to eq(1) + +# within(".protected-tags-list") do +# find(".js-allowed-to-push").click + +# within('.js-allowed-to-push-container') do +# expect(first("li")).to have_content("Roles") +# click_on access_type_name +# end +# end + +# wait_for_ajax +# expect(ProtectedTag.last.push_access_levels.map(&:access_level)).to include(access_type_id) +# end +# end + +# ProtectedTag::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| +# it "allows creating protected tags that #{access_type_name} can merge to" do +# visit namespace_project_protected_tags_path(project.namespace, project) +# set_protected_tag_name('master') +# within('.new_protected_tag') do +# allowed_to_merge_button = find(".js-allowed-to-merge") + +# unless allowed_to_merge_button.text == access_type_name +# allowed_to_merge_button.click +# within(".dropdown.open .dropdown-menu") { click_on access_type_name } +# end +# end +# click_on "Protect" + +# expect(ProtectedTag.count).to eq(1) +# expect(ProtectedTag.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) +# end + +# it "allows updating protected tags so that #{access_type_name} can merge to them" do +# visit namespace_project_protected_tags_path(project.namespace, project) +# set_protected_tag_name('master') +# click_on "Protect" + +# expect(ProtectedTag.count).to eq(1) + +# within(".protected-tags-list") do +# find(".js-allowed-to-merge").click + +# within('.js-allowed-to-merge-container') do +# expect(first("li")).to have_content("Roles") +# click_on access_type_name +# end +# end + +# wait_for_ajax +# expect(ProtectedTag.last.merge_access_levels.map(&:access_level)).to include(access_type_id) +# end +# end +# end diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb new file mode 100644 index 00000000000..546d6037ef7 --- /dev/null +++ b/spec/features/protected_tags_spec.rb @@ -0,0 +1,94 @@ +# require 'spec_helper' +# Dir["./spec/features/protected_tags/*.rb"].sort.each { |f| require f } + +# feature 'Projected Tags', feature: true, js: true do +# include WaitForAjax + +# let(:user) { create(:user, :admin) } +# let(:project) { create(:project) } + +# before { login_as(user) } + +# def set_protected_tag_name(tag_name) +# find(".js-protected-tag-select").click +# find(".dropdown-input-field").set(tag_name) +# click_on("Create wildcard #{tag_name}") +# end + +# describe "explicit protected tags" do +# it "allows creating explicit protected tags" do +# visit namespace_project_protected_tags_path(project.namespace, project) +# set_protected_tag_name('some-tag') +# click_on "Protect" + +# within(".protected-tags-list") { expect(page).to have_content('some-tag') } +# expect(ProtectedTag.count).to eq(1) +# expect(ProtectedTag.last.name).to eq('some-tag') +# end + +# it "displays the last commit on the matching tag if it exists" do +# commit = create(:commit, project: project) +# project.repository.add_tag(user, 'some-tag', commit.id) + +# visit namespace_project_protected_tags_path(project.namespace, project) +# set_protected_tag_name('some-tag') +# click_on "Protect" + +# within(".protected-tags-list") { expect(page).to have_content(commit.id[0..7]) } +# end + +# it "displays an error message if the named tag does not exist" do +# visit namespace_project_protected_tags_path(project.namespace, project) +# set_protected_tag_name('some-tag') +# click_on "Protect" + +# within(".protected-tags-list") { expect(page).to have_content('tag was removed') } +# end +# end + +# describe "wildcard protected tags" do +# it "allows creating protected tags with a wildcard" do +# visit namespace_project_protected_tags_path(project.namespace, project) +# set_protected_tag_name('*-stable') +# click_on "Protect" + +# within(".protected-tags-list") { expect(page).to have_content('*-stable') } +# expect(ProtectedTag.count).to eq(1) +# expect(ProtectedTag.last.name).to eq('*-stable') +# end + +# it "displays the number of matching tags" do +# project.repository.add_tag(user, 'production-stable', 'master') +# project.repository.add_tag(user, 'staging-stable', 'master') + +# visit namespace_project_protected_tags_path(project.namespace, project) +# set_protected_tag_name('*-stable') +# click_on "Protect" + +# within(".protected-tags-list") { expect(page).to have_content("2 matching tags") } +# end + +# it "displays all the tags matching the wildcard" do +# project.repository.add_tag(user, 'production-stable', 'master') +# project.repository.add_tag(user, 'staging-stable', 'master') +# project.repository.add_tag(user, 'development', 'master') + +# visit namespace_project_protected_tags_path(project.namespace, project) +# set_protected_tag_name('*-stable') +# click_on "Protect" + +# visit namespace_project_protected_tags_path(project.namespace, project) +# click_on "2 matching tags" + +# within(".protected-tags-list") do +# expect(page).to have_content("production-stable") +# expect(page).to have_content("staging-stable") +# expect(page).not_to have_content("development") +# end +# end +# end + +# describe "access control" do +# include_examples "protected tags > access control > CE" +# end +# end diff --git a/spec/models/protected_tag_spec.rb b/spec/models/protected_tag_spec.rb new file mode 100644 index 00000000000..05ad532935a --- /dev/null +++ b/spec/models/protected_tag_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe ProtectedTag, models: true do + subject { build_stubbed(:protected_branch) } + + describe 'Associations' do + it { is_expected.to belong_to(:project) } + end + + describe 'Validation' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:name) } + end +end diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb new file mode 100644 index 00000000000..e1fd73dbd07 --- /dev/null +++ b/spec/services/protected_tags/create_service_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe ProtectedTags::CreateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { project.owner } + let(:params) do + { + name: 'master', + merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }], + push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }] + } + end + + describe '#execute' do + subject(:service) { described_class.new(project, user, params) } + + it 'creates a new protected tag' do + expect { service.execute }.to change(ProtectedTag, :count).by(1) + expect(project.protected_tags.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) + expect(project.protected_tags.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) + end + end +end From f51eac1df967856299467f65ac6fb81e2d610ff5 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Fri, 17 Mar 2017 19:55:15 +0000 Subject: [PATCH 11/54] Settings::RepositoryController includes protected tags in JS --- .../settings/repository_controller.rb | 30 +++++++++++++---- app/models/concerns/protected_ref_access.rb | 32 +++++++++---------- app/models/project.rb | 1 + 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index b6ce4abca45..5160ee5e1e4 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -7,22 +7,23 @@ module Projects @deploy_keys = DeployKeysPresenter .new(@project, current_user: current_user) - define_protected_branches + define_protected_refs end private - def define_protected_branches - load_protected_branches + def define_protected_refs + @protected_branches = @project.protected_branches.order(:name).page(params[:page]) + @protected_tags = @project.protected_tags.order(:name).page(params[:page]) @protected_branch = @project.protected_branches.new + @protected_tag = @project.protected_tags.new load_gon_index end - def load_protected_branches - @protected_branches = @project.protected_branches.order(:name).page(params[:page]) - end def access_levels_options + #TODO: consider protected tags + #TODO: Refactor ProtectedBranch::PushAccessLevel so it doesn't mention branches { push_access_levels: { roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text| @@ -37,13 +38,28 @@ module Projects } end + #TODO: Move to Protections::TagMatcher.new(project).unprotected + def unprotected_tags + exact_protected_tag_names = @project.protected_tags.reject(&:wildcard?).map(&:name) + tag_names = @project.repository.tags.map(&:name) + non_open_tag_names = Set.new(exact_protected_tag_names).intersection(Set.new(tag_names)) + @project.repository.tags.reject { |tag| non_open_tag_names.include? tag.name } + end + + def unprotected_tags_hash + tags = unprotected_tags.map { |tag| { text: tag.name, id: tag.name, title: tag.name } } + { open_tags: tags } + end + def open_branches branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } { open_branches: branches } end def load_gon_index - gon.push(open_branches.merge(access_levels_options)) + gon.push(open_branches) + gon.push(unprotected_tags_hash) + gon.push(access_levels_options) end end end diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index 9dd4d9c6f24..2870cd9385b 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -1,21 +1,21 @@ -module ProtectedBranchAccess - extend ActiveSupport::Concern +# module ProtectedRefAccess +# extend ActiveSupport::Concern - included do - belongs_to :protected_branch - delegate :project, to: :protected_branch +# included do +# # belongs_to :protected_branch +# # delegate :project, to: :protected_branch - scope :master, -> { where(access_level: Gitlab::Access::MASTER) } - scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } - end +# scope :master, -> { where(access_level: Gitlab::Access::MASTER) } +# scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } +# end - def humanize - self.class.human_access_levels[self.access_level] - end +# def humanize +# self.class.human_access_levels[self.access_level] +# end - def check_access(user) - return true if user.is_admin? +# def check_access(user) +# return true if user.is_admin? - project.team.max_member_access(user.id) >= access_level - end -end +# project.team.max_member_access(user.id) >= access_level +# end +# end diff --git a/app/models/project.rb b/app/models/project.rb index 3f1a8a1a1e1..c04effc53bd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -866,6 +866,7 @@ class Project < ActiveRecord::Base end # Branches that are not _exactly_ matched by a protected branch. + #TODO: Move to Protections::BranchMatcher.new(project).unprotecte def open_branches exact_protected_branch_names = protected_branches.reject(&:wildcard?).map(&:name) branch_names = repository.branches.map(&:name) From 18b445ade4280c03e73ccbf2ca4d175e97a887c8 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Thu, 30 Mar 2017 23:35:19 +0100 Subject: [PATCH 12/54] Protected tags can be added/listed via UI --- .../protected_tags/protected_tag_dropdown.js | 4 +-- .../_create_protected_tag.html.haml | 4 +-- .../projects/protected_tags/_index.html.haml | 2 +- .../protected_tags/_protected_tag.html.haml | 21 +++++++++++++ .../protected_tags/_tags_list.html.haml | 30 +++++++++++++++---- .../protected_tags/_update_protected_tag.haml | 5 ++++ 6 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 app/views/projects/protected_tags/_protected_tag.html.haml create mode 100644 app/views/projects/protected_tags/_update_protected_tag.haml diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js index 5a0356f502c..ccc4c81fa18 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js +++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js @@ -53,9 +53,9 @@ class ProtectedTagDropdown { getProtectedTags(term, callback) { if (this.selectedTag) { - callback(gon.open_branches.concat(this.selectedTag)); + callback(gon.open_tags.concat(this.selectedTag)); } else { - callback(gon.open_branches); + callback(gon.open_tags); } } diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml index 110c24ac9e4..9fdebf2c982 100644 --- a/app/views/projects/protected_tags/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml @@ -1,11 +1,11 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'new_protected_tag' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new_protected_tag' } do |f| .panel.panel-default .panel-heading %h3.panel-title Protect a tag .panel-body .form-horizontal - = form_errors(@protected_branch) + = form_errors(@protected_tag) .form-group = f.label :name, class: 'col-md-2 text-right' do Tag: diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml index 0965bf75eae..591d64ae7de 100644 --- a/app/views/projects/protected_tags/_index.html.haml +++ b/app/views/projects/protected_tags/_index.html.haml @@ -6,7 +6,7 @@ %h4.prepend-top-0 Protected tags %p.prepend-top-20 - By default, Protected branches are designed to: + By default, Protected tags are designed to: %ul %li Prevent tag pushes from everybody except Masters %li Prevent anyone from force pushing to the tag diff --git a/app/views/projects/protected_tags/_protected_tag.html.haml b/app/views/projects/protected_tags/_protected_tag.html.haml new file mode 100644 index 00000000000..26bd3a1f5ed --- /dev/null +++ b/app/views/projects/protected_tags/_protected_tag.html.haml @@ -0,0 +1,21 @@ +%tr.js-protected-tag-edit-form{ data: { url: namespace_project_protected_tag_path(@project.namespace, @project, protected_tag) } } + %td + = protected_tag.name + - if @project.root_ref?(protected_tag.name) + %span.label.label-info.prepend-left-5 default + %td + - if protected_tag.wildcard? + - matching_tags = protected_tag.matching(repository.tags) + = link_to pluralize(matching_tags.count, "matching tag"), namespace_project_protected_tag_path(@project.namespace, @project, protected_tag) + - else + - if commit = protected_tag.commit + = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') + = time_ago_with_tooltip(commit.committed_date) + - else + (tag was removed from repository) + + = render partial: 'projects/protected_tags/update_protected_tag', locals: { protected_tag: protected_tag } + + - if can_admin_project + %td + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning' diff --git a/app/views/projects/protected_tags/_tags_list.html.haml b/app/views/projects/protected_tags/_tags_list.html.haml index e7ce90393e9..6dcd356e6f1 100644 --- a/app/views/projects/protected_tags/_tags_list.html.haml +++ b/app/views/projects/protected_tags/_tags_list.html.haml @@ -1,6 +1,26 @@ .panel.panel-default.protected-tags-list - .panel-heading - %h3.panel-title - Protected tag (0) - %p.settings-message.text-center - There are currently no protected tags, protect a tag with the form above. + - if @protected_tags.empty? + .panel-heading + %h3.panel-title + Protected tag (#{@protected_tags.size}) + %p.settings-message.text-center + There are currently no protected tags, protect a tag with the form above. + - else + - can_admin_project = can?(current_user, :admin_project, @project) + + %table.table.table-bordered + %colgroup + %col{ width: "25%" } + %col{ width: "55%" } + %col{ width: "20%" } + %thead + %tr + %th Protected tag (#{@protected_tags.size}) + %th Last commit + %th Allowed to push + - if can_admin_project + %th + %tbody + = render partial: 'projects/protected_tags/protected_tag', collection: @protected_tags, locals: { can_admin_project: can_admin_project} + + = paginate @protected_tags, theme: 'gitlab' diff --git a/app/views/projects/protected_tags/_update_protected_tag.haml b/app/views/projects/protected_tags/_update_protected_tag.haml new file mode 100644 index 00000000000..729a784a559 --- /dev/null +++ b/app/views/projects/protected_tags/_update_protected_tag.haml @@ -0,0 +1,5 @@ +%td + = hidden_field_tag "allowed_to_push_#{protected_tag.id}", protected_tag.push_access_levels.first.access_level + / = dropdown_tag( (protected_tag.push_access_levels.first.humanize || 'Select') , + / options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container', + / data: { field_name: "allowed_to_push_#{protected_tag.id}", access_level_id: protected_tag.push_access_levels.first.id }}) From b5fce1d5ac87546e8f31fb0ef6f6c4d514670198 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Fri, 31 Mar 2017 17:56:26 +0100 Subject: [PATCH 13/54] =?UTF-8?q?Removed=20unnecessary=20table=20=E2=80=98?= =?UTF-8?q?protected=5Ftag=5Fmerge=5Faccess=5Flevels=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed timestamps on protected_tags --- .../20170309173138_create_protected_tags.rb | 23 +++++-------------- db/schema.rb | 22 ++++++++---------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/db/migrate/20170309173138_create_protected_tags.rb b/db/migrate/20170309173138_create_protected_tags.rb index c69ef970410..00794529143 100644 --- a/db/migrate/20170309173138_create_protected_tags.rb +++ b/db/migrate/20170309173138_create_protected_tags.rb @@ -4,36 +4,25 @@ class CreateProtectedTags < ActiveRecord::Migration # Set this constant to true if this migration requires downtime. DOWNTIME = false - GitlabAccessMaster = 40 + GITLAB_ACCESS_MASTER = 40 def change create_table :protected_tags do |t| t.integer :project_id, null: false t.string :name, null: false - t.string :timestamps #TODO: `null: false`? Missing from protected_branches + t.timestamps null: false end add_index :protected_tags, :project_id - create_table :protected_tag_merge_access_levels do |t| - t.references :protected_tag, index: { name: "index_protected_tag_merge_access" }, foreign_key: true, null: false - - t.integer :access_level, default: GitlabAccessMaster, null: true #TODO: was false, check schema - t.integer :group_id #TODO: check why group/user id missing from CE - t.integer :user_id - t.timestamps null: false - end - create_table :protected_tag_push_access_levels do |t| t.references :protected_tag, index: { name: "index_protected_tag_push_access" }, foreign_key: true, null: false - t.integer :access_level, default: GitlabAccessMaster, null: true #TODO: was false, check schema - t.integer :group_id - t.integer :user_id + t.integer :access_level, default: GITLAB_ACCESS_MASTER, null: true + t.references :user, foreign_key: true, index: true + t.integer :group_id#TODO: Should this have an index? Doesn't appear in brances #, index: true t.timestamps null: false end - #TODO: These had rubocop set to disable Migration/AddConcurrentForeignKey - # add_foreign_key :protected_tag_merge_access_levels, :namespaces, column: :group_id - # add_foreign_key :protected_tag_push_access_levels, :namespaces, column: :group_id + add_foreign_key :protected_tag_push_access_levels, :namespaces, column: :group_id # rubocop: disable Migration/AddConcurrentForeignKey end end diff --git a/db/schema.rb b/db/schema.rb index 05b28b6a63b..650b18bb013 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170317203554) do +ActiveRecord::Schema.define(version: 20170315194013) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -963,28 +963,23 @@ ActiveRecord::Schema.define(version: 20170317203554) do add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree - create_table "protected_tag_merge_access_levels", force: :cascade do |t| - t.integer "protected_tag_id", null: false - t.integer "access_level", default: 40, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - add_index "protected_tag_merge_access_levels", ["protected_tag_id"], name: "index_protected_tag_merge_access", using: :btree - create_table "protected_tag_push_access_levels", force: :cascade do |t| t.integer "protected_tag_id", null: false - t.integer "access_level", default: 40, null: false + t.integer "access_level", default: 40 + t.integer "user_id" + t.integer "group_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "protected_tag_push_access_levels", ["protected_tag_id"], name: "index_protected_tag_push_access", using: :btree + add_index "protected_tag_push_access_levels", ["user_id"], name: "index_protected_tag_push_access_levels_on_user_id", using: :btree create_table "protected_tags", force: :cascade do |t| t.integer "project_id", null: false t.string "name", null: false - t.string "timestamps" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree @@ -1331,8 +1326,9 @@ ActiveRecord::Schema.define(version: 20170317203554) do add_foreign_key "project_statistics", "projects", on_delete: :cascade add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_push_access_levels", "protected_branches" - add_foreign_key "protected_tag_merge_access_levels", "protected_tags" + add_foreign_key "protected_tag_push_access_levels", "namespaces", column: "group_id" add_foreign_key "protected_tag_push_access_levels", "protected_tags" + add_foreign_key "protected_tag_push_access_levels", "users" add_foreign_key "subscriptions", "projects", on_delete: :cascade add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade From e3fbcd0093b07bbc084061992bb8ae6bd4343d52 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Fri, 31 Mar 2017 17:57:29 +0100 Subject: [PATCH 14/54] Protected Tags enforced over git --- app/models/project.rb | 8 ++++ lib/gitlab/checks/change_access.rb | 37 ++++++++++++++--- lib/gitlab/user_access.rb | 16 ++++++++ spec/lib/gitlab/checks/change_access_spec.rb | 42 +++++++++++++++++++- 4 files changed, 97 insertions(+), 6 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index c04effc53bd..7681da36335 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -893,6 +893,7 @@ class Project < ActiveRecord::Base end # Check if current branch name is marked as protected in the system + #TODO: Move elsewhere def protected_branch?(branch_name) return true if empty_repo? && default_branch_protected? @@ -900,6 +901,13 @@ class Project < ActiveRecord::Base ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present? end + #TODO: Move elsewhere + def protected_tag?(tag_name) + #TODO: Check if memoization necessary, find way to have it work elsewhere + @protected_tags ||= self.protected_tags.to_a + ProtectedTag.matching(tag_name, protected_tags: @protected_tags).present? + end + def user_can_push_to_empty_repo?(user) !default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER end diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index c85f79127bc..0d8f114cc59 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -10,6 +10,7 @@ module Gitlab ) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @branch_name = Gitlab::Git.branch_name(@ref) + @tag_name = Gitlab::Git.tag_name(@ref) @user_access = user_access @project = project @env = env @@ -36,7 +37,7 @@ module Gitlab if forced_push? return "You are not allowed to force push code to a protected branch on this project." - elsif Gitlab::Git.blank_ref?(@newrev) + elsif blank_ref? return "You are not allowed to delete protected branches from this project." end @@ -58,11 +59,33 @@ module Gitlab def tag_checks return if skip_authorization - tag_ref = Gitlab::Git.tag_name(@ref) + return unless @tag_name - if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project) + if tag_exists? && user_access.cannot_do_action?(:admin_project) "You are not allowed to change existing tags on this project." end + + protected_tag_checks + end + + def protected_tag_checks + return unless tag_protected? + + if forced_push? + return "You are not allowed to force push protected tags." #TODO: Wording, 'not allowed to update proteted tags'? + end + + if Gitlab::Git.blank_ref?(@newrev) + return "You are not allowed to delete protected tags." #TODO: Wording, do these need to mention 'you' if the rule applies to everyone + end + + if !user_access.can_push_tag?(@tag_name) + return "You are not allowed to create protected tags on this project." #TODO: Wording, it is a specific tag which you don't have access too, not all protected tags which might have different levels + end + end + + def tag_protected? + project.protected_tag?(@tag_name) end def push_checks @@ -75,14 +98,18 @@ module Gitlab private - def protected_tag?(tag_name) - project.repository.tag_exists?(tag_name) + def tag_exists? + project.repository.tag_exists?(@tag_name) end def forced_push? Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env) end + def blank_ref? + Gitlab::Git.blank_ref?(@newrev) + end + def matching_merge_request? Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match? end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index f260c0c535f..921159d91ef 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -28,6 +28,22 @@ module Gitlab true end + #TODO: Test this + #TODO move most to ProtectedTag::AccessChecker. Or maybe UserAccess::Protections::Tag + #TODO: then consider removing method, if it turns out can_access_git? and can?(:push_code are checked in change_access + def can_push_tag?(ref) + return false unless can_access_git? + + if project.protected_tag?(ref) + access_levels = project.protected_tags.matching(ref).map(&:push_access_levels).flatten + has_access = access_levels.any? { |access_level| access_level.check_access(user) } + + has_access + else + user.can?(:push_code, project) + end + end + def can_push_to_branch?(ref) return false unless can_access_git? diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index e22f88b7a32..cb2989c32df 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ).exec end - before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) } + before { project.add_developer(user) } context 'without failed checks' do it "doesn't return any error" do @@ -50,11 +50,51 @@ describe Gitlab::Checks::ChangeAccess, lib: true do end it 'returns an error if the user is not allowed to update tags' do + allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false) expect(subject.status).to be(false) expect(subject.message).to eq('You are not allowed to change existing tags on this project.') end + + context 'with protected tag' do + let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') } + + context 'deletion' do + let(:changes) do + { + oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c', + newrev: '0000000000000000000000000000000000000000', + ref: 'refs/tags/v1.0.0' + } + end + + it 'is prevented' do + expect(subject.status).to be(false) + expect(subject.message).to include('delete protected tags') + end + end + + it 'prevents force push' do + expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) + + expect(subject.status).to be(false) + expect(subject.message).to include('force push protected tags') + end + + it 'prevents creation below access level' do + expect(subject.status).to be(false) + expect(subject.message).to include('allowed to') + end + + context 'when user has access' do + let!(:protected_tag) { create(:protected_tag, :developers_can_push, project: project, name: 'v*') } + + it 'allows tag creation' do + expect(subject.status).to be(true) + end + end + end end context 'protected branches check' do From ab46353fd9c0c76c137bf828788ecbc34d0fe99a Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Fri, 31 Mar 2017 19:29:25 +0100 Subject: [PATCH 15/54] Added ProtectedTags#show page Uncommented protected tags feature specs copied from protected branches --- .../protected_tags/_matching_tag.html.haml | 9 ++ .../projects/protected_tags/show.html.haml | 25 +++ .../protected_tags/access_control_ce_spec.rb | 105 ++++--------- spec/features/protected_tags_spec.rb | 146 +++++++++--------- 4 files changed, 140 insertions(+), 145 deletions(-) create mode 100644 app/views/projects/protected_tags/_matching_tag.html.haml create mode 100644 app/views/projects/protected_tags/show.html.haml diff --git a/app/views/projects/protected_tags/_matching_tag.html.haml b/app/views/projects/protected_tags/_matching_tag.html.haml new file mode 100644 index 00000000000..97e5cd6f9d2 --- /dev/null +++ b/app/views/projects/protected_tags/_matching_tag.html.haml @@ -0,0 +1,9 @@ +%tr + %td + = link_to matching_tag.name, namespace_project_tree_path(@project.namespace, @project, matching_tag.name) + - if @project.root_ref?(matching_tag.name) + %span.label.label-info.prepend-left-5 default + %td + - commit = @project.commit(matching_tag.name) + = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') + = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/protected_tags/show.html.haml b/app/views/projects/protected_tags/show.html.haml new file mode 100644 index 00000000000..185807a7e8d --- /dev/null +++ b/app/views/projects/protected_tags/show.html.haml @@ -0,0 +1,25 @@ +- page_title @protected_tag.name, "Protected Tags" + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + = @protected_tag.name + + .col-lg-9 + %h5 Matching Tags + - if @matching_tags.present? + .table-responsive + %table.table.protected-tags-list + %colgroup + %col{ width: "30%" } + %col{ width: "30%" } + %thead + %tr + %th Tag + %th Last commit + %tbody + - @matching_tags.each do |matching_tag| + = render partial: "matching_tag", object: matching_tag + - else + %p.settings-message.text-center + Couldn't find any matching tags. diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb index 545d3bca74d..d7152926629 100644 --- a/spec/features/protected_tags/access_control_ce_spec.rb +++ b/spec/features/protected_tags/access_control_ce_spec.rb @@ -1,79 +1,40 @@ -# RSpec.shared_examples "protected tags > access control > CE" do -# ProtectedTag::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| -# it "allows creating protected tags that #{access_type_name} can push to" do -# visit namespace_project_protected_tags_path(project.namespace, project) -# set_protected_tag_name('master') -# within('.new_protected_tag') do -# allowed_to_push_button = find(".js-allowed-to-push") +RSpec.shared_examples "protected tags > access control > CE" do + ProtectedTag::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected tags that #{access_type_name} can push to" do + visit namespace_project_protected_tags_path(project.namespace, project) + set_protected_tag_name('master') + within('.new_protected_tag') do + allowed_to_push_button = find(".js-allowed-to-push") -# unless allowed_to_push_button.text == access_type_name -# allowed_to_push_button.click -# within(".dropdown.open .dropdown-menu") { click_on access_type_name } -# end -# end -# click_on "Protect" + unless allowed_to_push_button.text == access_type_name + allowed_to_push_button.click + within(".dropdown.open .dropdown-menu") { click_on access_type_name } + end + end + click_on "Protect" -# expect(ProtectedTag.count).to eq(1) -# expect(ProtectedTag.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) -# end + expect(ProtectedTag.count).to eq(1) + expect(ProtectedTag.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) + end -# it "allows updating protected tags so that #{access_type_name} can push to them" do -# visit namespace_project_protected_tags_path(project.namespace, project) -# set_protected_tag_name('master') -# click_on "Protect" + it "allows updating protected tags so that #{access_type_name} can push to them" do + visit namespace_project_protected_tags_path(project.namespace, project) + set_protected_tag_name('master') + click_on "Protect" -# expect(ProtectedTag.count).to eq(1) + expect(ProtectedTag.count).to eq(1) -# within(".protected-tags-list") do -# find(".js-allowed-to-push").click + within(".protected-tags-list") do + find(".js-allowed-to-push").click -# within('.js-allowed-to-push-container') do -# expect(first("li")).to have_content("Roles") -# click_on access_type_name -# end -# end + within('.js-allowed-to-push-container') do + expect(first("li")).to have_content("Roles") + click_on access_type_name + end + end -# wait_for_ajax -# expect(ProtectedTag.last.push_access_levels.map(&:access_level)).to include(access_type_id) -# end -# end - -# ProtectedTag::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| -# it "allows creating protected tags that #{access_type_name} can merge to" do -# visit namespace_project_protected_tags_path(project.namespace, project) -# set_protected_tag_name('master') -# within('.new_protected_tag') do -# allowed_to_merge_button = find(".js-allowed-to-merge") - -# unless allowed_to_merge_button.text == access_type_name -# allowed_to_merge_button.click -# within(".dropdown.open .dropdown-menu") { click_on access_type_name } -# end -# end -# click_on "Protect" - -# expect(ProtectedTag.count).to eq(1) -# expect(ProtectedTag.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) -# end - -# it "allows updating protected tags so that #{access_type_name} can merge to them" do -# visit namespace_project_protected_tags_path(project.namespace, project) -# set_protected_tag_name('master') -# click_on "Protect" - -# expect(ProtectedTag.count).to eq(1) - -# within(".protected-tags-list") do -# find(".js-allowed-to-merge").click - -# within('.js-allowed-to-merge-container') do -# expect(first("li")).to have_content("Roles") -# click_on access_type_name -# end -# end - -# wait_for_ajax -# expect(ProtectedTag.last.merge_access_levels.map(&:access_level)).to include(access_type_id) -# end -# end -# end + wait_for_ajax + expect(ProtectedTag.last.push_access_levels.map(&:access_level)).to include(access_type_id) + end + end +end diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb index 546d6037ef7..09e8c850de3 100644 --- a/spec/features/protected_tags_spec.rb +++ b/spec/features/protected_tags_spec.rb @@ -1,94 +1,94 @@ -# require 'spec_helper' -# Dir["./spec/features/protected_tags/*.rb"].sort.each { |f| require f } +require 'spec_helper' +Dir["./spec/features/protected_tags/*.rb"].sort.each { |f| require f } -# feature 'Projected Tags', feature: true, js: true do -# include WaitForAjax +feature 'Projected Tags', feature: true, js: true do + include WaitForAjax -# let(:user) { create(:user, :admin) } -# let(:project) { create(:project) } + let(:user) { create(:user, :admin) } + let(:project) { create(:project) } -# before { login_as(user) } + before { login_as(user) } -# def set_protected_tag_name(tag_name) -# find(".js-protected-tag-select").click -# find(".dropdown-input-field").set(tag_name) -# click_on("Create wildcard #{tag_name}") -# end + def set_protected_tag_name(tag_name) + find(".js-protected-tag-select").click + find(".dropdown-input-field").set(tag_name) + click_on("Create wildcard #{tag_name}") + end -# describe "explicit protected tags" do -# it "allows creating explicit protected tags" do -# visit namespace_project_protected_tags_path(project.namespace, project) -# set_protected_tag_name('some-tag') -# click_on "Protect" + describe "explicit protected tags" do + it "allows creating explicit protected tags" do + visit namespace_project_protected_tags_path(project.namespace, project) + set_protected_tag_name('some-tag') + click_on "Protect" -# within(".protected-tags-list") { expect(page).to have_content('some-tag') } -# expect(ProtectedTag.count).to eq(1) -# expect(ProtectedTag.last.name).to eq('some-tag') -# end + within(".protected-tags-list") { expect(page).to have_content('some-tag') } + expect(ProtectedTag.count).to eq(1) + expect(ProtectedTag.last.name).to eq('some-tag') + end -# it "displays the last commit on the matching tag if it exists" do -# commit = create(:commit, project: project) -# project.repository.add_tag(user, 'some-tag', commit.id) + it "displays the last commit on the matching tag if it exists" do + commit = create(:commit, project: project) + project.repository.add_tag(user, 'some-tag', commit.id) -# visit namespace_project_protected_tags_path(project.namespace, project) -# set_protected_tag_name('some-tag') -# click_on "Protect" + visit namespace_project_protected_tags_path(project.namespace, project) + set_protected_tag_name('some-tag') + click_on "Protect" -# within(".protected-tags-list") { expect(page).to have_content(commit.id[0..7]) } -# end + within(".protected-tags-list") { expect(page).to have_content(commit.id[0..7]) } + end -# it "displays an error message if the named tag does not exist" do -# visit namespace_project_protected_tags_path(project.namespace, project) -# set_protected_tag_name('some-tag') -# click_on "Protect" + it "displays an error message if the named tag does not exist" do + visit namespace_project_protected_tags_path(project.namespace, project) + set_protected_tag_name('some-tag') + click_on "Protect" -# within(".protected-tags-list") { expect(page).to have_content('tag was removed') } -# end -# end + within(".protected-tags-list") { expect(page).to have_content('tag was removed') } + end + end -# describe "wildcard protected tags" do -# it "allows creating protected tags with a wildcard" do -# visit namespace_project_protected_tags_path(project.namespace, project) -# set_protected_tag_name('*-stable') -# click_on "Protect" + describe "wildcard protected tags" do + it "allows creating protected tags with a wildcard" do + visit namespace_project_protected_tags_path(project.namespace, project) + set_protected_tag_name('*-stable') + click_on "Protect" -# within(".protected-tags-list") { expect(page).to have_content('*-stable') } -# expect(ProtectedTag.count).to eq(1) -# expect(ProtectedTag.last.name).to eq('*-stable') -# end + within(".protected-tags-list") { expect(page).to have_content('*-stable') } + expect(ProtectedTag.count).to eq(1) + expect(ProtectedTag.last.name).to eq('*-stable') + end -# it "displays the number of matching tags" do -# project.repository.add_tag(user, 'production-stable', 'master') -# project.repository.add_tag(user, 'staging-stable', 'master') + it "displays the number of matching tags" do + project.repository.add_tag(user, 'production-stable', 'master') + project.repository.add_tag(user, 'staging-stable', 'master') -# visit namespace_project_protected_tags_path(project.namespace, project) -# set_protected_tag_name('*-stable') -# click_on "Protect" + visit namespace_project_protected_tags_path(project.namespace, project) + set_protected_tag_name('*-stable') + click_on "Protect" -# within(".protected-tags-list") { expect(page).to have_content("2 matching tags") } -# end + within(".protected-tags-list") { expect(page).to have_content("2 matching tags") } + end -# it "displays all the tags matching the wildcard" do -# project.repository.add_tag(user, 'production-stable', 'master') -# project.repository.add_tag(user, 'staging-stable', 'master') -# project.repository.add_tag(user, 'development', 'master') + it "displays all the tags matching the wildcard" do + project.repository.add_tag(user, 'production-stable', 'master') + project.repository.add_tag(user, 'staging-stable', 'master') + project.repository.add_tag(user, 'development', 'master') -# visit namespace_project_protected_tags_path(project.namespace, project) -# set_protected_tag_name('*-stable') -# click_on "Protect" + visit namespace_project_protected_tags_path(project.namespace, project) + set_protected_tag_name('*-stable') + click_on "Protect" -# visit namespace_project_protected_tags_path(project.namespace, project) -# click_on "2 matching tags" + visit namespace_project_protected_tags_path(project.namespace, project) + click_on "2 matching tags" -# within(".protected-tags-list") do -# expect(page).to have_content("production-stable") -# expect(page).to have_content("staging-stable") -# expect(page).not_to have_content("development") -# end -# end -# end + within(".protected-tags-list") do + expect(page).to have_content("production-stable") + expect(page).to have_content("staging-stable") + expect(page).not_to have_content("development") + end + end + end -# describe "access control" do -# include_examples "protected tags > access control > CE" -# end -# end + describe "access control" do + include_examples "protected tags > access control > CE" + end +end From 553cf9ea54ccb0736a8b44e2f3d047d0860aa71e Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Fri, 31 Mar 2017 19:30:33 +0100 Subject: [PATCH 16/54] =?UTF-8?q?Added=20=E2=80=98protected=E2=80=99=20lab?= =?UTF-8?q?el=20and=20disabled=20delete=20button=20for=20tags=20index/show?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/tags_helper.rb | 4 ++++ app/views/projects/tags/_tag.html.haml | 7 ++++++- app/views/projects/tags/show.html.haml | 5 ++++- lib/gitlab/checks/change_access.rb | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index c0ec1634cdb..6672e3da348 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -21,4 +21,8 @@ module TagsHelper html.html_safe end + + def protected_tag?(project, tag) + project.protected_tag?(tag.name) + end end diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index dffe908e85a..451e011a4b8 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -6,6 +6,11 @@ %span.item-title = icon('tag') = tag.name + + - if protected_tag?(@project, tag) + %span.label.label-success + protected + - if tag.message.present?   = strip_gpg_signature(tag.message) @@ -30,5 +35,5 @@ = icon("pencil") - if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index fad3c5c2173..1c4135c8a54 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -7,6 +7,9 @@ .nav-text .title %span.item-title= @tag.name + - if protected_tag?(@project, @tag) + %span.label.label-success + protected - if @commit = render 'projects/branches/commit', commit: @commit, project: @project - else @@ -24,7 +27,7 @@ = render 'projects/buttons/download', project: @project, ref: @tag.name - if can?(current_user, :admin_project, @project) .btn-container.controls-item-full - = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do + = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do %i.fa.fa-trash-o - if @tag.message.present? diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 0d8f114cc59..540d95f2d1f 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -80,7 +80,7 @@ module Gitlab end if !user_access.can_push_tag?(@tag_name) - return "You are not allowed to create protected tags on this project." #TODO: Wording, it is a specific tag which you don't have access too, not all protected tags which might have different levels + return "You are not allowed to create this tag as it is protected." end end From 85e1fa8e21c73746bb6f6270a658453a230bb9a2 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 3 Apr 2017 11:40:21 +0530 Subject: [PATCH 17/54] ProtectedTagEdit class for edit protected tags --- .../protected_tags/protected_tag_edit.js | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/assets/javascripts/protected_tags/protected_tag_edit.js diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js new file mode 100644 index 00000000000..b93e903621e --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -0,0 +1,54 @@ +/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */ +/* global Flash */ + +(global => { + global.gl = global.gl || {}; + + gl.ProtectedTagEdit = class { + constructor(options) { + this.$wrap = options.$wrap; + this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + + this.buildDropdowns(); + } + + buildDropdowns() { + // Allowed to push dropdown + new gl.ProtectedTagAccessDropdown({ + $dropdown: this.$allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelect.bind(this) + }); + } + + onSelect() { + const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + + // Do not update if one dropdown has not selected any option + if (!$allowedToPushInput.length) return; + + this.$allowedToPushDropdown.disable(); + + $.ajax({ + type: 'POST', + url: this.$wrap.data('url'), + dataType: 'json', + data: { + _method: 'PATCH', + protected_tag: { + push_access_levels_attributes: [{ + id: this.$allowedToPushDropdown.data('access-level-id'), + access_level: $allowedToPushInput.val() + }] + } + }, + error() { + $.scrollTo(0); + new Flash('Failed to update tag!'); + } + }).always(() => { + this.$allowedToPushDropdown.enable(); + }); + } + }; +})(window); From 551dea7efe11c55799e4a0cacd9c75988ef9583b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 3 Apr 2017 11:40:43 +0530 Subject: [PATCH 18/54] Protected Tags List initializer --- .../protected_tags/protected_tag_edit_list.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/assets/javascripts/protected_tags/protected_tag_edit_list.js diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js new file mode 100644 index 00000000000..ba40c227ef4 --- /dev/null +++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js @@ -0,0 +1,18 @@ +/* eslint-disable arrow-parens, no-param-reassign, no-new, comma-dangle */ + +(global => { + global.gl = global.gl || {}; + + gl.ProtectedTagEditList = class { + constructor() { + this.$wrap = $('.protected-tags-list'); + + // Build edit forms + this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => { + new gl.ProtectedTagEdit({ + $wrap: $(el) + }); + }); + } + }; +})(window); From bbb09feaa1b535e10b20790d1857385bd34ae5f6 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 3 Apr 2017 11:41:05 +0530 Subject: [PATCH 19/54] Export Protected Tags Editing classes --- app/assets/javascripts/protected_tags/protected_tags_bundle.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/protected_tags/protected_tags_bundle.js b/app/assets/javascripts/protected_tags/protected_tags_bundle.js index d84d2e1ef70..889a8053e6f 100644 --- a/app/assets/javascripts/protected_tags/protected_tags_bundle.js +++ b/app/assets/javascripts/protected_tags/protected_tags_bundle.js @@ -1,3 +1,5 @@ require('./protected_tag_access_dropdown'); require('./protected_tag_create'); require('./protected_tag_dropdown'); +require('./protected_tag_edit'); +require('./protected_tag_edit_list'); From 8d232b2f2b1b4d33741925a0baf2eaf74f52008e Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 3 Apr 2017 11:41:28 +0530 Subject: [PATCH 20/54] Initialize Protected Tags Edit functionality --- app/assets/javascripts/dispatcher.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 6234092bdc2..d384927cc5b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -318,9 +318,12 @@ const UserCallout = require('./user_callout'); new Search(); break; case 'projects:repository:show': + // Initialize Protected Branch Settings new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); + // Initialize Protected Tag Settings new gl.ProtectedTagCreate(); + new gl.ProtectedTagEditList(); break; case 'projects:ci_cd:show': new gl.ProjectVariables(); From 48150f273d8b7cc5b042eb0a9f88b0a7a96431f4 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 3 Apr 2017 11:42:05 +0530 Subject: [PATCH 21/54] Render Push levels dropdown --- .../projects/protected_tags/_update_protected_tag.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/protected_tags/_update_protected_tag.haml b/app/views/projects/protected_tags/_update_protected_tag.haml index 729a784a559..802a5ef3b98 100644 --- a/app/views/projects/protected_tags/_update_protected_tag.haml +++ b/app/views/projects/protected_tags/_update_protected_tag.haml @@ -1,5 +1,5 @@ %td = hidden_field_tag "allowed_to_push_#{protected_tag.id}", protected_tag.push_access_levels.first.access_level - / = dropdown_tag( (protected_tag.push_access_levels.first.humanize || 'Select') , - / options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container', - / data: { field_name: "allowed_to_push_#{protected_tag.id}", access_level_id: protected_tag.push_access_levels.first.id }}) + = dropdown_tag( (protected_tag.push_access_levels.first.humanize || 'Select') , + options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container', + data: { field_name: "allowed_to_push_#{protected_tag.id}", access_level_id: protected_tag.push_access_levels.first.id }}) From df54560f5f5d94fd9117c21d03d946d07d28f6fa Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 3 Apr 2017 11:42:37 +0530 Subject: [PATCH 22/54] Increase dropdown width within Tags list --- app/assets/stylesheets/pages/projects.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index f7ef9275560..7c93ee8af18 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -778,6 +778,12 @@ pre.light-well { } } +.protected-tags-list { + .dropdown-menu-toggle { + width: 300px; + } +} + .custom-notifications-form { .is-loading { .custom-notification-event-loading { From a7c71c7f292c9cdf892f7d33dfb52d7e16af28e6 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 3 Apr 2017 11:42:55 +0530 Subject: [PATCH 23/54] Update column widths --- app/views/projects/protected_tags/_tags_list.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/protected_tags/_tags_list.html.haml b/app/views/projects/protected_tags/_tags_list.html.haml index 6dcd356e6f1..6f63971923d 100644 --- a/app/views/projects/protected_tags/_tags_list.html.haml +++ b/app/views/projects/protected_tags/_tags_list.html.haml @@ -11,8 +11,8 @@ %table.table.table-bordered %colgroup %col{ width: "25%" } - %col{ width: "55%" } - %col{ width: "20%" } + %col{ width: "25%" } + %col{ width: "50%" } %thead %tr %th Protected tag (#{@protected_tags.size}) From 65f3d5062f081d8f8ebf727a3408650d90ec9711 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 3 Apr 2017 15:17:24 +0100 Subject: [PATCH 24/54] Extract ProtectedRef Concern --- app/models/concerns/protected_ref.rb | 47 +++++++++++++++++++ app/models/project.rb | 4 +- app/models/protected_branch.rb | 31 +----------- app/models/protected_tag.rb | 31 +----------- lib/api/entities.rb | 4 +- lib/gitlab/user_access.rb | 11 ++--- spec/models/protected_branch_spec.rb | 8 ++-- .../protected_tags/create_service_spec.rb | 2 - 8 files changed, 60 insertions(+), 78 deletions(-) create mode 100644 app/models/concerns/protected_ref.rb diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb new file mode 100644 index 00000000000..77d7b597534 --- /dev/null +++ b/app/models/concerns/protected_ref.rb @@ -0,0 +1,47 @@ +module ProtectedRef + extend ActiveSupport::Concern + + included do + belongs_to :project + validates :name, presence: true + validates :project, presence: true + + def self.matching_refs_accesible_to(ref, user, action: :push) + access_levels_for_ref(ref, action).any? do |access_level| + access_level.check_access(user) + end + end + + def self.access_levels_for_ref(ref, action: :push) + self.matching(ref).map(&:"@#{action}_access_levels").flatten + end + + private + + def self.matching(ref_name, protected_refs: nil) + ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs) + end + end + + def commit + project.commit(self.name) + end + + def matching(refs) + ref_matcher.matching(refs) + end + + def matches?(refs) + ref_matcher.matches?(refs) + end + + def wildcard? + ref_matcher.wildcard? + end + + private + + def ref_matcher + @ref_matcher ||= ProtectedRefMatcher.new(self) + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 7681da36335..970de324a5b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -898,14 +898,14 @@ class Project < ActiveRecord::Base return true if empty_repo? && default_branch_protected? @protected_branches ||= self.protected_branches.to_a - ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present? + ProtectedBranch.matching(branch_name, protected_refs: @protected_branches).present? end #TODO: Move elsewhere def protected_tag?(tag_name) #TODO: Check if memoization necessary, find way to have it work elsewhere @protected_tags ||= self.protected_tags.to_a - ProtectedTag.matching(tag_name, protected_tags: @protected_tags).present? + ProtectedTag.matching(tag_name, protected_refs: @protected_tags).present? end def user_can_push_to_empty_repo?(user) diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 7681d5b5112..a0dbcf80c3d 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -1,9 +1,6 @@ class ProtectedBranch < ActiveRecord::Base include Gitlab::ShellAdapter - - belongs_to :project - validates :name, presence: true - validates :project, presence: true + include ProtectedRef has_many :merge_access_levels, dependent: :destroy has_many :push_access_levels, dependent: :destroy @@ -13,30 +10,4 @@ class ProtectedBranch < ActiveRecord::Base accepts_nested_attributes_for :push_access_levels accepts_nested_attributes_for :merge_access_levels - - def commit - project.commit(self.name) - end - - def self.matching(branch_name, protected_branches: nil) - ProtectedRefMatcher.matching(ProtectedBranch, branch_name, protected_refs: protected_branches) - end - - def matching(branches) - ref_matcher.matching(branches) - end - - def matches?(branch_name) - ref_matcher.matches?(branch_name) - end - - def wildcard? - ref_matcher.wildcard? - end - - private - - def ref_matcher - @ref_matcher ||= ProtectedRefMatcher.new(self) - end end diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb index d307549aa49..301fe2092e9 100644 --- a/app/models/protected_tag.rb +++ b/app/models/protected_tag.rb @@ -1,39 +1,10 @@ class ProtectedTag < ActiveRecord::Base include Gitlab::ShellAdapter - - belongs_to :project - validates :name, presence: true - validates :project, presence: true + include ProtectedRef has_many :push_access_levels, dependent: :destroy validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." } accepts_nested_attributes_for :push_access_levels - - def commit - project.commit(self.name) - end - - def self.matching(tag_name, protected_tags: nil) - ProtectedRefMatcher.matching(ProtectedTag, tag_name, protected_refs: protected_tags) - end - - def matching(branches) - ref_matcher.matching(branches) - end - - def matches?(tag_name) - ref_matcher.matches?(tag_name) - end - - def wildcard? - ref_matcher.wildcard? - end - - private - - def ref_matcher - @ref_matcher ||= ProtectedRefMatcher.new(self) - end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 5954aea8041..e000e3e33d0 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -189,13 +189,13 @@ module API expose :developers_can_push do |repo_branch, options| project = options[:project] - access_levels = project.protected_branches.matching(repo_branch.name).map(&:push_access_levels).flatten + access_levels = project.protected_branches.access_levels_for_ref(repo_branch.name, :push) access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER } end expose :developers_can_merge do |repo_branch, options| project = options[:project] - access_levels = project.protected_branches.matching(repo_branch.name).map(&:merge_access_levels).flatten + access_levels = project.protected_branches.access_levels_for_ref(repo_branch.name, :merge) access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER } end end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 921159d91ef..5a5a4ebd08b 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -35,10 +35,7 @@ module Gitlab return false unless can_access_git? if project.protected_tag?(ref) - access_levels = project.protected_tags.matching(ref).map(&:push_access_levels).flatten - has_access = access_levels.any? { |access_level| access_level.check_access(user) } - - has_access + project.protected_tags.matching_refs_accesible_to(ref, user) else user.can?(:push_code, project) end @@ -50,8 +47,7 @@ module Gitlab if project.protected_branch?(ref) return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) - access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten - has_access = access_levels.any? { |access_level| access_level.check_access(user) } + has_access = project.protected_branches.matching_refs_accesible_to(ref, user, action: :push) has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref) else @@ -63,8 +59,7 @@ module Gitlab return false unless can_access_git? if project.protected_branch?(ref) - access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten - access_levels.any? { |access_level| access_level.check_access(user) } + project.protected_branches.matching_refs_accesible_to(ref, user, action: :merge) else user.can?(:push_code, project) end diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index 8bf0d24a128..1c02f8bfc3f 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -113,8 +113,8 @@ describe ProtectedBranch, models: true do staging = build(:protected_branch, name: "staging") expect(ProtectedBranch.matching("production")).to be_empty - expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).to include(production) - expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).not_to include(staging) + expect(ProtectedBranch.matching("production", protected_refs: [production, staging])).to include(production) + expect(ProtectedBranch.matching("production", protected_refs: [production, staging])).not_to include(staging) end end @@ -132,8 +132,8 @@ describe ProtectedBranch, models: true do staging = build(:protected_branch, name: "staging/*") expect(ProtectedBranch.matching("production/some-branch")).to be_empty - expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).to include(production) - expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).not_to include(staging) + expect(ProtectedBranch.matching("production/some-branch", protected_refs: [production, staging])).to include(production) + expect(ProtectedBranch.matching("production/some-branch", protected_refs: [production, staging])).not_to include(staging) end end end diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb index e1fd73dbd07..70ea96a954f 100644 --- a/spec/services/protected_tags/create_service_spec.rb +++ b/spec/services/protected_tags/create_service_spec.rb @@ -6,7 +6,6 @@ describe ProtectedTags::CreateService, services: true do let(:params) do { name: 'master', - merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }], push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }] } end @@ -17,7 +16,6 @@ describe ProtectedTags::CreateService, services: true do it 'creates a new protected tag' do expect { service.execute }.to change(ProtectedTag, :count).by(1) expect(project.protected_tags.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) - expect(project.protected_tags.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) end end end From b8c7bef5c092152ea85d1840e587cfc04293e1d7 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 3 Apr 2017 17:10:58 +0100 Subject: [PATCH 25/54] Extracted ProtectableDropdown to clean up Project#open_branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes it clear this is only used in dropdowns, instead of cluttering up Project class. Since we only care about branch names, it is also possible to refactor out a lot of the set/reject logic. A benchmark on Array/Set subtraction favoured using Arrays. This was with 5000 ‘branches’ and 2000 ‘protections’ to ensure a similar comparison to the commit which introduced using Set for intersection. Comparison: array subtraction: 485.8 i/s set subtraction: 128.7 i/s - 3.78x slower --- .../settings/repository_controller.rb | 25 +++++------------- app/models/project.rb | 9 ------- app/models/protectable_dropdown.rb | 26 +++++++++++++++++++ spec/models/project_spec.rb | 19 -------------- spec/models/protectable_dropdown_spec.rb | 24 +++++++++++++++++ 5 files changed, 57 insertions(+), 46 deletions(-) create mode 100644 app/models/protectable_dropdown.rb create mode 100644 spec/models/protectable_dropdown_spec.rb diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index 5160ee5e1e4..a87927fe1ec 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -4,8 +4,7 @@ module Projects before_action :authorize_admin_project! def show - @deploy_keys = DeployKeysPresenter - .new(@project, current_user: current_user) + @deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user) define_protected_refs end @@ -38,27 +37,17 @@ module Projects } end - #TODO: Move to Protections::TagMatcher.new(project).unprotected - def unprotected_tags - exact_protected_tag_names = @project.protected_tags.reject(&:wildcard?).map(&:name) - tag_names = @project.repository.tags.map(&:name) - non_open_tag_names = Set.new(exact_protected_tag_names).intersection(Set.new(tag_names)) - @project.repository.tags.reject { |tag| non_open_tag_names.include? tag.name } + def protectable_tags_for_dropdown + { open_tags: ProtectableDropdown.new(@project, :tags).hash } end - def unprotected_tags_hash - tags = unprotected_tags.map { |tag| { text: tag.name, id: tag.name, title: tag.name } } - { open_tags: tags } - end - - def open_branches - branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } - { open_branches: branches } + def protectable_branches_for_dropdown + { open_branches: ProtectableDropdown.new(@project, :branches).hash } end def load_gon_index - gon.push(open_branches) - gon.push(unprotected_tags_hash) + gon.push(protectable_tags_for_dropdown) + gon.push(protectable_branches_for_dropdown) gon.push(access_levels_options) end end diff --git a/app/models/project.rb b/app/models/project.rb index 970de324a5b..4175bfab0a9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -865,15 +865,6 @@ class Project < ActiveRecord::Base @repo_exists = false end - # Branches that are not _exactly_ matched by a protected branch. - #TODO: Move to Protections::BranchMatcher.new(project).unprotecte - def open_branches - exact_protected_branch_names = protected_branches.reject(&:wildcard?).map(&:name) - branch_names = repository.branches.map(&:name) - non_open_branch_names = Set.new(exact_protected_branch_names).intersection(Set.new(branch_names)) - repository.branches.reject { |branch| non_open_branch_names.include? branch.name } - end - def root_ref?(branch) repository.root_ref == branch end diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb new file mode 100644 index 00000000000..c9b2b213cd2 --- /dev/null +++ b/app/models/protectable_dropdown.rb @@ -0,0 +1,26 @@ +class ProtectableDropdown + def initialize(project, ref_type) + @project = project + @ref_type = ref_type + end + + # Tags/branches which are yet to be individually protected + def protectable_ref_names + non_wildcard_protections = protections.reject(&:wildcard?) + refs.map(&:name) - non_wildcard_protections.map(&:name) + end + + def hash + protectable_ref_names.map { |ref_name| { text: ref_name, id: ref_name, title: ref_name } } + end + + private + + def refs + @project.repository.public_send(@ref_type) + end + + def protections + @project.public_send("protected_#{@ref_type}") + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f68631ebe06..cc06949974e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -702,25 +702,6 @@ describe Project, models: true do end end - describe '#open_branches' do - let(:project) { create(:project, :repository) } - - before do - project.protected_branches.create(name: 'master') - end - - it { expect(project.open_branches.map(&:name)).to include('feature') } - it { expect(project.open_branches.map(&:name)).not_to include('master') } - - it "includes branches matching a protected branch wildcard" do - expect(project.open_branches.map(&:name)).to include('feature') - - create(:protected_branch, name: 'feat*', project: project) - - expect(Project.find(project.id).open_branches.map(&:name)).to include('feature') - end - end - describe '#star_count' do it 'counts stars from multiple users' do user1 = create :user diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb new file mode 100644 index 00000000000..7f8ef7195e5 --- /dev/null +++ b/spec/models/protectable_dropdown_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe ProtectableDropdown, models: true do + let(:project) { create(:project, :repository) } + let(:subject) { described_class.new(project, :branches) } + + describe '#protectable_ref_names' do + before do + project.protected_branches.create(name: 'master') + end + + it { expect(subject.protectable_ref_names).to include('feature') } + it { expect(subject.protectable_ref_names).not_to include('master') } + + it "includes branches matching a protected branch wildcard" do + expect(subject.protectable_ref_names).to include('feature') + + create(:protected_branch, name: 'feat*', project: project) + + subject = described_class.new(project.reload, :branches) + expect(subject.protectable_ref_names).to include('feature') + end + end +end From bf3cc824e4ce6cf49a82210eaaf1cca06f7fd281 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 3 Apr 2017 18:59:58 +0100 Subject: [PATCH 26/54] Moved Project#protected_branch? to ProtectedBranch, similar for tags --- app/helpers/branches_helper.rb | 2 +- app/helpers/tags_helper.rb | 2 +- app/models/merge_request.rb | 2 +- app/models/project.rb | 22 ++++---- app/models/protected_branch.rb | 8 +++ app/models/protected_tag.rb | 5 ++ app/services/delete_branch_service.rb | 2 +- app/services/git_push_service.rb | 2 +- app/views/projects/branches/_branch.html.haml | 2 +- lib/api/entities.rb | 2 +- lib/gitlab/checks/change_access.rb | 4 +- lib/gitlab/user_access.rb | 6 +- spec/lib/gitlab/checks/change_access_spec.rb | 2 +- spec/models/merge_request_spec.rb | 2 +- spec/models/project_spec.rb | 56 ------------------- spec/models/protected_branch_spec.rb | 56 +++++++++++++++++++ 16 files changed, 94 insertions(+), 81 deletions(-) diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 3fc85dc6b2b..a852b90c57e 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -1,6 +1,6 @@ module BranchesHelper def can_remove_branch?(project, branch_name) - if project.protected_branch? branch_name + if ProtectedBranch.protected?(project, branch_name) false elsif branch_name == project.repository.root_ref false diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index 6672e3da348..31aaf9e5607 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -23,6 +23,6 @@ module TagsHelper end def protected_tag?(project, tag) - project.protected_tag?(tag.name) + ProtectedTag.protected?(project, tag.name) end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index cef8ad76b07..38eefad96b6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -442,7 +442,7 @@ class MergeRequest < ActiveRecord::Base end def can_remove_source_branch?(current_user) - !source_project.protected_branch?(source_branch) && + !ProtectedBranch.protected?(source_project, source_branch) && !source_project.root_ref?(source_branch) && Ability.allowed?(current_user, :push_code, source_project) && diff_head_commit == source_branch_head diff --git a/app/models/project.rb b/app/models/project.rb index 4175bfab0a9..5cc224b4440 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -883,20 +883,19 @@ class Project < ActiveRecord::Base "#{url}.git" end - # Check if current branch name is marked as protected in the system - #TODO: Move elsewhere - def protected_branch?(branch_name) - return true if empty_repo? && default_branch_protected? - @protected_branches ||= self.protected_branches.to_a - ProtectedBranch.matching(branch_name, protected_refs: @protected_branches).present? + def empty_and_default_branch_protected? + empty_repo? && default_branch_protected? end - #TODO: Move elsewhere - def protected_tag?(tag_name) - #TODO: Check if memoization necessary, find way to have it work elsewhere - @protected_tags ||= self.protected_tags.to_a - ProtectedTag.matching(tag_name, protected_refs: @protected_tags).present? + #TODO: Check with if this is still needed, maybe because of `.select {` in ProtectedRefsMatcher + #Either with tests or by asking Tim + def protected_tags_array + @protected_tags_array ||= self.protected_tags.to_a + end + + def protected_branches_array + @protected_branches_array ||= self.protected_branches.to_a end def user_can_push_to_empty_repo?(user) @@ -1367,6 +1366,7 @@ class Project < ActiveRecord::Base "projects/#{id}/pushes_since_gc" end + #TODO: Move this and methods which depend upon it def default_branch_protected? current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index a0dbcf80c3d..eca8d5e0f7d 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -10,4 +10,12 @@ class ProtectedBranch < ActiveRecord::Base accepts_nested_attributes_for :push_access_levels accepts_nested_attributes_for :merge_access_levels + + # Check if branch name is marked as protected in the system + def self.protected?(project, ref_name) + return true if project.empty_and_default_branch_protected? + + protected_refs = project.protected_branches_array + self.matching(ref_name, protected_refs: protected_refs).present? + end end diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb index 301fe2092e9..bca5522759d 100644 --- a/app/models/protected_tag.rb +++ b/app/models/protected_tag.rb @@ -7,4 +7,9 @@ class ProtectedTag < ActiveRecord::Base validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." } accepts_nested_attributes_for :push_access_levels + + def self.protected?(project, ref_name) + protected_refs = project.protected_tags_array + self.matching(ref_name, protected_refs: protected_refs).present? + end end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 11a045f4c31..38a113caec7 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -11,7 +11,7 @@ class DeleteBranchService < BaseService return error('Cannot remove HEAD branch', 405) end - if project.protected_branch?(branch_name) + if ProtectedBranch.protected?(project, branch_name) return error('Protected branch cant be removed', 405) end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index bc7431c89a8..45411c779cc 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -127,7 +127,7 @@ class GitPushService < BaseService project.change_head(branch_name) # Set protection on the default branch if configured - if current_application_settings.default_branch_protection != PROTECTION_NONE && !@project.protected_branch?(@project.default_branch) + if current_application_settings.default_branch_protection != PROTECTION_NONE && !ProtectedBranch.protected?(@project, @project.default_branch) params = { name: @project.default_branch, diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 9eb610ba9c0..d84fa9e55c0 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -15,7 +15,7 @@ %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" } merged - - if @project.protected_branch? branch.name + - if ProtectedBranch.protected?(@project, branch.name) %span.label.label-success protected .controls.hidden-xs< diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e000e3e33d0..0fe7eb864e4 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -184,7 +184,7 @@ module API end expose :protected do |repo_branch, options| - options[:project].protected_branch?(repo_branch.name) + ProtectedBranch.protected?(options[:project], repo_branch.name) end expose :developers_can_push do |repo_branch, options| diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 540d95f2d1f..6f574a41727 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -33,7 +33,7 @@ module Gitlab def protected_branch_checks return if skip_authorization return unless @branch_name - return unless project.protected_branch?(@branch_name) + return unless ProtectedBranch.protected?(project, @branch_name) if forced_push? return "You are not allowed to force push code to a protected branch on this project." @@ -85,7 +85,7 @@ module Gitlab end def tag_protected? - project.protected_tag?(@tag_name) + ProtectedTag.protected?(project, @tag_name) end def push_checks diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 5a5a4ebd08b..5541a45e948 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -34,7 +34,7 @@ module Gitlab def can_push_tag?(ref) return false unless can_access_git? - if project.protected_tag?(ref) + if ProtectedTag.protected?(project, ref) project.protected_tags.matching_refs_accesible_to(ref, user) else user.can?(:push_code, project) @@ -44,7 +44,7 @@ module Gitlab def can_push_to_branch?(ref) return false unless can_access_git? - if project.protected_branch?(ref) + if ProtectedBranch.protected?(project, ref) return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) has_access = project.protected_branches.matching_refs_accesible_to(ref, user, action: :push) @@ -58,7 +58,7 @@ module Gitlab def can_merge_to_branch?(ref) return false unless can_access_git? - if project.protected_branch?(ref) + if ProtectedBranch.protected?(project, ref) project.protected_branches.matching_refs_accesible_to(ref, user, action: :merge) else user.can?(:push_code, project) diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index cb2989c32df..8525422908b 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -99,7 +99,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do context 'protected branches check' do before do - allow(project).to receive(:protected_branch?).with('master').and_return(true) + allow(ProtectedBranch).to receive(:protected?).with(project, 'master').and_return(true) end it 'returns an error if the user is not allowed to do forced pushes to protected branches' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 24e7c1b17d9..2f6614c133e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -441,7 +441,7 @@ describe MergeRequest, models: true do end it "can't be removed when its a protected branch" do - allow(subject.source_project).to receive(:protected_branch?).and_return(true) + allow(ProtectedBranch).to receive(:protected?).and_return(true) expect(subject.can_remove_source_branch?(user)).to be_falsey end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index cc06949974e..e6b23a1cc05 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1272,62 +1272,6 @@ describe Project, models: true do end end - describe '#protected_branch?' do - context 'existing project' do - let(:project) { create(:project, :repository) } - - it 'returns true when the branch matches a protected branch via direct match' do - create(:protected_branch, project: project, name: "foo") - - expect(project.protected_branch?('foo')).to eq(true) - end - - it 'returns true when the branch matches a protected branch via wildcard match' do - create(:protected_branch, project: project, name: "production/*") - - expect(project.protected_branch?('production/some-branch')).to eq(true) - end - - it 'returns false when the branch does not match a protected branch via direct match' do - expect(project.protected_branch?('foo')).to eq(false) - end - - it 'returns false when the branch does not match a protected branch via wildcard match' do - create(:protected_branch, project: project, name: "production/*") - - expect(project.protected_branch?('staging/some-branch')).to eq(false) - end - end - - context "new project" do - let(:project) { create(:empty_project) } - - it 'returns false when default_protected_branch is unprotected' do - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) - - expect(project.protected_branch?('master')).to be false - end - - it 'returns false when default_protected_branch lets developers push' do - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) - - expect(project.protected_branch?('master')).to be false - end - - it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) - - expect(project.protected_branch?('master')).to be true - end - - it 'returns true when default_branch_protection is in full protection' do - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL) - - expect(project.protected_branch?('master')).to be true - end - end - end - describe '#user_can_push_to_empty_repo?' do let(:project) { create(:empty_project) } let(:user) { create(:user) } diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index 1c02f8bfc3f..179a443c43d 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -137,4 +137,60 @@ describe ProtectedBranch, models: true do end end end + + describe '#protected?' do + context 'existing project' do + let(:project) { create(:project, :repository) } + + it 'returns true when the branch matches a protected branch via direct match' do + create(:protected_branch, project: project, name: "foo") + + expect(ProtectedBranch.protected?(project, 'foo')).to eq(true) + end + + it 'returns true when the branch matches a protected branch via wildcard match' do + create(:protected_branch, project: project, name: "production/*") + + expect(ProtectedBranch.protected?(project, 'production/some-branch')).to eq(true) + end + + it 'returns false when the branch does not match a protected branch via direct match' do + expect(ProtectedBranch.protected?(project, 'foo')).to eq(false) + end + + it 'returns false when the branch does not match a protected branch via wildcard match' do + create(:protected_branch, project: project, name: "production/*") + + expect(ProtectedBranch.protected?(project, 'staging/some-branch')).to eq(false) + end + end + + context "new project" do + let(:project) { create(:empty_project) } + + it 'returns false when default_protected_branch is unprotected' do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) + + expect(ProtectedBranch.protected?(project, 'master')).to be false + end + + it 'returns false when default_protected_branch lets developers push' do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(ProtectedBranch.protected?(project, 'master')).to be false + end + + it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + expect(ProtectedBranch.protected?(project, 'master')).to be true + end + + it 'returns true when default_branch_protection is in full protection' do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL) + + expect(ProtectedBranch.protected?(project, 'master')).to be true + end + end + end end From 4f71c29c8bb35642ed5d26015619fc3f838f5e56 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 3 Apr 2017 19:28:54 +0100 Subject: [PATCH 27/54] Moved default_branch_protected? out of Project --- app/models/project.rb | 13 +------------ app/models/protected_branch.rb | 7 ++++++- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 5cc224b4440..fdb0a679e28 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -883,11 +883,6 @@ class Project < ActiveRecord::Base "#{url}.git" end - - def empty_and_default_branch_protected? - empty_repo? && default_branch_protected? - end - #TODO: Check with if this is still needed, maybe because of `.select {` in ProtectedRefsMatcher #Either with tests or by asking Tim def protected_tags_array @@ -899,7 +894,7 @@ class Project < ActiveRecord::Base end def user_can_push_to_empty_repo?(user) - !default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER + !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER end def forked? @@ -1366,12 +1361,6 @@ class Project < ActiveRecord::Base "projects/#{id}/pushes_since_gc" end - #TODO: Move this and methods which depend upon it - def default_branch_protected? - current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || - current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE - end - # Similar to the normal callbacks that hook into the life cycle of an # Active Record object, you can also define callbacks that get triggered # when you add an object to an association collection. If any of these diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index eca8d5e0f7d..0f0ac18b1a3 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -13,9 +13,14 @@ class ProtectedBranch < ActiveRecord::Base # Check if branch name is marked as protected in the system def self.protected?(project, ref_name) - return true if project.empty_and_default_branch_protected? + return true if project.empty_repo? && default_branch_protected? protected_refs = project.protected_branches_array self.matching(ref_name, protected_refs: protected_refs).present? end + + def self.default_branch_protected? + current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || + current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE + end end From 04a50bd9515e09d047d6f678c5d9485f89b31df7 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 3 Apr 2017 19:47:08 +0100 Subject: [PATCH 28/54] Removed protected_tags_array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This memorized array appears to originally come from https://gitlab.com/gitlab-org/gitlab-ee/commit/19c2c90ccac86a21eb4266b9a5972162f917f692 which has a commit message of ‘fix warnings’. Without any comments on the original pull request I think we can safely get rid of it unless warnings re-appear. --- app/models/project.rb | 10 ---------- app/models/protected_branch.rb | 3 +-- app/models/protected_tag.rb | 3 +-- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index fdb0a679e28..13c5c181cc5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -883,16 +883,6 @@ class Project < ActiveRecord::Base "#{url}.git" end - #TODO: Check with if this is still needed, maybe because of `.select {` in ProtectedRefsMatcher - #Either with tests or by asking Tim - def protected_tags_array - @protected_tags_array ||= self.protected_tags.to_a - end - - def protected_branches_array - @protected_branches_array ||= self.protected_branches.to_a - end - def user_can_push_to_empty_repo?(user) !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 0f0ac18b1a3..28b7d5ad072 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -15,8 +15,7 @@ class ProtectedBranch < ActiveRecord::Base def self.protected?(project, ref_name) return true if project.empty_repo? && default_branch_protected? - protected_refs = project.protected_branches_array - self.matching(ref_name, protected_refs: protected_refs).present? + self.matching(ref_name, protected_refs: project.protected_branches).present? end def self.default_branch_protected? diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb index bca5522759d..a52fe90bb2b 100644 --- a/app/models/protected_tag.rb +++ b/app/models/protected_tag.rb @@ -9,7 +9,6 @@ class ProtectedTag < ActiveRecord::Base accepts_nested_attributes_for :push_access_levels def self.protected?(project, ref_name) - protected_refs = project.protected_tags_array - self.matching(ref_name, protected_refs: protected_refs).present? + self.matching(ref_name, protected_refs: project.protected_tags).present? end end From 35b719f60b21fbd09a1a2b4dc0d3f1e3e74e89e1 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 3 Apr 2017 19:48:00 +0100 Subject: [PATCH 29/54] Use delegation in ProtectedRef concern --- app/models/concerns/protected_ref.rb | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index 77d7b597534..4f3500d998a 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -6,6 +6,8 @@ module ProtectedRef validates :name, presence: true validates :project, presence: true + delegate :matching, :matches?, :wildcard?, to: :ref_matcher + def self.matching_refs_accesible_to(ref, user, action: :push) access_levels_for_ref(ref, action).any? do |access_level| access_level.check_access(user) @@ -27,18 +29,6 @@ module ProtectedRef project.commit(self.name) end - def matching(refs) - ref_matcher.matching(refs) - end - - def matches?(refs) - ref_matcher.matches?(refs) - end - - def wildcard? - ref_matcher.wildcard? - end - private def ref_matcher From 9f4b8dba805915bd21d315f159035449f9f4bef0 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 3 Apr 2017 20:06:06 +0100 Subject: [PATCH 30/54] Clean up non TODO rubocop errors --- .../projects/settings/repository_controller.rb | 2 +- app/models/concerns/protected_ref.rb | 2 -- app/models/concerns/protected_ref_access.rb | 1 + lib/gitlab/checks/change_access.rb | 8 ++++---- lib/gitlab/import_export/relation_factory.rb | 2 +- .../projects/protected_tags_controller_spec.rb | 12 ++++++------ 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index a87927fe1ec..fb175b4a636 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -13,7 +13,7 @@ module Projects def define_protected_refs @protected_branches = @project.protected_branches.order(:name).page(params[:page]) - @protected_tags = @project.protected_tags.order(:name).page(params[:page]) + @protected_tags = @project.protected_tags.order(:name).page(params[:page]) #TODO duplicated pagination param? @protected_branch = @project.protected_branches.new @protected_tag = @project.protected_tags.new load_gon_index diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index 4f3500d998a..3681ae63e3a 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -18,8 +18,6 @@ module ProtectedRef self.matching(ref).map(&:"@#{action}_access_levels").flatten end - private - def self.matching(ref_name, protected_refs: nil) ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs) end diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index 2870cd9385b..08377127f69 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -1,3 +1,4 @@ +#TODO: Refactor, checking EE # module ProtectedRefAccess # extend ActiveSupport::Concern diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 6f574a41727..07fd4024346 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -71,15 +71,15 @@ module Gitlab def protected_tag_checks return unless tag_protected? - if forced_push? - return "You are not allowed to force push protected tags." #TODO: Wording, 'not allowed to update proteted tags'? + if forced_push? #TODO: Verify if this should prevent all updates, and mention in UI and documentation + return "Protected tags cannot be updated." end if Gitlab::Git.blank_ref?(@newrev) - return "You are not allowed to delete protected tags." #TODO: Wording, do these need to mention 'you' if the rule applies to everyone + return "Protected tags cannot be deleted." end - if !user_access.can_push_tag?(@tag_name) + unless user_access.can_push_tag?(@tag_name) return "You are not allowed to create this tag as it is protected." end end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index d44563333a5..9d269c5d384 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -7,7 +7,7 @@ module Gitlab triggers: 'Ci::Trigger', builds: 'Ci::Build', hooks: 'ProjectHook', - merge_access_levels: 'ProtectedBranch::MergeAccessLevel', + merge_access_levels: 'ProtectedBranch::MergeAccessLevel', #TODO: Tags push_access_levels: 'ProtectedBranch::PushAccessLevel', labels: :project_labels, priorities: :label_priorities, diff --git a/spec/controllers/projects/protected_tags_controller_spec.rb b/spec/controllers/projects/protected_tags_controller_spec.rb index 729017c1483..ac802981294 100644 --- a/spec/controllers/projects/protected_tags_controller_spec.rb +++ b/spec/controllers/projects/protected_tags_controller_spec.rb @@ -1,10 +1,10 @@ require('spec_helper') describe Projects::ProtectedTagsController do - # describe "GET #index" do - # let(:project) { create(:project_empty_repo, :public) } - # it "redirects empty repo to projects page" do - # get(:index, namespace_id: project.namespace.to_param, project_id: project) - # end - # end + describe "GET #index" do + let(:project) { create(:project_empty_repo, :public) } + it "redirects empty repo to projects page" do + get(:index, namespace_id: project.namespace.to_param, project_id: project) + end + end end From 3c91841d032f02b0b0d4c532998bbc923247e804 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 3 Apr 2017 21:00:51 +0100 Subject: [PATCH 31/54] Created ProtectedRefsController to reduce Tags/Branches duplication Fixes ProtectedBranches#create flash errors bug due to typo in 'flash[:alert] = @protected_branches.errors' --- .../projects/protected_branches_controller.rb | 60 ++++++------------- .../projects/protected_refs_controller.rb | 48 +++++++++++++++ .../projects/protected_tags_controller.rb | 60 ++++++------------- 3 files changed, 86 insertions(+), 82 deletions(-) create mode 100644 app/controllers/projects/protected_refs_controller.rb diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index a8cb07eb67a..a245a60910e 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,58 +1,36 @@ -class Projects::ProtectedBranchesController < Projects::ApplicationController - include RepositorySettingsRedirect - # Authorize - before_action :require_non_empty_project - before_action :authorize_admin_project! - before_action :load_protected_branch, only: [:show, :update, :destroy] +class Projects::ProtectedBranchesController < Projects::ProtectedRefsController - layout "project_settings" + protected - def index - redirect_to_repository_settings(@project) + def protected_ref + @protected_branch end - def create - @protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute - unless @protected_branch.persisted? - flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe - end - redirect_to_repository_settings(@project) + def protected_ref=(val) + @protected_branch = val end - def show - @matching_branches = @protected_branch.matching(@project.repository.branches) + def matching_refs=(val) + @matching_branches = val end - def update - @protected_branch = ::ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch) - - if @protected_branch.valid? - respond_to do |format| - format.json { render json: @protected_branch, status: :ok } - end - else - respond_to do |format| - format.json { render json: @protected_branch.errors, status: :unprocessable_entity } - end - end + def project_refs + @project.repository.branches end - def destroy - @protected_branch.destroy - - respond_to do |format| - format.html { redirect_to_repository_settings(@project) } - format.js { head :ok } - end + def create_service + ::ProtectedBranches::CreateService end - private - - def load_protected_branch - @protected_branch = @project.protected_branches.find(params[:id]) + def update_service + ::ProtectedBranches::UpdateService end - def protected_branch_params + def load_protected_ref + self.protected_ref = @project.protected_branches.find(params[:id]) + end + + def protected_ref_params params.require(:protected_branch).permit(:name, merge_access_levels_attributes: [:access_level, :id], push_access_levels_attributes: [:access_level, :id]) diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb new file mode 100644 index 00000000000..63f005124a9 --- /dev/null +++ b/app/controllers/projects/protected_refs_controller.rb @@ -0,0 +1,48 @@ +class Projects::ProtectedRefsController < Projects::ApplicationController + include RepositorySettingsRedirect + # Authorize + before_action :require_non_empty_project + before_action :authorize_admin_project! + before_action :load_protected_ref, only: [:show, :update, :destroy] + + layout "project_settings" + + def index + redirect_to_repository_settings(@project) + end + + def create + self.protected_ref = create_service.new(@project, current_user, protected_ref_params).execute + unless protected_ref.persisted? + flash[:alert] = protected_ref.errors.full_messages.join(', ').html_safe + end + redirect_to_repository_settings(@project) + end + + def show + self.matching_refs = protected_ref.matching(project_refs) + end + + def update + self.protected_ref = update_service.new(@project, current_user, protected_ref_params).execute(protected_ref) + + if protected_ref.valid? + respond_to do |format| + format.json { render json: protected_ref, status: :ok } + end + else + respond_to do |format| + format.json { render json: protected_ref.errors, status: :unprocessable_entity } + end + end + end + + def destroy + protected_ref.destroy + + respond_to do |format| + format.html { redirect_to_repository_settings(@project) } + format.js { head :ok } + end + end +end diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb index 5ab5d1d997b..8f407b42ac8 100644 --- a/app/controllers/projects/protected_tags_controller.rb +++ b/app/controllers/projects/protected_tags_controller.rb @@ -1,58 +1,36 @@ -class Projects::ProtectedTagsController < Projects::ApplicationController - include RepositorySettingsRedirect - # Authorize - before_action :require_non_empty_project - before_action :authorize_admin_project! - before_action :load_protected_tag, only: [:show, :update, :destroy] +class Projects::ProtectedTagsController < Projects::ProtectedRefsController - layout "project_settings" + protected - def index - redirect_to_repository_settings(@project) + def protected_ref + @protected_tag end - def create - @protected_tag = ::ProtectedTags::CreateService.new(@project, current_user, protected_tag_params).execute - unless @protected_tag.persisted? - flash[:alert] = @protected_tags.errors.full_messages.join(', ').html_safe - end - redirect_to_repository_settings(@project) + def protected_ref=(val) + @protected_tag = val end - def show - @matching_tags = @protected_tag.matching(@project.repository.tags) + def matching_refs=(val) + @matching_tags = val end - def update - @protected_tag = ::ProtectedTags::UpdateService.new(@project, current_user, protected_tag_params).execute(@protected_tag) - - if @protected_tag.valid? - respond_to do |format| - format.json { render json: @protected_tag, status: :ok } - end - else - respond_to do |format| - format.json { render json: @protected_tag.errors, status: :unprocessable_entity } - end - end + def project_refs + @project.repository.tags end - def destroy - @protected_tag.destroy - - respond_to do |format| - format.html { redirect_to_repository_settings(@project) } - format.js { head :ok } - end + def create_service + ::ProtectedTags::CreateService end - private - - def load_protected_tag - @protected_tag = @project.protected_tags.find(params[:id]) + def update_service + ::ProtectedTags::UpdateService end - def protected_tag_params + def load_protected_ref + self.protected_ref = @project.protected_tags.find(params[:id]) + end + + def protected_ref_params params.require(:protected_tag).permit(:name, push_access_levels_attributes: [:access_level, :id]) end end From ff2713a57046bd08764ad391d7f34bd27f787610 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 3 Apr 2017 22:04:37 +0100 Subject: [PATCH 32/54] Fix typos in ProtectedRef concern and whitespace detected by rubocop --- app/controllers/projects/protected_branches_controller.rb | 1 - app/controllers/projects/protected_tags_controller.rb | 1 - app/controllers/projects/settings/repository_controller.rb | 1 - app/models/concerns/protected_ref.rb | 4 ++-- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index a245a60910e..c2a55c9500a 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,5 +1,4 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController - protected def protected_ref diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb index 8f407b42ac8..ff132056aa4 100644 --- a/app/controllers/projects/protected_tags_controller.rb +++ b/app/controllers/projects/protected_tags_controller.rb @@ -1,5 +1,4 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController - protected def protected_ref diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index fb175b4a636..ff818d9e51a 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -19,7 +19,6 @@ module Projects load_gon_index end - def access_levels_options #TODO: consider protected tags #TODO: Refactor ProtectedBranch::PushAccessLevel so it doesn't mention branches diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index 3681ae63e3a..f6841669ab0 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -9,13 +9,13 @@ module ProtectedRef delegate :matching, :matches?, :wildcard?, to: :ref_matcher def self.matching_refs_accesible_to(ref, user, action: :push) - access_levels_for_ref(ref, action).any? do |access_level| + access_levels_for_ref(ref, action: action).any? do |access_level| access_level.check_access(user) end end def self.access_levels_for_ref(ref, action: :push) - self.matching(ref).map(&:"@#{action}_access_levels").flatten + self.matching(ref).map(&:"#{action}_access_levels").flatten end def self.matching(ref_name, protected_refs: nil) From d5acb69e116cbbed105e29552d7cca2e864f0c8f Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 4 Apr 2017 00:05:51 +0100 Subject: [PATCH 33/54] Protected Tags prevents all updates instead of just force pushes. This only changes behaviour for masters, as developers are already prevented from updating/deleting tags without the Protected Tags feature --- .../projects/protected_tags/_index.html.haml | 4 +- lib/gitlab/checks/change_access.rb | 14 +-- spec/lib/gitlab/checks/change_access_spec.rb | 87 +++++++++---------- 3 files changed, 51 insertions(+), 54 deletions(-) diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml index 591d64ae7de..0bfb1ad191d 100644 --- a/app/views/projects/protected_tags/_index.html.haml +++ b/app/views/projects/protected_tags/_index.html.haml @@ -8,8 +8,8 @@ %p.prepend-top-20 By default, Protected tags are designed to: %ul - %li Prevent tag pushes from everybody except Masters - %li Prevent anyone from force pushing to the tag + %li Prevent tag creation by everybody except Masters + %li Prevent anyone from updating the tag %li Prevent anyone from deleting the tag .col-lg-9 - if can? current_user, :admin_project, @project diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 07fd4024346..d0bbd713710 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -37,7 +37,7 @@ module Gitlab if forced_push? return "You are not allowed to force push code to a protected branch on this project." - elsif blank_ref? + elsif deletion? return "You are not allowed to delete protected branches from this project." end @@ -62,7 +62,7 @@ module Gitlab return unless @tag_name if tag_exists? && user_access.cannot_do_action?(:admin_project) - "You are not allowed to change existing tags on this project." + return "You are not allowed to change existing tags on this project." end protected_tag_checks @@ -71,11 +71,11 @@ module Gitlab def protected_tag_checks return unless tag_protected? - if forced_push? #TODO: Verify if this should prevent all updates, and mention in UI and documentation + if update? return "Protected tags cannot be updated." end - if Gitlab::Git.blank_ref?(@newrev) + if deletion? return "Protected tags cannot be deleted." end @@ -106,7 +106,11 @@ module Gitlab Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env) end - def blank_ref? + def update? + !Gitlab::Git.blank_ref?(@oldrev) && !deletion? + end + + def deletion? Gitlab::Git.blank_ref?(@newrev) end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 8525422908b..afc29baa7e6 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -5,13 +5,10 @@ describe Gitlab::Checks::ChangeAccess, lib: true do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:user_access) { Gitlab::UserAccess.new(user, project: project) } - let(:changes) do - { - oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c', - newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51', - ref: 'refs/heads/master' - } - end + let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + let(:ref) { 'refs/heads/master' } + let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } } let(:protocol) { 'ssh' } subject do @@ -41,13 +38,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do end context 'tags check' do - let(:changes) do - { - oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c', - newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51', - ref: 'refs/tags/v1.0.0' - } - end + let(:ref) { 'refs/tags/v1.0.0' } it 'returns an error if the user is not allowed to update tags' do allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) @@ -60,38 +51,46 @@ describe Gitlab::Checks::ChangeAccess, lib: true do context 'with protected tag' do let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') } - context 'deletion' do - let(:changes) do - { - oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c', - newrev: '0000000000000000000000000000000000000000', - ref: 'refs/tags/v1.0.0' - } + context 'as master' do + before { project.add_master(user) } + + context 'deletion' do + let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } + let(:newrev) { '0000000000000000000000000000000000000000' } + + it 'is prevented' do + expect(subject.status).to be(false) + expect(subject.message).to include('cannot be deleted') + end end - it 'is prevented' do + context 'update' do + let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + + it 'is prevented' do + expect(subject.status).to be(false) + expect(subject.message).to include('cannot be updated') + end + end + end + + context 'creation' do + let(:oldrev) { '0000000000000000000000000000000000000000' } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + let(:ref) { 'refs/tags/v9.1.0' } + + it 'prevents creation below access level' do expect(subject.status).to be(false) - expect(subject.message).to include('delete protected tags') + expect(subject.message).to include('allowed to create this tag as it is protected') end - end - it 'prevents force push' do - expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) + context 'when user has access' do + let!(:protected_tag) { create(:protected_tag, :developers_can_push, project: project, name: 'v*') } - expect(subject.status).to be(false) - expect(subject.message).to include('force push protected tags') - end - - it 'prevents creation below access level' do - expect(subject.status).to be(false) - expect(subject.message).to include('allowed to') - end - - context 'when user has access' do - let!(:protected_tag) { create(:protected_tag, :developers_can_push, project: project, name: 'v*') } - - it 'allows tag creation' do - expect(subject.status).to be(true) + it 'allows tag creation' do + expect(subject.status).to be(true) + end end end end @@ -126,13 +125,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do end context 'branch deletion' do - let(:changes) do - { - oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c', - newrev: '0000000000000000000000000000000000000000', - ref: 'refs/heads/master' - } - end + let(:newrev) { '0000000000000000000000000000000000000000' } it 'returns an error if the user is not allowed to delete protected branches' do expect(subject.status).to be(false) From 90c8bb8301b4bc3268a5fa4ea8bddafbc29d6871 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 4 Apr 2017 01:39:34 +0100 Subject: [PATCH 34/54] Fixed developers_can_push in RepoBranch API entity --- app/models/concerns/protected_ref.rb | 6 ++++++ lib/api/entities.rb | 8 ++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index f6841669ab0..a04dea0bc55 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -14,6 +14,12 @@ module ProtectedRef end end + def self.developers_can?(action, ref) + access_levels_for_ref(ref, action: action).any? do |access_level| + access_level.access_level == Gitlab::Access::DEVELOPER + end + end + def self.access_levels_for_ref(ref, action: :push) self.matching(ref).map(&:"#{action}_access_levels").flatten end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 0fe7eb864e4..0cc6188938d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -188,15 +188,11 @@ module API end expose :developers_can_push do |repo_branch, options| - project = options[:project] - access_levels = project.protected_branches.access_levels_for_ref(repo_branch.name, :push) - access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER } + options[:project].protected_branches.developers_can?(:push, repo_branch.name) end expose :developers_can_merge do |repo_branch, options| - project = options[:project] - access_levels = project.protected_branches.access_levels_for_ref(repo_branch.name, :merge) - access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER } + options[:project].protected_branches.developers_can?(:merge, repo_branch.name) end end From 1e15444ae6dda02744db42d08c817252953c7b1f Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 4 Apr 2017 02:05:42 +0100 Subject: [PATCH 35/54] Cleanup & tests for UserAccess#can_create_tag? --- app/models/concerns/protected_ref.rb | 2 +- lib/gitlab/checks/change_access.rb | 2 +- lib/gitlab/user_access.rb | 11 ++--- spec/lib/gitlab/user_access_spec.rb | 70 ++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index a04dea0bc55..ab28eb19b64 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -8,7 +8,7 @@ module ProtectedRef delegate :matching, :matches?, :wildcard?, to: :ref_matcher - def self.matching_refs_accesible_to(ref, user, action: :push) + def self.protected_ref_accessible_to?(ref, user, action: :push) access_levels_for_ref(ref, action: action).any? do |access_level| access_level.check_access(user) end diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index d0bbd713710..0d96c4d41d7 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -79,7 +79,7 @@ module Gitlab return "Protected tags cannot be deleted." end - unless user_access.can_push_tag?(@tag_name) + unless user_access.can_create_tag?(@tag_name) return "You are not allowed to create this tag as it is protected." end end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 5541a45e948..6af5de4dc08 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -28,14 +28,11 @@ module Gitlab true end - #TODO: Test this - #TODO move most to ProtectedTag::AccessChecker. Or maybe UserAccess::Protections::Tag - #TODO: then consider removing method, if it turns out can_access_git? and can?(:push_code are checked in change_access - def can_push_tag?(ref) + def can_create_tag?(ref) return false unless can_access_git? if ProtectedTag.protected?(project, ref) - project.protected_tags.matching_refs_accesible_to(ref, user) + project.protected_tags.protected_ref_accessible_to?(ref, user) else user.can?(:push_code, project) end @@ -47,7 +44,7 @@ module Gitlab if ProtectedBranch.protected?(project, ref) return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) - has_access = project.protected_branches.matching_refs_accesible_to(ref, user, action: :push) + has_access = project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push) has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref) else @@ -59,7 +56,7 @@ module Gitlab return false unless can_access_git? if ProtectedBranch.protected?(project, ref) - project.protected_branches.matching_refs_accesible_to(ref, user, action: :merge) + project.protected_branches.protected_ref_accessible_to?(ref, user, action: :merge) else user.can?(:push_code, project) end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 369e55f61f1..c425aef359a 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -142,4 +142,74 @@ describe Gitlab::UserAccess, lib: true do end end end + + describe 'can_create_tag?' do + describe 'push to none protected tag' do + it 'returns true if user is a master' do + project.add_user(user, :master) + + expect(access.can_create_tag?('random_tag')).to be_truthy + end + + it 'returns true if user is a developer' do + project.add_user(user, :developer) + + expect(access.can_create_tag?('random_tag')).to be_truthy + end + + it 'returns false if user is a reporter' do + project.add_user(user, :reporter) + + expect(access.can_create_tag?('random_tag')).to be_falsey + end + end + + + describe 'push to protected tag' do + let(:tag) { create(:protected_tag, project: project, name: "test") } + let(:not_existing_tag) { create :protected_tag, project: project } + + it 'returns true if user is a master' do + project.add_user(user, :master) + + expect(access.can_create_tag?(tag.name)).to be_truthy + end + + it 'returns false if user is a developer' do + project.add_user(user, :developer) + + expect(access.can_create_tag?(tag.name)).to be_falsey + end + + it 'returns false if user is a reporter' do + project.add_user(user, :reporter) + + expect(access.can_create_tag?(tag.name)).to be_falsey + end + end + + describe 'push to protected tag if allowed for developers' do + before do + @tag = create(:protected_tag, :developers_can_push, project: project) + end + + it 'returns true if user is a master' do + project.add_user(user, :master) + + expect(access.can_create_tag?(@tag.name)).to be_truthy + end + + it 'returns true if user is a developer' do + project.add_user(user, :developer) + + expect(access.can_create_tag?(@tag.name)).to be_truthy + end + + it 'returns false if user is a reporter' do + project.add_user(user, :reporter) + + expect(access.can_create_tag?(@tag.name)).to be_falsey + end + end + end end From 3bb3a6886f3b206a2ec089d6b1e8854615daa0b8 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 4 Apr 2017 02:19:04 +0100 Subject: [PATCH 36/54] Attempt to fix import/export of push_access_levels for protected tags --- lib/gitlab/import_export/import_export.yml | 2 ++ lib/gitlab/import_export/relation_factory.rb | 3 ++- spec/lib/gitlab/import_export/all_models.yml | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index ab74c8782f6..7bf5568a8ee 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -46,6 +46,8 @@ project_tree: - protected_branches: - :merge_access_levels - :push_access_levels + - protected_tags: + - :push_access_levels - :project_feature # Only include the following attributes for the models specified. diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 9d269c5d384..b1d8e385f2e 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -7,8 +7,9 @@ module Gitlab triggers: 'Ci::Trigger', builds: 'Ci::Build', hooks: 'ProjectHook', - merge_access_levels: 'ProtectedBranch::MergeAccessLevel', #TODO: Tags + merge_access_levels: 'ProtectedBranch::MergeAccessLevel', push_access_levels: 'ProtectedBranch::PushAccessLevel', + #TODO: How to add?- push_access_levels: 'ProtectedTag::PushAccessLevel', labels: :project_labels, priorities: :label_priorities, label: :project_label }.freeze diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index ddeb71730e7..83503c73e75 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -111,10 +111,14 @@ protected_branches: - project - merge_access_levels - push_access_levels +protected_tags: +- project +- push_access_levels merge_access_levels: - protected_branch push_access_levels: - protected_branch +- protected_tag project: - taggings - base_tags @@ -169,6 +173,7 @@ project: - snippets - hooks - protected_branches +- protected_tags - project_members - users - requesters From f9e849c076efb3162a3d951d8aae2e7be3e574f4 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 4 Apr 2017 02:59:37 +0100 Subject: [PATCH 37/54] Cleaned up duplication with ProtectedRefAccess concern --- .../settings/repository_controller.rb | 19 ++++++----- .../concerns/protected_branch_access.rb | 15 ++------- app/models/concerns/protected_ref_access.rb | 32 ++++++++----------- app/models/concerns/protected_tag_access.rb | 15 ++------- spec/lib/gitlab/user_access_spec.rb | 1 - 5 files changed, 27 insertions(+), 55 deletions(-) diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index ff818d9e51a..9022cf8f0d8 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -23,19 +23,18 @@ module Projects #TODO: consider protected tags #TODO: Refactor ProtectedBranch::PushAccessLevel so it doesn't mention branches { - push_access_levels: { - roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text| - { id: id, text: text, before_divider: true } - end - }, - merge_access_levels: { - roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text| - { id: id, text: text, before_divider: true } - end - } + push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel), + merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel) } end + def levels_for_dropdown(access_level_type) + roles = access_level_type.human_access_levels.map do |id, text| + { id: id, text: text, before_divider: true } + end + { roles: roles } + end + def protectable_tags_for_dropdown { open_tags: ProtectableDropdown.new(@project, :tags).hash } end diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 9dd4d9c6f24..06cae00249a 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -2,20 +2,9 @@ module ProtectedBranchAccess extend ActiveSupport::Concern included do + include ProtectedRefAccess + belongs_to :protected_branch delegate :project, to: :protected_branch - - scope :master, -> { where(access_level: Gitlab::Access::MASTER) } - scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } - end - - def humanize - self.class.human_access_levels[self.access_level] - end - - def check_access(user) - return true if user.is_admin? - - project.team.max_member_access(user.id) >= access_level end end diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index 08377127f69..0c7e5157cdf 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -1,22 +1,18 @@ -#TODO: Refactor, checking EE -# module ProtectedRefAccess -# extend ActiveSupport::Concern +module ProtectedRefAccess + extend ActiveSupport::Concern -# included do -# # belongs_to :protected_branch -# # delegate :project, to: :protected_branch + included do + scope :master, -> { where(access_level: Gitlab::Access::MASTER) } + scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } + end -# scope :master, -> { where(access_level: Gitlab::Access::MASTER) } -# scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } -# end + def humanize + self.class.human_access_levels[self.access_level] + end -# def humanize -# self.class.human_access_levels[self.access_level] -# end + def check_access(user) + return true if user.is_admin? -# def check_access(user) -# return true if user.is_admin? - -# project.team.max_member_access(user.id) >= access_level -# end -# end + project.team.max_member_access(user.id) >= access_level + end +end diff --git a/app/models/concerns/protected_tag_access.rb b/app/models/concerns/protected_tag_access.rb index cf66a6434b5..9b7d31a6fd5 100644 --- a/app/models/concerns/protected_tag_access.rb +++ b/app/models/concerns/protected_tag_access.rb @@ -2,20 +2,9 @@ module ProtectedTagAccess extend ActiveSupport::Concern included do + include ProtectedRefAccess + belongs_to :protected_tag delegate :project, to: :protected_tag - - scope :master, -> { where(access_level: Gitlab::Access::MASTER) } - scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } - end - - def humanize - self.class.human_access_levels[self.access_level] - end - - def check_access(user) - return true if user.is_admin? - - project.team.max_member_access(user.id) >= access_level end end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index c425aef359a..c69ff3446ea 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -164,7 +164,6 @@ describe Gitlab::UserAccess, lib: true do end end - describe 'push to protected tag' do let(:tag) { create(:protected_tag, project: project, name: "test") } let(:not_existing_tag) { create :protected_tag, project: project } From 07d7d8e65905a39164b63f55eccdcea8f10f5d14 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 4 Apr 2017 03:37:22 +0100 Subject: [PATCH 38/54] Renamed ProtectedTag push_access_levels to create_access_levels --- .../protected_tags/protected_tag_create.js | 14 ++++++------ .../protected_tags/protected_tag_edit.js | 22 +++++++++---------- .../projects/protected_tags_controller.rb | 2 +- .../settings/repository_controller.rb | 5 ++--- app/models/protected_tag.rb | 6 ++--- ...access_level.rb => create_access_level.rb} | 2 +- .../_create_protected_tag.html.haml | 10 ++++----- .../protected_tags/_tags_list.html.haml | 2 +- .../protected_tags/_update_protected_tag.haml | 8 +++---- .../20170309173138_create_protected_tags.rb | 8 +++---- db/schema.rb | 12 +++++----- lib/gitlab/import_export/import_export.yml | 2 +- lib/gitlab/import_export/relation_factory.rb | 2 +- spec/factories/protected_tags.rb | 10 ++++----- .../protected_tags/access_control_ce_spec.rb | 20 ++++++++--------- spec/lib/gitlab/checks/change_access_spec.rb | 2 +- spec/lib/gitlab/import_export/all_models.yml | 3 ++- spec/lib/gitlab/user_access_spec.rb | 2 +- .../protected_tags/create_service_spec.rb | 4 ++-- 19 files changed, 68 insertions(+), 68 deletions(-) rename app/models/protected_tag/{push_access_level.rb => create_access_level.rb} (91%) diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 4c652e7747f..84b1b232649 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -11,20 +11,20 @@ } buildDropdowns() { - const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + const $allowedToCreateDropdown = this.$wrap.find('.js-allowed-to-create'); // Cache callback this.onSelectCallback = this.onSelect.bind(this); - // Allowed to Push dropdown + // Allowed to Create dropdown new gl.ProtectedTagAccessDropdown({ - $dropdown: $allowedToPushDropdown, - data: gon.push_access_levels, + $dropdown: $allowedToCreateDropdown, + data: gon.create_access_levels, onSelect: this.onSelectCallback }); // Select default - $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); + $allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0); // Protected tag dropdown new ProtectedTagDropdown({ @@ -37,9 +37,9 @@ onSelect() { // Enable submit button const $tagInput = this.$wrap.find('input[name="protected_tag[name]"]'); - const $allowedToPushInput = this.$wrap.find('input[name="protected_tag[push_access_levels_attributes][0][access_level]"]'); + const $allowedToCreateInput = this.$wrap.find('input[name="protected_tag[create_access_levels_attributes][0][access_level]"]'); - this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToPushInput.length)); + this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length)); } }; })(window); diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index b93e903621e..0227be35c8f 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -7,27 +7,27 @@ gl.ProtectedTagEdit = class { constructor(options) { this.$wrap = options.$wrap; - this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + this.$allowedToCreateDropdown = this.$wrap.find('.js-allowed-to-create'); this.buildDropdowns(); } buildDropdowns() { - // Allowed to push dropdown + // Allowed to create dropdown new gl.ProtectedTagAccessDropdown({ - $dropdown: this.$allowedToPushDropdown, - data: gon.push_access_levels, + $dropdown: this.$allowedToCreateDropdown, + data: gon.create_access_levels, onSelect: this.onSelect.bind(this) }); } onSelect() { - const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdown.data('fieldName')}"]`); // Do not update if one dropdown has not selected any option - if (!$allowedToPushInput.length) return; + if (!$allowedToCreateInput.length) return; - this.$allowedToPushDropdown.disable(); + this.$allowedToCreateDropdown.disable(); $.ajax({ type: 'POST', @@ -36,9 +36,9 @@ data: { _method: 'PATCH', protected_tag: { - push_access_levels_attributes: [{ - id: this.$allowedToPushDropdown.data('access-level-id'), - access_level: $allowedToPushInput.val() + create_access_levels_attributes: [{ + id: this.$allowedToCreateDropdown.data('access-level-id'), + access_level: $allowedToCreateInput.val() }] } }, @@ -47,7 +47,7 @@ new Flash('Failed to update tag!'); } }).always(() => { - this.$allowedToPushDropdown.enable(); + this.$allowedToCreateDropdown.enable(); }); } }; diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb index ff132056aa4..0e00baedbdf 100644 --- a/app/controllers/projects/protected_tags_controller.rb +++ b/app/controllers/projects/protected_tags_controller.rb @@ -30,6 +30,6 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController end def protected_ref_params - params.require(:protected_tag).permit(:name, push_access_levels_attributes: [:access_level, :id]) + params.require(:protected_tag).permit(:name, create_access_levels_attributes: [:access_level, :id]) end end diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index 9022cf8f0d8..44de8a49593 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -13,16 +13,15 @@ module Projects def define_protected_refs @protected_branches = @project.protected_branches.order(:name).page(params[:page]) - @protected_tags = @project.protected_tags.order(:name).page(params[:page]) #TODO duplicated pagination param? + @protected_tags = @project.protected_tags.order(:name).page(params[:page]) @protected_branch = @project.protected_branches.new @protected_tag = @project.protected_tags.new load_gon_index end def access_levels_options - #TODO: consider protected tags - #TODO: Refactor ProtectedBranch::PushAccessLevel so it doesn't mention branches { + create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel), push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel), merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel) } diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb index a52fe90bb2b..83964095516 100644 --- a/app/models/protected_tag.rb +++ b/app/models/protected_tag.rb @@ -2,11 +2,11 @@ class ProtectedTag < ActiveRecord::Base include Gitlab::ShellAdapter include ProtectedRef - has_many :push_access_levels, dependent: :destroy + has_many :create_access_levels, dependent: :destroy - validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." } + validates :create_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." } - accepts_nested_attributes_for :push_access_levels + accepts_nested_attributes_for :create_access_levels def self.protected?(project, ref_name) self.matching(ref_name, protected_refs: project.protected_tags).present? diff --git a/app/models/protected_tag/push_access_level.rb b/app/models/protected_tag/create_access_level.rb similarity index 91% rename from app/models/protected_tag/push_access_level.rb rename to app/models/protected_tag/create_access_level.rb index 9282af841ce..c7e1319719d 100644 --- a/app/models/protected_tag/push_access_level.rb +++ b/app/models/protected_tag/create_access_level.rb @@ -1,4 +1,4 @@ -class ProtectedTag::PushAccessLevel < ActiveRecord::Base +class ProtectedTag::CreateAccessLevel < ActiveRecord::Base include ProtectedTagAccess validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml index 9fdebf2c982..af332f942d6 100644 --- a/app/views/projects/protected_tags/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml @@ -19,14 +19,14 @@ %code production/* are supported .form-group - %label.col-md-2.text-right{ for: 'push_access_levels_attributes' } - Allowed to push: + %label.col-md-2.text-right{ for: 'create_access_levels_attributes' } + Allowed to create: .col-md-10 - .push_access_levels-container + .create_access_levels-container = dropdown_tag('Select', - options: { toggle_class: 'js-allowed-to-push wide', + options: { toggle_class: 'js-allowed-to-create wide', dropdown_class: 'dropdown-menu-selectable', - data: { field_name: 'protected_tag[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) + data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }}) .panel-footer = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/protected_tags/_tags_list.html.haml b/app/views/projects/protected_tags/_tags_list.html.haml index 6f63971923d..cc006ed8a0b 100644 --- a/app/views/projects/protected_tags/_tags_list.html.haml +++ b/app/views/projects/protected_tags/_tags_list.html.haml @@ -17,7 +17,7 @@ %tr %th Protected tag (#{@protected_tags.size}) %th Last commit - %th Allowed to push + %th Allowed to create - if can_admin_project %th %tbody diff --git a/app/views/projects/protected_tags/_update_protected_tag.haml b/app/views/projects/protected_tags/_update_protected_tag.haml index 802a5ef3b98..62823bee46e 100644 --- a/app/views/projects/protected_tags/_update_protected_tag.haml +++ b/app/views/projects/protected_tags/_update_protected_tag.haml @@ -1,5 +1,5 @@ %td - = hidden_field_tag "allowed_to_push_#{protected_tag.id}", protected_tag.push_access_levels.first.access_level - = dropdown_tag( (protected_tag.push_access_levels.first.humanize || 'Select') , - options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container', - data: { field_name: "allowed_to_push_#{protected_tag.id}", access_level_id: protected_tag.push_access_levels.first.id }}) + = hidden_field_tag "allowed_to_create_#{protected_tag.id}", protected_tag.create_access_levels.first.access_level + = dropdown_tag( (protected_tag.create_access_levels.first.humanize || 'Select') , + options: { toggle_class: 'js-allowed-to-create', dropdown_class: 'dropdown-menu-selectable js-allowed-to-create-container', + data: { field_name: "allowed_to_create_#{protected_tag.id}", access_level_id: protected_tag.create_access_levels.first.id }}) diff --git a/db/migrate/20170309173138_create_protected_tags.rb b/db/migrate/20170309173138_create_protected_tags.rb index 00794529143..538f28479c7 100644 --- a/db/migrate/20170309173138_create_protected_tags.rb +++ b/db/migrate/20170309173138_create_protected_tags.rb @@ -15,14 +15,14 @@ class CreateProtectedTags < ActiveRecord::Migration add_index :protected_tags, :project_id - create_table :protected_tag_push_access_levels do |t| - t.references :protected_tag, index: { name: "index_protected_tag_push_access" }, foreign_key: true, null: false + create_table :protected_tag_create_access_levels do |t| + t.references :protected_tag, index: { name: "index_protected_tag_create_access" }, foreign_key: true, null: false t.integer :access_level, default: GITLAB_ACCESS_MASTER, null: true t.references :user, foreign_key: true, index: true - t.integer :group_id#TODO: Should this have an index? Doesn't appear in brances #, index: true + t.integer :group_id t.timestamps null: false end - add_foreign_key :protected_tag_push_access_levels, :namespaces, column: :group_id # rubocop: disable Migration/AddConcurrentForeignKey + add_foreign_key :protected_tag_create_access_levels, :namespaces, column: :group_id # rubocop: disable Migration/AddConcurrentForeignKey end end diff --git a/db/schema.rb b/db/schema.rb index 650b18bb013..2a070583834 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -963,7 +963,7 @@ ActiveRecord::Schema.define(version: 20170315194013) do add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree - create_table "protected_tag_push_access_levels", force: :cascade do |t| + create_table "protected_tag_create_access_levels", force: :cascade do |t| t.integer "protected_tag_id", null: false t.integer "access_level", default: 40 t.integer "user_id" @@ -972,8 +972,8 @@ ActiveRecord::Schema.define(version: 20170315194013) do t.datetime "updated_at", null: false end - add_index "protected_tag_push_access_levels", ["protected_tag_id"], name: "index_protected_tag_push_access", using: :btree - add_index "protected_tag_push_access_levels", ["user_id"], name: "index_protected_tag_push_access_levels_on_user_id", using: :btree + add_index "protected_tag_create_access_levels", ["protected_tag_id"], name: "index_protected_tag_create_access", using: :btree + add_index "protected_tag_create_access_levels", ["user_id"], name: "index_protected_tag_create_access_levels_on_user_id", using: :btree create_table "protected_tags", force: :cascade do |t| t.integer "project_id", null: false @@ -1326,9 +1326,9 @@ ActiveRecord::Schema.define(version: 20170315194013) do add_foreign_key "project_statistics", "projects", on_delete: :cascade add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_push_access_levels", "protected_branches" - add_foreign_key "protected_tag_push_access_levels", "namespaces", column: "group_id" - add_foreign_key "protected_tag_push_access_levels", "protected_tags" - add_foreign_key "protected_tag_push_access_levels", "users" + add_foreign_key "protected_tag_create_access_levels", "namespaces", column: "group_id" + add_foreign_key "protected_tag_create_access_levels", "protected_tags" + add_foreign_key "protected_tag_create_access_levels", "users" add_foreign_key "subscriptions", "projects", on_delete: :cascade add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 7bf5568a8ee..25e03ec64d3 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -47,7 +47,7 @@ project_tree: - :merge_access_levels - :push_access_levels - protected_tags: - - :push_access_levels + - :create_access_levels - :project_feature # Only include the following attributes for the models specified. diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index b1d8e385f2e..430de9a1bf8 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -9,7 +9,7 @@ module Gitlab hooks: 'ProjectHook', merge_access_levels: 'ProtectedBranch::MergeAccessLevel', push_access_levels: 'ProtectedBranch::PushAccessLevel', - #TODO: How to add?- push_access_levels: 'ProtectedTag::PushAccessLevel', + create_access_levels: 'ProtectedTag::CreateAccessLevel', labels: :project_labels, priorities: :label_priorities, label: :project_label }.freeze diff --git a/spec/factories/protected_tags.rb b/spec/factories/protected_tags.rb index f0016b37d66..d8e90ae1ee1 100644 --- a/spec/factories/protected_tags.rb +++ b/spec/factories/protected_tags.rb @@ -4,18 +4,18 @@ FactoryGirl.define do project after(:build) do |protected_tag| - protected_tag.push_access_levels.new(access_level: Gitlab::Access::MASTER) + protected_tag.create_access_levels.new(access_level: Gitlab::Access::MASTER) end - trait :developers_can_push do + trait :developers_can_create do after(:create) do |protected_tag| - protected_tag.push_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER) + protected_tag.create_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER) end end - trait :no_one_can_push do + trait :no_one_can_create do after(:create) do |protected_tag| - protected_tag.push_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS) + protected_tag.create_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS) end end end diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb index d7152926629..33a07786007 100644 --- a/spec/features/protected_tags/access_control_ce_spec.rb +++ b/spec/features/protected_tags/access_control_ce_spec.rb @@ -1,23 +1,23 @@ RSpec.shared_examples "protected tags > access control > CE" do - ProtectedTag::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| - it "allows creating protected tags that #{access_type_name} can push to" do + ProtectedTag::CreateAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected tags that #{access_type_name} can create" do visit namespace_project_protected_tags_path(project.namespace, project) set_protected_tag_name('master') within('.new_protected_tag') do - allowed_to_push_button = find(".js-allowed-to-push") + allowed_to_create_button = find(".js-allowed-to-create") - unless allowed_to_push_button.text == access_type_name - allowed_to_push_button.click + unless allowed_to_create_button.text == access_type_name + allowed_to_create_button.click within(".dropdown.open .dropdown-menu") { click_on access_type_name } end end click_on "Protect" expect(ProtectedTag.count).to eq(1) - expect(ProtectedTag.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) + expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to eq([access_type_id]) end - it "allows updating protected tags so that #{access_type_name} can push to them" do + it "allows updating protected tags so that #{access_type_name} can create them" do visit namespace_project_protected_tags_path(project.namespace, project) set_protected_tag_name('master') click_on "Protect" @@ -25,16 +25,16 @@ RSpec.shared_examples "protected tags > access control > CE" do expect(ProtectedTag.count).to eq(1) within(".protected-tags-list") do - find(".js-allowed-to-push").click + find(".js-allowed-to-create").click - within('.js-allowed-to-push-container') do + within('.js-allowed-to-create-container') do expect(first("li")).to have_content("Roles") click_on access_type_name end end wait_for_ajax - expect(ProtectedTag.last.push_access_levels.map(&:access_level)).to include(access_type_id) + expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to include(access_type_id) end end end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index afc29baa7e6..959ae02c222 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -86,7 +86,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do end context 'when user has access' do - let!(:protected_tag) { create(:protected_tag, :developers_can_push, project: project, name: 'v*') } + let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') } it 'allows tag creation' do expect(subject.status).to be(true) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 83503c73e75..53cfd674b02 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -113,11 +113,12 @@ protected_branches: - push_access_levels protected_tags: - project -- push_access_levels +- create_access_levels merge_access_levels: - protected_branch push_access_levels: - protected_branch +create_access_levels: - protected_tag project: - taggings diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index c69ff3446ea..611cdbbc865 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -189,7 +189,7 @@ describe Gitlab::UserAccess, lib: true do describe 'push to protected tag if allowed for developers' do before do - @tag = create(:protected_tag, :developers_can_push, project: project) + @tag = create(:protected_tag, :developers_can_create, project: project) end it 'returns true if user is a master' do diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb index 70ea96a954f..d91a58e8de5 100644 --- a/spec/services/protected_tags/create_service_spec.rb +++ b/spec/services/protected_tags/create_service_spec.rb @@ -6,7 +6,7 @@ describe ProtectedTags::CreateService, services: true do let(:params) do { name: 'master', - push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }] + create_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }] } end @@ -15,7 +15,7 @@ describe ProtectedTags::CreateService, services: true do it 'creates a new protected tag' do expect { service.execute }.to change(ProtectedTag, :count).by(1) - expect(project.protected_tags.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) + expect(project.protected_tags.last.create_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) end end end From d85471ac1a4574053b057dd7cc02858d591a8ffd Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 4 Apr 2017 03:50:15 +0100 Subject: [PATCH 39/54] Fixed UserAccess#can_create_tag? after create_access_levels rename --- app/models/concerns/protected_ref.rb | 4 ++-- lib/gitlab/user_access.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index ab28eb19b64..7c0183952a0 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -8,7 +8,7 @@ module ProtectedRef delegate :matching, :matches?, :wildcard?, to: :ref_matcher - def self.protected_ref_accessible_to?(ref, user, action: :push) + def self.protected_ref_accessible_to?(ref, user, action:) access_levels_for_ref(ref, action: action).any? do |access_level| access_level.check_access(user) end @@ -20,7 +20,7 @@ module ProtectedRef end end - def self.access_levels_for_ref(ref, action: :push) + def self.access_levels_for_ref(ref, action:) self.matching(ref).map(&:"#{action}_access_levels").flatten end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 6af5de4dc08..54728e5ff0e 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -32,7 +32,7 @@ module Gitlab return false unless can_access_git? if ProtectedTag.protected?(project, ref) - project.protected_tags.protected_ref_accessible_to?(ref, user) + project.protected_tags.protected_ref_accessible_to?(ref, user, action: :create) else user.can?(:push_code, project) end From 9f9a7a2d90f6775b42bef308ba72d9f59345f50f Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 4 Apr 2017 15:20:32 +0100 Subject: [PATCH 40/54] Added ProtectedTag to import/export son and safe_model_attributes --- spec/lib/gitlab/import_export/project.json | 18 ++++++++++++++++++ .../project_tree_restorer_spec.rb | 4 ++++ .../import_export/safe_model_attributes.yml | 14 ++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index d9b67426818..7a0b0b06d4b 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -7455,6 +7455,24 @@ ] } ], + "protected_tags": [ + { + "id": 1, + "project_id": 9, + "name": "v*", + "created_at": "2017-04-04T13:48:13.426Z", + "updated_at": "2017-04-04T13:48:13.426Z", + "create_access_levels": [ + { + "id": 1, + "protected_tag_id": 1, + "access_level": 40, + "created_at": "2017-04-04T13:48:13.458Z", + "updated_at": "2017-04-04T13:48:13.458Z" + } + ] + } + ], "project_feature": { "builds_access_level": 0, "created_at": "2014-12-26T09:26:45.000Z", diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index c36f12dbd82..96d5ff414dc 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -64,6 +64,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(ProtectedBranch.first.push_access_levels).not_to be_empty end + it 'contains the create access levels on a protected tag' do + expect(ProtectedTag.first.create_access_levels).not_to be_empty + end + context 'event at forth level of the tree' do let(:event) { Event.where(title: 'test levels').first } diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 1ad16a9b57d..0c315ac672e 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -300,6 +300,12 @@ ProtectedBranch: - name - created_at - updated_at +ProtectedTag: +- id +- project_id +- name +- created_at +- updated_at Project: - description - issues_enabled @@ -333,6 +339,14 @@ ProtectedBranch::PushAccessLevel: - access_level - created_at - updated_at +ProtectedTag::CreateAccessLevel: +- id +- protected_tag_id +- access_level +- created_at +- updated_at +- user_id +- group_id AwardEmoji: - id - user_id From 678672b0664d94d33b606d8ad13e1333cad2a5be Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 6 Apr 2017 14:16:52 +0530 Subject: [PATCH 41/54] Bundle renamed to `index.js` --- .../protected_tags/{protected_tags_bundle.js => index.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/protected_tags/{protected_tags_bundle.js => index.js} (100%) diff --git a/app/assets/javascripts/protected_tags/protected_tags_bundle.js b/app/assets/javascripts/protected_tags/index.js similarity index 100% rename from app/assets/javascripts/protected_tags/protected_tags_bundle.js rename to app/assets/javascripts/protected_tags/index.js From 115d4a41061a94c294fa7cbd218a983ef06e0965 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 6 Apr 2017 14:17:19 +0530 Subject: [PATCH 42/54] Convert to pure ES6 class --- .../protected_tag_access_dropdown.js | 51 ++++++----- .../protected_tags/protected_tag_create.js | 70 +++++++-------- .../protected_tags/protected_tag_dropdown.js | 12 +-- .../protected_tags/protected_tag_edit.js | 86 +++++++++---------- .../protected_tags/protected_tag_edit_list.js | 29 +++---- 5 files changed, 117 insertions(+), 131 deletions(-) diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js index b85c2991dd9..681b060f859 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js +++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js @@ -1,29 +1,26 @@ -/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, max-len */ +export default class ProtectedTagAccessDropdown { + constructor(options) { + this.options = options; + this.initDropdown(); + } -(global => { - global.gl = global.gl || {}; - - gl.ProtectedTagAccessDropdown = class { - constructor(options) { - const { $dropdown, data, onSelect } = options; - - $dropdown.glDropdown({ - data: data, - selectable: true, - inputId: $dropdown.data('input-id'), - fieldName: $dropdown.data('field-name'), - toggleLabel(item, el) { - if (el.is('.is-active')) { - return item.text; - } else { - return 'Select'; - } - }, - clicked(item, $el, e) { - e.preventDefault(); - onSelect(); + initDropdown() { + const { onSelect } = this.options; + this.options.$dropdown.glDropdown({ + data: this.options.data, + selectable: true, + inputId: this.options.$dropdown.data('input-id'), + fieldName: this.options.$dropdown.data('field-name'), + toggleLabel(item, el) { + if (el.is('.is-active')) { + return item.text; } - }); - } - }; -})(window); + return 'Select'; + }, + clicked(item, $el, e) { + e.preventDefault(); + onSelect(); + }, + }); + } +} diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 84b1b232649..964e67c9de0 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -1,45 +1,41 @@ -/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */ -/* global ProtectedTagDropdown */ +import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; +import ProtectedTagDropdown from './protected_tag_dropdown'; -(global => { - global.gl = global.gl || {}; +export default class ProtectedTagCreate { + constructor() { + this.$form = $('.new_protected_tag'); + this.buildDropdowns(); + } - gl.ProtectedTagCreate = class { - constructor() { - this.$wrap = this.$form = $('.new_protected_tag'); - this.buildDropdowns(); - } + buildDropdowns() { + const $allowedToCreateDropdown = this.$form.find('.js-allowed-to-create'); - buildDropdowns() { - const $allowedToCreateDropdown = this.$wrap.find('.js-allowed-to-create'); + // Cache callback + this.onSelectCallback = this.onSelect.bind(this); - // Cache callback - this.onSelectCallback = this.onSelect.bind(this); + // Allowed to Create dropdown + this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({ + $dropdown: $allowedToCreateDropdown, + data: gon.create_access_levels, + onSelect: this.onSelectCallback, + }); - // Allowed to Create dropdown - new gl.ProtectedTagAccessDropdown({ - $dropdown: $allowedToCreateDropdown, - data: gon.create_access_levels, - onSelect: this.onSelectCallback - }); + // Select default + $allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0); - // Select default - $allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0); + // Protected tag dropdown + this.protectedTagDropdown = new ProtectedTagDropdown({ + $dropdown: this.$form.find('.js-protected-tag-select'), + onSelect: this.onSelectCallback, + }); + } - // Protected tag dropdown - new ProtectedTagDropdown({ - $dropdown: this.$wrap.find('.js-protected-tag-select'), - onSelect: this.onSelectCallback - }); - } + // This will run after clicked callback + onSelect() { + // Enable submit button + const $tagInput = this.$form.find('input[name="protected_tag[name]"]'); + const $allowedToCreateInput = this.$form.find('input[name="protected_tag[create_access_levels_attributes][0][access_level]"]'); - // This will run after clicked callback - onSelect() { - // Enable submit button - const $tagInput = this.$wrap.find('input[name="protected_tag[name]"]'); - const $allowedToCreateInput = this.$wrap.find('input[name="protected_tag[create_access_levels_attributes][0][access_level]"]'); - - this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length)); - } - }; -})(window); + this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length)); + } +} diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js index ccc4c81fa18..9be9e2bea6f 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js +++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js @@ -1,6 +1,4 @@ -/* eslint-disable comma-dangle, no-unused-vars */ - -class ProtectedTagDropdown { +export default class ProtectedTagDropdown { constructor(options) { this.onSelect = options.onSelect; this.$dropdown = options.$dropdown; @@ -21,7 +19,7 @@ class ProtectedTagDropdown { filterable: true, remote: false, search: { - fields: ['title'] + fields: ['title'], }, selectable: true, toggleLabel(selected) { @@ -38,7 +36,7 @@ class ProtectedTagDropdown { clicked: (item, $el, e) => { e.preventDefault(); this.onSelect(); - } + }, }); } @@ -63,7 +61,7 @@ class ProtectedTagDropdown { this.selectedTag = { title: tagName, id: tagName, - text: tagName + text: tagName, }; if (tagName) { @@ -75,5 +73,3 @@ class ProtectedTagDropdown { this.$dropdownFooter.toggleClass('hidden', !tagName); } } - -window.ProtectedTagDropdown = ProtectedTagDropdown; diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index 0227be35c8f..b5092877138 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -1,54 +1,52 @@ -/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */ +/* eslint-disable no-new */ /* global Flash */ -(global => { - global.gl = global.gl || {}; +import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; - gl.ProtectedTagEdit = class { - constructor(options) { - this.$wrap = options.$wrap; - this.$allowedToCreateDropdown = this.$wrap.find('.js-allowed-to-create'); +export default class ProtectedTagEdit { + constructor(options) { + this.$wrap = options.$wrap; + this.$allowedToCreateDropdown = this.$wrap.find('.js-allowed-to-create'); - this.buildDropdowns(); - } + this.buildDropdowns(); + } - buildDropdowns() { - // Allowed to create dropdown - new gl.ProtectedTagAccessDropdown({ - $dropdown: this.$allowedToCreateDropdown, - data: gon.create_access_levels, - onSelect: this.onSelect.bind(this) - }); - } + buildDropdowns() { + // Allowed to create dropdown + this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({ + $dropdown: this.$allowedToCreateDropdown, + data: gon.create_access_levels, + onSelect: this.onSelect.bind(this), + }); + } - onSelect() { - const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdown.data('fieldName')}"]`); + onSelect() { + const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdown.data('fieldName')}"]`); - // Do not update if one dropdown has not selected any option - if (!$allowedToCreateInput.length) return; + // Do not update if one dropdown has not selected any option + if (!$allowedToCreateInput.length) return; - this.$allowedToCreateDropdown.disable(); + this.$allowedToCreateDropdown.disable(); - $.ajax({ - type: 'POST', - url: this.$wrap.data('url'), - dataType: 'json', - data: { - _method: 'PATCH', - protected_tag: { - create_access_levels_attributes: [{ - id: this.$allowedToCreateDropdown.data('access-level-id'), - access_level: $allowedToCreateInput.val() - }] - } + $.ajax({ + type: 'POST', + url: this.$wrap.data('url'), + dataType: 'json', + data: { + _method: 'PATCH', + protected_tag: { + create_access_levels_attributes: [{ + id: this.$allowedToCreateDropdown.data('access-level-id'), + access_level: $allowedToCreateInput.val(), + }], }, - error() { - $.scrollTo(0); - new Flash('Failed to update tag!'); - } - }).always(() => { - this.$allowedToCreateDropdown.enable(); - }); - } - }; -})(window); + }, + error() { + $.scrollTo(0); + new Flash('Failed to update tag!'); + }, + }).always(() => { + this.$allowedToCreateDropdown.enable(); + }); + } +} diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js index ba40c227ef4..88c7accdec6 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js @@ -1,18 +1,17 @@ -/* eslint-disable arrow-parens, no-param-reassign, no-new, comma-dangle */ +import ProtectedTagEdit from './protected_tag_edit'; -(global => { - global.gl = global.gl || {}; +export default class ProtectedTagEditList { + constructor() { + this.$wrap = $('.protected-tags-list'); + this.protectedTagList = []; + this.initEditForm(); + } - gl.ProtectedTagEditList = class { - constructor() { - this.$wrap = $('.protected-tags-list'); - - // Build edit forms - this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => { - new gl.ProtectedTagEdit({ - $wrap: $(el) - }); + initEditForm() { + this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => { + this.protectedTagList[i] = new ProtectedTagEdit({ + $wrap: $(el), }); - } - }; -})(window); + }); + } +} From 92b75704c2b6e827d333b85ab3d1d99aed84cb6f Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 6 Apr 2017 14:17:41 +0530 Subject: [PATCH 43/54] Re-export classes using ES6 `export` --- app/assets/javascripts/protected_tags/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js index 889a8053e6f..da036f5437e 100644 --- a/app/assets/javascripts/protected_tags/index.js +++ b/app/assets/javascripts/protected_tags/index.js @@ -1,5 +1,5 @@ -require('./protected_tag_access_dropdown'); -require('./protected_tag_create'); -require('./protected_tag_dropdown'); -require('./protected_tag_edit'); -require('./protected_tag_edit_list'); +export { default as ProtectedTagAccessDropdown } from './protected_tag_access_dropdown'; +export { default as ProtectedTagCreate } from './protected_tag_create'; +export { default as ProtectedTagDropdown } from './protected_tag_dropdown'; +export { default as ProtectedTagEdit } from './protected_tag_edit'; +export { default as ProtectedTagEditList } from './protected_tag_edit_list'; From 59be20f06fa6d4a98dac5b45c3dee2aeebe813e2 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 6 Apr 2017 14:18:03 +0530 Subject: [PATCH 44/54] Import Protected Tags classes --- app/assets/javascripts/dispatcher.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d384927cc5b..4dc82bee6c3 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -41,6 +41,7 @@ import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; +import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; const ShortcutsBlob = require('./shortcuts_blob'); const UserCallout = require('./user_callout'); @@ -322,8 +323,8 @@ const UserCallout = require('./user_callout'); new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); // Initialize Protected Tag Settings - new gl.ProtectedTagCreate(); - new gl.ProtectedTagEditList(); + new ProtectedTagCreate(); + new ProtectedTagEditList(); break; case 'projects:ci_cd:show': new gl.ProjectVariables(); From 3c414a9fa92e48ca021c1671fca9ad096a34d1c4 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 6 Apr 2017 14:18:17 +0530 Subject: [PATCH 45/54] Update bundle path for Protected Tags bundle --- config/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index d861fa0c7a4..d5580650545 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -39,7 +39,7 @@ var config = { network: './network/network_bundle.js', profile: './profile/profile_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js', - protected_tags: './protected_tags/protected_tags_bundle.js', + protected_tags: './protected_tags', snippet: './snippet/snippet_bundle.js', terminal: './terminal/terminal_bundle.js', u2f: ['vendor/u2f'], From cd5b36d04e79ed8fcd649127e0d47e09ec325242 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 6 Apr 2017 14:47:50 +0530 Subject: [PATCH 46/54] Add constructor doc, toggle helper --- .../protected_tags/protected_tag_dropdown.js | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js index 9be9e2bea6f..9c78f2816a4 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js +++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js @@ -1,4 +1,10 @@ export default class ProtectedTagDropdown { + /** + * @param {Object} options containing + * `$dropdown` target element + * `onSelect` event callback + * $dropdown must be an element created using `dropdown_tag()` rails helper + */ constructor(options) { this.onSelect = options.onSelect; this.$dropdown = options.$dropdown; @@ -10,7 +16,7 @@ export default class ProtectedTagDropdown { this.bindEvents(); // Hide footer - this.$dropdownFooter.addClass('hidden'); + this.toggleFooter(true); } buildDropdown() { @@ -58,18 +64,22 @@ export default class ProtectedTagDropdown { } toggleCreateNewButton(tagName) { - this.selectedTag = { - title: tagName, - id: tagName, - text: tagName, - }; - if (tagName) { + this.selectedTag = { + title: tagName, + id: tagName, + text: tagName, + }; + this.$dropdownContainer .find('.create-new-protected-tag code') .text(tagName); } - this.$dropdownFooter.toggleClass('hidden', !tagName); + this.toggleFooter(!tagName); + } + + toggleFooter(toggleState) { + this.$dropdownFooter.toggleClass('hidden', toggleState); } } From f16377e7dc762462817dd0b34e36811c55988b10 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Wed, 5 Apr 2017 18:59:46 +0100 Subject: [PATCH 47/54] Protected Tags backend review changes Added changelog --- .../projects/protected_branches_controller.rb | 18 +++---------- .../projects/protected_refs_controller.rb | 21 +++++++-------- .../projects/protected_tags_controller.rb | 18 +++---------- app/helpers/branches_helper.rb | 4 +++ .../concerns/protected_branch_access.rb | 1 + app/models/concerns/protected_ref.rb | 1 + app/models/concerns/protected_tag_access.rb | 1 + app/models/project.rb | 2 +- app/models/protectable_dropdown.rb | 11 ++++++-- .../protected_branches/update_service.rb | 7 ++--- app/services/protected_tags/update_service.rb | 7 ++--- app/views/projects/branches/_branch.html.haml | 2 +- .../protected_branches/show.html.haml | 8 +++--- .../projects/protected_tags/show.html.haml | 8 +++--- ...471-restrict-tag-pushes-protected-tags.yml | 4 +++ .../20170309173138_create_protected_tags.rb | 1 - lib/gitlab/checks/change_access.rb | 10 ++----- .../protected_branches_controller_spec.rb | 1 + .../protected_tags_controller_spec.rb | 1 + .../access_control_ce_spec.rb | 12 +++++++++ .../protected_tags/access_control_ce_spec.rb | 6 +++++ spec/models/protectable_dropdown_spec.rb | 1 + spec/models/protected_tag_spec.rb | 2 -- .../protected_branches/update_service_spec.rb | 26 +++++++++++++++++++ .../protected_tags/update_service_spec.rb | 26 +++++++++++++++++++ 25 files changed, 125 insertions(+), 74 deletions(-) create mode 100644 changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml create mode 100644 spec/services/protected_branches/update_service_spec.rb create mode 100644 spec/services/protected_tags/update_service_spec.rb diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index c2a55c9500a..ba24fa9acfe 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,32 +1,20 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController protected - def protected_ref - @protected_branch - end - - def protected_ref=(val) - @protected_branch = val - end - - def matching_refs=(val) - @matching_branches = val - end - def project_refs @project.repository.branches end - def create_service + def create_service_class ::ProtectedBranches::CreateService end - def update_service + def update_service_class ::ProtectedBranches::UpdateService end def load_protected_ref - self.protected_ref = @project.protected_branches.find(params[:id]) + @protected_ref = @project.protected_branches.find(params[:id]) end def protected_ref_params diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb index 63f005124a9..083a70968e5 100644 --- a/app/controllers/projects/protected_refs_controller.rb +++ b/app/controllers/projects/protected_refs_controller.rb @@ -1,5 +1,6 @@ class Projects::ProtectedRefsController < Projects::ApplicationController include RepositorySettingsRedirect + # Authorize before_action :require_non_empty_project before_action :authorize_admin_project! @@ -12,33 +13,31 @@ class Projects::ProtectedRefsController < Projects::ApplicationController end def create - self.protected_ref = create_service.new(@project, current_user, protected_ref_params).execute + protected_ref = create_service_class.new(@project, current_user, protected_ref_params).execute + unless protected_ref.persisted? flash[:alert] = protected_ref.errors.full_messages.join(', ').html_safe end + redirect_to_repository_settings(@project) end def show - self.matching_refs = protected_ref.matching(project_refs) + @matching_refs = @protected_ref.matching(project_refs) end def update - self.protected_ref = update_service.new(@project, current_user, protected_ref_params).execute(protected_ref) + @protected_ref = update_service_class.new(@project, current_user, protected_ref_params).execute(@protected_ref) - if protected_ref.valid? - respond_to do |format| - format.json { render json: protected_ref, status: :ok } - end + if @protected_ref.valid? + render json: @protected_ref, status: :ok else - respond_to do |format| - format.json { render json: protected_ref.errors, status: :unprocessable_entity } - end + render json: @protected_ref.errors, status: :unprocessable_entity end end def destroy - protected_ref.destroy + @protected_ref.destroy respond_to do |format| format.html { redirect_to_repository_settings(@project) } diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb index 0e00baedbdf..c61ddf145e6 100644 --- a/app/controllers/projects/protected_tags_controller.rb +++ b/app/controllers/projects/protected_tags_controller.rb @@ -1,32 +1,20 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController protected - def protected_ref - @protected_tag - end - - def protected_ref=(val) - @protected_tag = val - end - - def matching_refs=(val) - @matching_tags = val - end - def project_refs @project.repository.tags end - def create_service + def create_service_class ::ProtectedTags::CreateService end - def update_service + def update_service_class ::ProtectedTags::UpdateService end def load_protected_ref - self.protected_ref = @project.protected_tags.find(params[:id]) + @protected_ref = @project.protected_tags.find(params[:id]) end def protected_ref_params diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index a852b90c57e..b7a28b1b4a7 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -29,4 +29,8 @@ module BranchesHelper def project_branches options_for_select(@project.repository.branch_names, @project.default_branch) end + + def protected_branch?(project, branch) + ProtectedBranch.protected?(project, branch.name) + end end diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 06cae00249a..c41b807df8a 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -5,6 +5,7 @@ module ProtectedBranchAccess include ProtectedRefAccess belongs_to :protected_branch + delegate :project, to: :protected_branch end end diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index 7c0183952a0..62eaec2407f 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -3,6 +3,7 @@ module ProtectedRef included do belongs_to :project + validates :name, presence: true validates :project, presence: true diff --git a/app/models/concerns/protected_tag_access.rb b/app/models/concerns/protected_tag_access.rb index 9b7d31a6fd5..ee65de24dd8 100644 --- a/app/models/concerns/protected_tag_access.rb +++ b/app/models/concerns/protected_tag_access.rb @@ -5,6 +5,7 @@ module ProtectedTagAccess include ProtectedRefAccess belongs_to :protected_tag + delegate :project, to: :protected_tag end end diff --git a/app/models/project.rb b/app/models/project.rb index 2469e6f8523..2c631050042 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -134,7 +134,7 @@ class Project < ActiveRecord::Base has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet' has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy - has_many :protected_tags, dependent: :destroy + has_many :protected_tags, dependent: :destroy has_many :project_authorizations has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb index c9b2b213cd2..122fbce257d 100644 --- a/app/models/protectable_dropdown.rb +++ b/app/models/protectable_dropdown.rb @@ -6,8 +6,7 @@ class ProtectableDropdown # Tags/branches which are yet to be individually protected def protectable_ref_names - non_wildcard_protections = protections.reject(&:wildcard?) - refs.map(&:name) - non_wildcard_protections.map(&:name) + @protectable_ref_names ||= ref_names - non_wildcard_protected_ref_names end def hash @@ -20,7 +19,15 @@ class ProtectableDropdown @project.repository.public_send(@ref_type) end + def ref_names + refs.map(&:name) + end + def protections @project.public_send("protected_#{@ref_type}") end + + def non_wildcard_protected_ref_names + protections.reject(&:wildcard?).map(&:name) + end end diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb index 89d8ba60134..4b3337a5c9d 100644 --- a/app/services/protected_branches/update_service.rb +++ b/app/services/protected_branches/update_service.rb @@ -1,13 +1,10 @@ module ProtectedBranches class UpdateService < BaseService - attr_reader :protected_branch - def execute(protected_branch) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) - @protected_branch = protected_branch - @protected_branch.update(params) - @protected_branch + protected_branch.update(params) + protected_branch end end end diff --git a/app/services/protected_tags/update_service.rb b/app/services/protected_tags/update_service.rb index 8a2419efd7b..aea6a48968d 100644 --- a/app/services/protected_tags/update_service.rb +++ b/app/services/protected_tags/update_service.rb @@ -1,13 +1,10 @@ module ProtectedTags class UpdateService < BaseService - attr_reader :protected_tag - def execute(protected_tag) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) - @protected_tag = protected_tag - @protected_tag.update(params) - @protected_tag + protected_tag.update(params) + protected_tag end end end diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index d84fa9e55c0..931a5f920d3 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -15,7 +15,7 @@ %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" } merged - - if ProtectedBranch.protected?(@project, branch.name) + - if protected_branch?(@project, branch) %span.label.label-success protected .controls.hidden-xs< diff --git a/app/views/projects/protected_branches/show.html.haml b/app/views/projects/protected_branches/show.html.haml index 4d8169815b3..f8cfe5e4b11 100644 --- a/app/views/projects/protected_branches/show.html.haml +++ b/app/views/projects/protected_branches/show.html.haml @@ -1,13 +1,13 @@ -- page_title @protected_branch.name, "Protected Branches" +- page_title @protected_ref.name, "Protected Branches" .row.prepend-top-default.append-bottom-default .col-lg-3 %h4.prepend-top-0 - = @protected_branch.name + = @protected_ref.name .col-lg-9 %h5 Matching Branches - - if @matching_branches.present? + - if @matching_refs.present? .table-responsive %table.table.protected-branches-list %colgroup @@ -18,7 +18,7 @@ %th Branch %th Last commit %tbody - - @matching_branches.each do |matching_branch| + - @matching_refs.each do |matching_branch| = render partial: "matching_branch", object: matching_branch - else %p.settings-message.text-center diff --git a/app/views/projects/protected_tags/show.html.haml b/app/views/projects/protected_tags/show.html.haml index 185807a7e8d..63743f28b3c 100644 --- a/app/views/projects/protected_tags/show.html.haml +++ b/app/views/projects/protected_tags/show.html.haml @@ -1,13 +1,13 @@ -- page_title @protected_tag.name, "Protected Tags" +- page_title @protected_ref.name, "Protected Tags" .row.prepend-top-default.append-bottom-default .col-lg-3 %h4.prepend-top-0 - = @protected_tag.name + = @protected_ref.name .col-lg-9 %h5 Matching Tags - - if @matching_tags.present? + - if @matching_refs.present? .table-responsive %table.table.protected-tags-list %colgroup @@ -18,7 +18,7 @@ %th Tag %th Last commit %tbody - - @matching_tags.each do |matching_tag| + - @matching_refs.each do |matching_tag| = render partial: "matching_tag", object: matching_tag - else %p.settings-message.text-center diff --git a/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml b/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml new file mode 100644 index 00000000000..c6ea5da65a5 --- /dev/null +++ b/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml @@ -0,0 +1,4 @@ +--- +title: Protected Tags feature +merge_request: 10356 +author: diff --git a/db/migrate/20170309173138_create_protected_tags.rb b/db/migrate/20170309173138_create_protected_tags.rb index 538f28479c7..796f3c90344 100644 --- a/db/migrate/20170309173138_create_protected_tags.rb +++ b/db/migrate/20170309173138_create_protected_tags.rb @@ -1,7 +1,6 @@ class CreateProtectedTags < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers - # Set this constant to true if this migration requires downtime. DOWNTIME = false GITLAB_ACCESS_MASTER = 40 diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 0d96c4d41d7..eb2f2e144fd 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -70,14 +70,8 @@ module Gitlab def protected_tag_checks return unless tag_protected? - - if update? - return "Protected tags cannot be updated." - end - - if deletion? - return "Protected tags cannot be deleted." - end + return "Protected tags cannot be updated." if update? + return "Protected tags cannot be deleted." if deletion? unless user_access.can_create_tag?(@tag_name) return "You are not allowed to create this tag as it is protected." diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb index e378b5714fe..80be135b5d8 100644 --- a/spec/controllers/projects/protected_branches_controller_spec.rb +++ b/spec/controllers/projects/protected_branches_controller_spec.rb @@ -3,6 +3,7 @@ require('spec_helper') describe Projects::ProtectedBranchesController do describe "GET #index" do let(:project) { create(:project_empty_repo, :public) } + it "redirects empty repo to projects page" do get(:index, namespace_id: project.namespace.to_param, project_id: project) end diff --git a/spec/controllers/projects/protected_tags_controller_spec.rb b/spec/controllers/projects/protected_tags_controller_spec.rb index ac802981294..64658988b3f 100644 --- a/spec/controllers/projects/protected_tags_controller_spec.rb +++ b/spec/controllers/projects/protected_tags_controller_spec.rb @@ -3,6 +3,7 @@ require('spec_helper') describe Projects::ProtectedTagsController do describe "GET #index" do let(:project) { create(:project_empty_repo, :public) } + it "redirects empty repo to projects page" do get(:index, namespace_id: project.namespace.to_param, project_id: project) end diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb index e4aca25a339..eb3cea775da 100644 --- a/spec/features/protected_branches/access_control_ce_spec.rb +++ b/spec/features/protected_branches/access_control_ce_spec.rb @@ -2,7 +2,9 @@ RSpec.shared_examples "protected branches > access control > CE" do ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can push to" do visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do allowed_to_push_button = find(".js-allowed-to-push") @@ -11,6 +13,7 @@ RSpec.shared_examples "protected branches > access control > CE" do within(".dropdown.open .dropdown-menu") { click_on access_type_name } end end + click_on "Protect" expect(ProtectedBranch.count).to eq(1) @@ -19,7 +22,9 @@ RSpec.shared_examples "protected branches > access control > CE" do it "allows updating protected branches so that #{access_type_name} can push to them" do visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" expect(ProtectedBranch.count).to eq(1) @@ -34,6 +39,7 @@ RSpec.shared_examples "protected branches > access control > CE" do end wait_for_ajax + expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) end end @@ -41,7 +47,9 @@ RSpec.shared_examples "protected branches > access control > CE" do ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can merge to" do visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do allowed_to_merge_button = find(".js-allowed-to-merge") @@ -50,6 +58,7 @@ RSpec.shared_examples "protected branches > access control > CE" do within(".dropdown.open .dropdown-menu") { click_on access_type_name } end end + click_on "Protect" expect(ProtectedBranch.count).to eq(1) @@ -58,7 +67,9 @@ RSpec.shared_examples "protected branches > access control > CE" do it "allows updating protected branches so that #{access_type_name} can merge to them" do visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" expect(ProtectedBranch.count).to eq(1) @@ -73,6 +84,7 @@ RSpec.shared_examples "protected branches > access control > CE" do end wait_for_ajax + expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id) end end diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb index 33a07786007..de2556041e7 100644 --- a/spec/features/protected_tags/access_control_ce_spec.rb +++ b/spec/features/protected_tags/access_control_ce_spec.rb @@ -2,7 +2,9 @@ RSpec.shared_examples "protected tags > access control > CE" do ProtectedTag::CreateAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected tags that #{access_type_name} can create" do visit namespace_project_protected_tags_path(project.namespace, project) + set_protected_tag_name('master') + within('.new_protected_tag') do allowed_to_create_button = find(".js-allowed-to-create") @@ -11,6 +13,7 @@ RSpec.shared_examples "protected tags > access control > CE" do within(".dropdown.open .dropdown-menu") { click_on access_type_name } end end + click_on "Protect" expect(ProtectedTag.count).to eq(1) @@ -19,7 +22,9 @@ RSpec.shared_examples "protected tags > access control > CE" do it "allows updating protected tags so that #{access_type_name} can create them" do visit namespace_project_protected_tags_path(project.namespace, project) + set_protected_tag_name('master') + click_on "Protect" expect(ProtectedTag.count).to eq(1) @@ -34,6 +39,7 @@ RSpec.shared_examples "protected tags > access control > CE" do end wait_for_ajax + expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to include(access_type_id) end end diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb index 7f8ef7195e5..4c9bade592b 100644 --- a/spec/models/protectable_dropdown_spec.rb +++ b/spec/models/protectable_dropdown_spec.rb @@ -18,6 +18,7 @@ describe ProtectableDropdown, models: true do create(:protected_branch, name: 'feat*', project: project) subject = described_class.new(project.reload, :branches) + expect(subject.protectable_ref_names).to include('feature') end end diff --git a/spec/models/protected_tag_spec.rb b/spec/models/protected_tag_spec.rb index 05ad532935a..51353852a93 100644 --- a/spec/models/protected_tag_spec.rb +++ b/spec/models/protected_tag_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe ProtectedTag, models: true do - subject { build_stubbed(:protected_branch) } - describe 'Associations' do it { is_expected.to belong_to(:project) } end diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb new file mode 100644 index 00000000000..62bdd49a4d7 --- /dev/null +++ b/spec/services/protected_branches/update_service_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe ProtectedBranches::UpdateService, services: true do + let(:protected_branch) { create(:protected_branch) } + let(:project) { protected_branch.project } + let(:user) { project.owner } + let(:params) { { name: 'new protected branch name' } } + + describe '#execute' do + subject(:service) { described_class.new(project, user, params) } + + it 'updates a protected branch' do + result = service.execute(protected_branch) + + expect(result.reload.name).to eq(params[:name]) + end + + context 'without admin_project permissions' do + let(:user) { create(:user) } + + it "raises error" do + expect{ service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end + end +end diff --git a/spec/services/protected_tags/update_service_spec.rb b/spec/services/protected_tags/update_service_spec.rb new file mode 100644 index 00000000000..e78fde4c48d --- /dev/null +++ b/spec/services/protected_tags/update_service_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe ProtectedTags::UpdateService, services: true do + let(:protected_tag) { create(:protected_tag) } + let(:project) { protected_tag.project } + let(:user) { project.owner } + let(:params) { { name: 'new protected tag name' } } + + describe '#execute' do + subject(:service) { described_class.new(project, user, params) } + + it 'updates a protected tag' do + result = service.execute(protected_tag) + + expect(result.reload.name).to eq(params[:name]) + end + + context 'without admin_project permissions' do + let(:user) { create(:user) } + + it "raises error" do + expect{ service.execute(protected_tag) }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end + end +end From ca9cded45d78e821a1c778e22fde523873ffaf93 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 6 Apr 2017 19:09:48 +0530 Subject: [PATCH 48/54] Fixes as per feedback --- app/assets/javascripts/protected_tags/index.js | 3 --- .../protected_tag_access_dropdown.js | 4 ++-- .../protected_tags/protected_tag_create.js | 4 ++-- .../protected_tags/protected_tag_dropdown.js | 3 ++- .../protected_tags/protected_tag_edit.js | 16 ++++++++-------- .../protected_tags/protected_tag_edit_list.js | 5 +++-- app/assets/stylesheets/pages/projects.scss | 3 ++- .../_create_protected_tag.html.haml | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js index da036f5437e..61e7ba53862 100644 --- a/app/assets/javascripts/protected_tags/index.js +++ b/app/assets/javascripts/protected_tags/index.js @@ -1,5 +1,2 @@ -export { default as ProtectedTagAccessDropdown } from './protected_tag_access_dropdown'; export { default as ProtectedTagCreate } from './protected_tag_create'; -export { default as ProtectedTagDropdown } from './protected_tag_dropdown'; -export { default as ProtectedTagEdit } from './protected_tag_edit'; export { default as ProtectedTagEditList } from './protected_tag_edit_list'; diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js index 681b060f859..fff83f3af3b 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js +++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js @@ -11,8 +11,8 @@ export default class ProtectedTagAccessDropdown { selectable: true, inputId: this.options.$dropdown.data('input-id'), fieldName: this.options.$dropdown.data('field-name'), - toggleLabel(item, el) { - if (el.is('.is-active')) { + toggleLabel(item, $el) { + if ($el.is('.is-active')) { return item.text; } return 'Select'; diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 964e67c9de0..91bd140bd12 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -3,7 +3,7 @@ import ProtectedTagDropdown from './protected_tag_dropdown'; export default class ProtectedTagCreate { constructor() { - this.$form = $('.new_protected_tag'); + this.$form = $('.js-new-protected-tag'); this.buildDropdowns(); } @@ -34,7 +34,7 @@ export default class ProtectedTagCreate { onSelect() { // Enable submit button const $tagInput = this.$form.find('input[name="protected_tag[name]"]'); - const $allowedToCreateInput = this.$form.find('input[name="protected_tag[create_access_levels_attributes][0][access_level]"]'); + const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes'); this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length)); } diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js index 9c78f2816a4..5ff4e443262 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js +++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js @@ -50,9 +50,10 @@ export default class ProtectedTagDropdown { this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this)); } - onClickCreateWildcard() { + onClickCreateWildcard(e) { this.$dropdown.data('glDropdown').remote.execute(); this.$dropdown.data('glDropdown').selectRowAtIndex(); + e.preventDefault(); } getProtectedTags(term, callback) { diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index b5092877138..624067a5a09 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -6,7 +6,8 @@ import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; export default class ProtectedTagEdit { constructor(options) { this.$wrap = options.$wrap; - this.$allowedToCreateDropdown = this.$wrap.find('.js-allowed-to-create'); + this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create'); + this.onSelectCallback = this.onSelect.bind(this); this.buildDropdowns(); } @@ -14,19 +15,19 @@ export default class ProtectedTagEdit { buildDropdowns() { // Allowed to create dropdown this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({ - $dropdown: this.$allowedToCreateDropdown, + $dropdown: this.$allowedToCreateDropdownButton, data: gon.create_access_levels, - onSelect: this.onSelect.bind(this), + onSelect: this.onSelectCallback, }); } onSelect() { - const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdown.data('fieldName')}"]`); + const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`); // Do not update if one dropdown has not selected any option if (!$allowedToCreateInput.length) return; - this.$allowedToCreateDropdown.disable(); + this.$allowedToCreateDropdownButton.disable(); $.ajax({ type: 'POST', @@ -36,17 +37,16 @@ export default class ProtectedTagEdit { _method: 'PATCH', protected_tag: { create_access_levels_attributes: [{ - id: this.$allowedToCreateDropdown.data('access-level-id'), + id: this.$allowedToCreateDropdownButton.data('access-level-id'), access_level: $allowedToCreateInput.val(), }], }, }, error() { - $.scrollTo(0); new Flash('Failed to update tag!'); }, }).always(() => { - this.$allowedToCreateDropdown.enable(); + this.$allowedToCreateDropdownButton.enable(); }); } } diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js index 88c7accdec6..bd9fc872266 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js @@ -1,15 +1,16 @@ +/* eslint-disable no-new */ + import ProtectedTagEdit from './protected_tag_edit'; export default class ProtectedTagEditList { constructor() { this.$wrap = $('.protected-tags-list'); - this.protectedTagList = []; this.initEditForm(); } initEditForm() { this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => { - this.protectedTagList[i] = new ProtectedTagEdit({ + new ProtectedTagEdit({ $wrap: $(el), }); }); diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 960875674cc..61b973e0aa9 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -779,7 +779,8 @@ pre.light-well { .protected-tags-list { .dropdown-menu-toggle { - width: 300px; + width: 100%; + max-width: 300px; } } diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml index af332f942d6..148efc16e64 100644 --- a/app/views/projects/protected_tags/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new_protected_tag' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'js-new-protected-tag' } do |f| .panel.panel-default .panel-heading %h3.panel-title From 7d17fcea84760d4c8d94b03adc4a23ef733ad4e5 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Thu, 6 Apr 2017 15:47:13 +0100 Subject: [PATCH 49/54] Fix projected import failing on missing relations --- lib/gitlab/import_export/project_tree_restorer.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index df21ff22216..2e349b5f9a9 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -52,7 +52,11 @@ module Gitlab create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash) relation_key = relation.is_a?(Hash) ? relation.keys.first : relation - relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s]) + relation_hash_list = @tree_hash[relation_key.to_s] + + next unless relation_hash_list + + relation_hash = create_relation(relation_key, relation_hash_list) saved << restored_project.append_or_update_attribute(relation_key, relation_hash) end saved.all? From 902054db59e02cb14c28ecffd9dff95994dbb01f Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Thu, 6 Apr 2017 21:21:13 +0100 Subject: [PATCH 50/54] Fix within('#new_protected_tag') in protected tags spec --- spec/features/protected_tags/access_control_ce_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb index de2556041e7..6e2bff82785 100644 --- a/spec/features/protected_tags/access_control_ce_spec.rb +++ b/spec/features/protected_tags/access_control_ce_spec.rb @@ -5,7 +5,7 @@ RSpec.shared_examples "protected tags > access control > CE" do set_protected_tag_name('master') - within('.new_protected_tag') do + within('#new_protected_tag') do allowed_to_create_button = find(".js-allowed-to-create") unless allowed_to_create_button.text == access_type_name From 3dc8c3127bf96a820efc807c86e62fbffc9a5b0d Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 7 Apr 2017 02:24:45 +0530 Subject: [PATCH 51/54] Fix selector for form wrapper --- spec/features/protected_tags/access_control_ce_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb index de2556041e7..5b2baf8616c 100644 --- a/spec/features/protected_tags/access_control_ce_spec.rb +++ b/spec/features/protected_tags/access_control_ce_spec.rb @@ -5,7 +5,7 @@ RSpec.shared_examples "protected tags > access control > CE" do set_protected_tag_name('master') - within('.new_protected_tag') do + within('.js-new-protected-tag') do allowed_to_create_button = find(".js-allowed-to-create") unless allowed_to_create_button.text == access_type_name @@ -31,7 +31,7 @@ RSpec.shared_examples "protected tags > access control > CE" do within(".protected-tags-list") do find(".js-allowed-to-create").click - + within('.js-allowed-to-create-container') do expect(first("li")).to have_content("Roles") click_on access_type_name From 9db87fce130bdcb4831631c46fbb9d5717472af2 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Fri, 7 Apr 2017 01:14:10 +0100 Subject: [PATCH 52/54] Protected tags changes from backend maintainer review --- app/models/concerns/protected_ref_access.rb | 2 +- app/models/protected_ref_matcher.rb | 4 +++- .../unreleased/18471-restrict-tag-pushes-protected-tags.yml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index 0c7e5157cdf..c4f158e569a 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -11,7 +11,7 @@ module ProtectedRefAccess end def check_access(user) - return true if user.is_admin? + return true if user.admin? project.team.max_member_access(user.id) >= access_level end diff --git a/app/models/protected_ref_matcher.rb b/app/models/protected_ref_matcher.rb index 83f44240259..d970f2b01fc 100644 --- a/app/models/protected_ref_matcher.rb +++ b/app/models/protected_ref_matcher.rb @@ -4,7 +4,7 @@ class ProtectedRefMatcher end # Returns all protected refs that match the given ref name. - # This realizes all records from the scope built up so far, and does + # This checks all records from the scope built up so far, and does # _not_ return a relation. # # This method optionally takes in a list of `protected_refs` to search @@ -38,6 +38,8 @@ class ProtectedRefMatcher end def wildcard_match?(ref_name) + return false unless wildcard? + wildcard_regex === ref_name end diff --git a/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml b/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml index c6ea5da65a5..fabe24e485a 100644 --- a/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml +++ b/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml @@ -1,4 +1,4 @@ --- -title: Protected Tags feature +title: Tags can be protected, restricting creation of matching tags by user role merge_request: 10356 author: From 72419f3a8b94874de059f3567980844c7aaa6336 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Fri, 7 Apr 2017 02:35:59 +0100 Subject: [PATCH 53/54] Documentation for protected tags --- .../_create_protected_tag.html.haml | 6 +- doc/university/glossary/README.md | 4 ++ doc/user/permissions.md | 1 + .../img/project_repository_settings.png | Bin 0 -> 35236 bytes .../project/img/protected_tag_matches.png | Bin 0 -> 85305 bytes doc/user/project/img/protected_tags_list.png | Bin 0 -> 24490 bytes doc/user/project/img/protected_tags_page.png | Bin 0 -> 56112 bytes .../protected_tags_permissions_dropdown.png | Bin 0 -> 26514 bytes doc/user/project/protected_tags.md | 60 ++++++++++++++++++ doc/workflow/README.md | 1 + 10 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 doc/user/project/img/project_repository_settings.png create mode 100644 doc/user/project/img/protected_tag_matches.png create mode 100644 doc/user/project/img/protected_tags_list.png create mode 100644 doc/user/project/img/protected_tags_page.png create mode 100644 doc/user/project/img/protected_tags_permissions_dropdown.png create mode 100644 doc/user/project/protected_tags.md diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml index 148efc16e64..6e187b54a59 100644 --- a/app/views/projects/protected_tags/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml @@ -12,11 +12,11 @@ .col-md-10 = render partial: "projects/protected_tags/dropdown", locals: { f: f } .help-block - = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches') + = link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags') such as - %code *-stable + %code v* or - %code production/* + %code *-release are supported .form-group %label.col-md-2.text-right{ for: 'create_access_levels_attributes' } diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index ec565c3e7bf..0b17e4ff7c1 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -411,6 +411,10 @@ An [object-relational](https://en.wikipedia.org/wiki/PostgreSQL) database. Toute A [feature](https://docs.gitlab.com/ce/user/project/protected_branches.html) that protects branches from unauthorized pushes, force pushing or deletion. +### Protected Tags + +A [feature](https://docs.gitlab.com/ce/user/project/protected_tags.html) that protects tags from unauthorized creation, update or deletion + ### Pull Git command to [synchronize](https://git-scm.com/docs/git-pull) the local repository with the remote repository, by fetching all remote changes and merging them into the local repository. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 0ea6d01411f..3122e95fc0e 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -55,6 +55,7 @@ The following table depicts the various user permission levels in a project. | Push to protected branches | | | | ✓ | ✓ | | Enable/disable branch protection | | | | ✓ | ✓ | | Turn on/off protected branch push for devs| | | | ✓ | ✓ | +| Enable/disable tag protections | | | | ✓ | ✓ | | Rewrite/remove Git tags | | | | ✓ | ✓ | | Edit project | | | | ✓ | ✓ | | Add deploy keys to project | | | | ✓ | ✓ | diff --git a/doc/user/project/img/project_repository_settings.png b/doc/user/project/img/project_repository_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..1aa7efc36f1a51ce5caabcc091e854fa6c52f892 GIT binary patch literal 35236 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 literal 0 HcmV?d00001 diff --git a/doc/user/project/img/protected_tag_matches.png b/doc/user/project/img/protected_tag_matches.png new file mode 100644 index 0000000000000000000000000000000000000000..a36a11a1271339268faaa3cab40500acae292f48 GIT binary patch literal 85305 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 z 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 t)Gu^}^}5QW=kRy3K4_v{|71|(g*+m literal 0 HcmV?d00001 diff --git a/doc/user/project/img/protected_tags_list.png b/doc/user/project/img/protected_tags_list.png new file mode 100644 index 0000000000000000000000000000000000000000..c5e42dc0705c850e6e7398e38a30d20b7606201a GIT binary patch literal 24490 zcmcG!b980PzJMFswrxA<*iJgOZQHh;j&0lQ*tV07opkcD_dfTWbNjyc=UZc}HRh_S zpJ&aQv*s5rFDnKMg#`rw001i?F02Ru0L<`p>;M7w^&4f!vI_tJgKjP)BrhQ(L?G{A zYhrF?3;-Y=o~#P4tR#V+qpAo2Ns9nTna3{Rk-Qb@=TwWBfFKx7h@gBinjc^(Ac8{O z4wb*70ibTBR$B|Pavl&7QBQ|vRr;O?GUNQ|@ZmYt`P9YzwDoeCo$hJ^Am3;T8Ju2< z1fU+ErJ(!+o|%v_T;dE6f*%krlQ=S)_&z2I23THn+c(vf{ReAOMM{F&=f!7D{>1K4 zEg(M;q3rHKZ36)x5J2@9T7&~2ffIjM--T8lNKdfZcMwP*eiA7LxsWCj4v~ooi5pE5DV%?BPBB4cMEY%1U(vYDGdenHM^^ zD_cgTVS{?LPDYxaB2-4-d}0_9S)V>>Mm|$2rEg9!UTYQRg#{E6vh+90nM{Hu84hAt zpcVUx2~_-_fpUqPQg^t`_kOG(zWKPs&{tyW0dUOXr07x%)}~MhWg(vP23V^TmC-kf z4ZO7VF%ol#uuxl&@MVyX!oXvPH}FJb^F*}RJH8i)Su4H(_8|-;hv1FLMTV!|2u~l~ z?DrF@__Y#~6LUEYJ_-0_*gls=i{p^)T0z{bD=Y9PO@Jv)k7G_kHyAV&8kPxNBxj-_ zliUpl1oTXsf(lQ%8QFq#BsMhKu)_%g)ZGD33NN&}q(?CI*s!}l5U00Mvx1Aw>l zKMXTjAl6wU_M0|~gZ~H$gLW8x_bTktiUWVp9d4{IAr()4Jd)sL>O~G~u#?#Ub{!bl znTKZ6+`x3jd@!VniIKb~e?<%o0~Bsx#IqXZLS{qU`NYv|HK zvf^;TuoRI!OlL5u2CQd5UME@!%T3p3NWELd)?MtYb6xPL8XU1?eQR{8DI&}j(* zINqsSfX-F6+DPbm3TX=U#@)P4+V)xOh$#@%ssd74(-NB;faKG4^=cnh=yh8ZBenaq zUJh_a5hl6QmMaAiU?c~C-}gzz^cu^ih7|zkN-t8xoe7uD9RUE8qCYzi(8&*wA((R& zNNfkJ)9)JuAR+-6NdRW;nK=P;eGcqVAn6Z?BtKR?;1!tWK3YMErxJUPhFP z0ZBxe@!8_6x6}?WoY7jsT=DDT@N=P^qI`K6k|N&~tQRn{!?+5z%qXw|WyRdh*fL_# z@+)Q>8j&_*mkLT|f1WruVtf0;82}`XV5H%hgmW5ptAV5h6&Vm&5voKb>nE$xE`c1s zQY}S47~ zWupif#5+ic6v`C-E=(+>Q=~ryR?n%FY9_SFD1e$LM&oCgcPT0`+jJw}O1{X$ijghut90#~WMjHOgs zwOJuXeo9fNtW)kK|CSjwA)+p#2TH6+-Z*s}Gg-_j%7*``sNq!VwBz*nq^n%`JMyC5 zqSGSMB5?Vya$pN8i$@E43&#bjg_sIwW`+<(;}fmeTxz2 zXwMoxolU9i3Y|)w5+Ci~iC{_ty2L_;Ric5z-ZEPAT8Z(|Ec( zA2ZL~8l@?1DoIN`RoWWX8i8wEYyNA3zhu@-8{eEUxOBKqxz3$sPwr3WoOzvI+RvTk z&iG8r8N1TcCmfE{kNk2{bmDT8ifg}7I1j_q70?`5ig|r98 zLhfKKVXiR7FgdY{Fs)fESUqg53~+2aC%%G64{w*KmFSj;9^+NYC@V2uXuDnC0|m=raz`6FdycOi`Ay{mJta>n9#{$oI7G z{@()^e};!3Cz4fXuW;Q|GzB^rKiHgiuu-uQvVpS6u+>;kTCX;vHZNb8xd6IEUSR#w zS=C%|{ME9$?eK$J+C}YXX774vc1+_XPVyL<4g?)PRfKMm z4qqEr+ff_7?c>noG-bDW`qwH+qQr(o2RX09va4y^mDAOuo(GBN#r4VK!kv-3iU;34 z)AgR)uN#ju>!a+;uI;y?*rwRglAdX-f2u^HC*l4&-oL7bk9%+;%7R+oMCil$) z&t1{ofa?$l1Vo%Vx;>vl2_u&STct21q7r3kcO+wE)^Fvk8F;+R9=i7(7`bVSjIM?< zhC_9<_7V2;d*%CEBX2{g!>;7g(rR&boC?hGxhrYHk`YpCi7Xt8zGoq2UGq4_s4^Xl zn?|o$r|70;Q=?h-Ud4OTOA`ZQK?lzV-ebAyh?SB{dXjeIDM^6M^q$0Tpd*nDIrAgV z$0-iE#qA|2#eDm1cS|>~b3F6obFVMm@00JUrvj(z^V_DvMi-M9Q&H2EKZ#69O>vP+ zA(%i+O;0B>m@Aab2x*X9Ogl_$NAVDXBOVV3IOiR)9KY`>l*m|7eMoF19>=T3Kk2({ zTnsoS9YY*7PO|297+lW3`@QQP?cS~2X&jwT>N541`acC^L&pbGLTjR7(oktFba2?& zFCvX6M3MHOtDz^Mb!ubvG?vMC*xBzEAp&HCw$ocyao=zd?1NW~zA6 z9--(`)zMK>zjP*a-Of$3eM|l1>7;W0{$??*!?e+8GJia2o>`;%th``7yH;=T^Q|hYN_)Mt zdC_KhOQ-(#Jlsc&CO5F_k}8&J$}ghJ@%0BI*Nw|1-7Md-t%9}^qv_PHfUeE&3g1mD zvdb%;%ztTW2QGzKzCOyWZ`=1?@f%hd|C-_w)gBYDllQuQsTu3p;z02zJ*>UkT_YA1 zYsM+Ur>zz{J6?ikDP(nGCu4KLgXUbCJ6LkMXRp|PWIJb7#Zl$%eg!!c-VNVPd?5#y zgOq#7J<9f3U}`wLOb=pSo|5HU_o2Jr2)+Y6o^JGL$?4qj$$QB^A2LkMqG#1|>D1aj znsK>z4c(4y=5AirwQrwW*`1qR;4HfnPBmRVt?*Hu)`jkNdGUCDn8L1Ox7o~cX?R0B zUTRoRZ_(Wx>tJ?rz2WWe+Iy3H`iW=8Tlzk55!-Gva=Fjz!)py+fWPHE=yShkKea#I z6)a~aH=c9GSLI{!R@p}Xqh|kQ;kb)Fh(6>s@$s@TWvYlSs|{*R~Rlf~=v zWLuW43vbKMW@842>%NXkSD~xOZ)RSluP5(y4@&PPB|;6~68Tbn8QW)_qIbgn@CQXr<`uJOba;x>B<14b{S6hS_aQ#NM{_aJ(8=m8ASff2x_ zDc1Fv1JaiTwlk3 zHPaIj{5{0Ul9xzLMxH>(*1?#7m5zmufrt-^fPjFY-<(W1#}X9KT%SV`O7t;`!Uo|LOXVr~f6Y{vS~e&i^I(Up@aN z`IjqPat`LkUs?Jq7krF7^#7;rU*mb`|4P(Q>e zVF5`RN-kQkHfmJeko*2084x}Qz+gc@WaLDe{S=h6!a|b4kr5%XM1OYDp9#qLMt^(x zAFVsQ2n=T=RLM|iXlOM<6qF2*kdU>FjX`Z~oYV|8gnPh$M%YOYBAC{#KA*%4o6p`u@~qL);4`Z9|PH z3;(Cjt4LqUG+Dd?{?xT0>W7-Pp+b~}_us|-%Z+-{FJ;YU&me#5`ih^L!B&%q3Df9) z#2?b@48(|u69sChBxh2N+qb4sHUHN*?WC%WD@`LKL^K4#AD5gtwrrZ8}?x9fScE#<^dr<%(4gBbscmQNOlUaZ45-3Jsl z%NA!nJoor_P+P;X-c)P@qwA%Y)s{UFqy;aE!#={RZ+}3z4zDvWvdHuW z9czsUHr;3;r*a@kYn)>EXPj?zyuycN?|MeU8HZ(z7~2i7Xf@o)G8*^dDJVFb;8Mq* zD0L^MNJ#PJhb0HTdn4?Br>LIrdI|-jO)I7cu4-^YH)VIY3*7DC2_jV%zducYcgFK} zI!q>60@%bpJ1st8o> zK`Z&WKnc(O3rn70G21Y{cYAaH)}8C8FOvutYKT*W7YX?gAqx)>3*~POgr!)abCbN~ z8AkCu4rVHQragi=+K+lg5)wYLfZ);89pjxx4H75*(biT=RFlt6L@G^B37wtI3XG+5 zYA5d4Y-+ zJ9vKAPd*E&#bnz;oupjX1eHK| z_IPg2Iy{hj73&v(^7HK&PJeuGHjT*=)xhj>P)$Z;)e^~|tScgEjT*BoXI6nQELtMz zEQ~REm`#atEeGadXa)?khN#W_VZ=>XE3pXd37!z5)GmS6$VF4i)P9dI9TwHt% zrKD9Wp&)uFNXPp6JfxwfX?eI?m|_ugK9jIqT2};1BtxfbE6wMIk{2HH%*o{m14OC2 z;l%V{L9WygXr{s0(~9@%NwR(ZGJfqj)mo%D=obE;i^gb5FL$;J@}CU+MsMHu5 zHpLRL<3QjXLKZGK6}HD~yJ=`8hg~_+^gKcmW(o`e0RdY&C8re++V|f%1bpg&ojEUh zCKfq|J2EiH$OZ)n_kx4SQa3@ptD>OP(99Bm5z$aX&|JkzX@s^qt}E)Af(3||$V_J^ zNnsE^G3bN}wT%T{?yZDeL^viXY_$yR{yDd)cxD7;QZPdi$(iy659}1F+jx+Kz6G|Q zc;XJ_{{CH^byuRLEMc?A*wiIJCr=_$QQB=do7{^+KH#J*Jlqt!EQ~FFZCf^ z_>g;|E?7k5UdD!p`E6){wsGNo&>lQ?oDIY+tgJPaHcgv^T@91&LJ zo6sXv8n}9aPSfNj=t<>10psz!#kis5YIS$(n_bH~3q$KhJ=YU1!;)9mkk83S6m))5 zmxno|M7eaK;<9ljOrhT8ls~k9~rV0E8^1>DH#! zyRbka1u9%KPZV&r5?RtG9|A&1Ljy!*i7G9wk0>dwCJzumXrLJ0o2dL*YaQ)Yyn*`Ru1h8ibNR7smHGIR+2f_Zp z$Y+-w1Q22KI+IHh91eNVNFQ}SL6vYdK1Z$;Yc7EFteO~ZpA zyUo_Yeu+*EyXLA6t$P1ta2dX05hs;p;b?NW?_F4hRwyVWm$6Ksp1&l;VE(O2VqxB# znQXurJo&`d_M2wuvt|!ve{=uH5_)}hlt2(dGwxSw%_M@Wv+46XL z#hsMmw?|%ua;Z9UaZhD`c!ldsgT8xQN}5kjkC6<&8Z#Zv$}WTsj-4cOVW0J3KU$+Jf$$?0ZuO}c`~@)!@E^4&Bn!j zRBAIiT;Md8=_Vz`2-9G%Lz@a+=x5-FPfM{VvK*c8>M^`mUkIggQKVEVw3i!kAJh6zeWfNj3|L|==g+TBlE&dfc?f=ca_h)Buz58#=$(2uzG zXb(33lCn1#GV*wgA^cv<-{N9DTQ?1lu~;Hn%wsH*5PO6Is1F);+Y<@w%O{+B1AX(9 z5x&y)27#q|BBg+ZlI9zsSuHhK{L;Yaxrj$$Za~J$2?;8-IB>yHt?uJAein-{$t!RvZQWCsOor1+knS%5JGw|vYh>cj*+F?vr zowXH~n^GfW4v*UW$%p^#y*>klNXe`;`iVvbbKZ2)ucm{yu~4rQroWtM;0CwidLRij zte8!+SjNA~6jC~!4|*}8WB<1M5aZ#eeJcm$&C`IG+3FRyH1-_sNJ~|3<7p%UwG0DE zUx}6TL#sNlYN!0oN-Gp2#l|CbkLum_%-B z{g?I^JJ!g7Tla5zHzD8j3`|0VJuWT0`2fB}T?pWq@6qprEdGSKX8+gX%DOYlpSxpj8)CB^Gt!=0(ziYZLf-NjLTUM!_pp*)=yx#? zyAQT^-43eOc^-7&;+!!hr_+ua?J3=R+2wh22VY>neNN)@wc7cq!-Pmw`78Q*oL2vjxRDFNNetyWZTadr8T?%w>NOohFM;+1Ag{N^fMI5~HM zqscc^3j=hQHz3j24A7QanL3|!JS#0m^!TTb!DQ^GhKrTKjnnN|JR&|hLbpWFh~jho zQ!Nbdx5vOhOxjK#V};5XE@#hJq64sl0BF}7(^9B+R!tz~?$<)`O5IKfx?I*gXKl|~ z!+8so=J1h#7w9phemz`n7$1<|W;PI`m*KlXv%#a-|)}mdCHffCJAd1@K`Xe?T zU}SLp+o|vd`!|{>j8*!CBG1FgBNUrv6pxo{5qQp$2D|r=t$FqXT;uz<>BtD~Z%A#6 z*=!_9V2yS5H{wg_Pc|GJh8HQ>b`-=hq-%vJ-)!hlbYcvo8X4WTfd*Jx738UHUoCQy#o~ml8?V? z?7uGVCs3gvy5W;wf$sOMHn`y-%DpN8RsVK|dXYp4j-~-q3WXoJQ^u}bYlY=*t`u?3 z1c}0A2K$(`Fxv-Wu#EyVcI# ze646SO%qO6Ea%<0&TY$y zBU3%^_Y9`m%eiu^TISAk2x;2ybC-KuPRuvBi;>vCj@^~pbhWx;l!Jbj0sns<$3K#L z&xx;myGY-ljaZ~`QE_4hJY#lN>WEjm^)bO7abr-D=`B*Jg|f3k(Du&dkALc)EOsM8 zq1^)+})*VN_OM~o!{_lw9 zrpP$sQ55&hP15v7(RA@%{RH)TY0Z0Eg+Q(9q|(Jk;*8_F!P72N3HO&!%&|JL3;`5DDmmK?;UDncJwKQcHsejQjDbB951<6B8mK-F_93tZmHc zlGOr4lo>E88PXEOsbrvSvTkuH>HIdZn8y^%+9Tg|kO)k;kToZ?TCgLFqlEFOnOC?D zg(lBX5oIpJC5#r4=+xU$2tGt$Shz4VT+U+eO62kqTCiy)_ z$5a~b7x)b`LdQ!$s?qv#B41&{?X_7>c6;GyH{;3O1$R5kOXli2_m293u&+E0FovL}Vj zeP@S%Nz74dJdyjtn(25fjs3$*4y07eNn(~m+*-LvLuOasID(5-Ljd(RgxJ(Z@{*ZR zE>a%LiQV1`VXLZbV0@q_ku9F5a_KVSHta=UPi(fIZxmjWh4RR6O?U*E%=!&pU$mEq z6rv)*Xr<*G0c7NW3fBVpCLN(5-p;57ANvztD;%#49KByj_}(FpViqgfTE0jjabl|W zw928jLOiI%fh)H&i0nulpvyH z10@dBIN$2fwKbGUFqu#)6hkY&p@k8pvbXi4qi1ZG&mg z=7|W^7gEVrSgS5n97XS&P7wyL#Y|9bP%}LLYnxCUWUCB1Jpip-4D?%H0Ovdj{t3!j z(P)FP3=J+51Zk(W85*~A#6mEbBT-m46&)4ASa>+al~S)=R1!|x2F@WB+Q^F`e7yLU zjLJe?n2ih!lE@6{(0;sVG`a7U$ht|%Nvql&5pnLBxEq%k0U;yYlesHN?DTkGlt9Sl z&(Xkq>BY4@7c4!+jf44Ih2oS;L?-<0jcB&X7}JFGX&T?#f;cSr%>o^ z;aO|46>~1j8iLw5p15Ylc@q&}0xZp}8ImNCfddj_KC82`i zE;{9RG!>I|1RTuJgzptXoW9_fZ(Y4FKU?IB?djb_@kVq_r~0>NVHNE7HyyShdIW!! zaKZIR<3w$b<`W%h3wIQzY7#|M-Y8JQ>*p!QCtT3TSeTC%P4ur}zF0|@$&I$-oeF!( z90w&3f_j`75C!Zib>mgJBYscLL044kq*nFox4;d^H`5ePFWA*63$e6|U8+$>qXRaz zWFFI?I5>Qu6oE?GWvUaM5k7^mv{)ALatOwILf%6}_J#l5^PEMO--t*9`=F}QywnJ% zC{_c8g~Y<{otUQ5zj zRg1(b(tOlFwmL6gbqXtxB=Ga^y~Q+uX!R{p5LikYouQw4n`^9gEVGKHB!NW{Xqt-T zWX@sc@tCNHH}ezyfKW=zK|}JP#GqyDB4$6n!9HdScuRO_+Gu%Ev9kuF!ajOYH;wG6 z^Ac&wNy=_94y!HPeeil3L{aP+$5MWI@gPWS!!lBwgw4)t)?Pal`I}8 z6>ITc89jxxDT#%i(&j&?jLJ)e>I+uu-7;2@`({p+LQ!%ld?ZHd-^eGmlvoU&>dDdJ z4F-cSOMf5gNuMrcq3N&dE$8>G_{h;pD6F;-5?vtXBP;FF(6FH-obob{g*WQ4WSUJ> z(U=nlGLVp%OT_0z)?=zg zI#j(GDyJ^lk)02Gurk-ZPpe@CAxKfRggdGl>_Gc&bUvcP{o0DS8R+q+nbvVCSCEe2 z7meUo8TX^0gVq?8A{G@E4Sy9*4t5bi{?5!r1$9J)UB9F*AO)SAXdYiT5#mx!3@Vz4 z5((hU6tSXKY)Vi7L=9%?$Q*=i39Xrn`o>YQMuX8{58?5{5vE{SX z6f#1`Nj9GyM%iNyg-SmnV&iG(igle!wgZRrBg-Y&;E&2w>l8>vlom|&#jG0=HVs9S zqRoqwGmH!^2^q!$XDTL+_SgKR3R)L@j_4nn^fQN<7NeguIGL(iLmxsPqe_i5JUB8t zaw;ZmZLp&6wGiLCtj%dtFp{uc${R?b-x|JUn`{5Ts)Jm(jhiDRP8C-Cx^K7a_@E7w z0WajwG)vQ5*%MK#G-^W=qYMut7Qt_nh#VKB*u^ZO+_A8MbCw4w0{`Ls%91C1j-`Zz zD8rq*%85a?D1JC9|9N--C*~`A0QLA5TdPujnx)SSJ4?pVp}zMdVOVE(E+d`AgSbeA z8q`LHC$#2DXiTPbPzY1>z)Y5MFjXx05;Do?Xa=uncH&;b6CSOJBI&3$1G40)6>3Rl03Vv-RzQO9?P=ZHbCJD}oc z+PqUUIA23u@jL-LD5hGVI1K#hTRD)+4os3B|Iu=~NzGlh{{qe%r!{S`kBmSrErGIU zhLBO}D10~F@uSd|P%XaaITt-wKEq4~=)t{cJSmXsg3L#!)=w)W0RitM^z_@*$h^1) zOU|TpEBEA}*jw;SwXr?Pac-nCg~M3k1&>5bh^h}3i{q#;zgX~(VrL-(Q^q>#tc(p{ zXO>dA2)I1J6eL=WCZY5_5k1rL;Wp^yq9z5hMvmu=_~=Focff>+4fz&z&nSHM2$vLQ z0XyMJlH4UmVlE~$1r{}rrl=s+nb`sgsJTmz$dt-ru53Z(KFJO4ZfKdag^?|4Ir%y# zceRo2p>`S1c*%%H!IXe5O_g{r+VbSvVveoe+Z4(Y+fShbN+u#v#pZ3J=%MwIQKWhz z0+}h{ZzCaHBl0EYaub=Hz>Y@~Ll^4IjFxB0E9Ne_LPAXA`TyW%Mc#3j=M@h%qkk92;6BdTTvNyZRS#lerklKrAq_4PK`dxcL*r?tKV z7FnKHLLI^%N^eD?!cJ+0dhzZ^-zCxe8kpZ|-zIP+Yakcw^VGV$NbetmcM_L#Dp^H? z!;+j7;cKp?g%h|y z(x{hmRA+GtWv&!HXg?OObXYLL<#dR6t7Zg{X-|Is^A?(b8|ahacIq-jXWLgiC^oAW3l8$2~8V~oqqvk1yYzt=ne0lPCTd@m6u zOO=|S-q031X0>R}$hAaiP7QdBAYi9%*A|shg(fDT%Z;Fk3AAD(cP5wQnzJ|OFsE} ze(ioS!SQG;C!iEZGWCFyzTh#dyN0;OWhu=~!Auh?lE*izb*~nZDByegKFFX(?70~H{?Z-Ey`{R`#Irb>ZCzW~_#}9A`O(0X^*Telsv(!GGf~&X zgLqyvrqe+)iC}|`_sxiID&r4L@P$g1__dx)H!PBo`{2MGVIN}ye%&@fb>S(EnDSp> zy|WQC5l1vtH7mNQ&p5$j7`5W`WIv4l#d1&g%h1I_m_wTUGPKQzQR;!g)e zzat5OH;s(qo96wBp1-b_=-d2Z)Oz6jqp`~d9xX*UxDA&V+W2}s0w3Y=f?hg5d*E!I zs{zc^#=xqEjzg`2$pHT|+OzTDy|t}pu9&{@!8IhDnGsVV-^TOa(hliv_+g-Iz?rb* zK_K-ibSJaUam-q=H7^{!L6JL4#b7UIj+QI$t&cg4r;zz$MH~C|nINm}6=73!B8iM1 zYq2ug+R!v_^H^ZilsXV!hA4e?eb&(|t+AojUYK#4NA2-9f=1>%;Jj&!U}{b140PyQ zp3>X2LW_lZXRAAG>`8aJRL7i$Y@IJT_&@ub&+I@EV<<>tC90iPa-GL})&ZQqGOzYE zp-3aq@~i10{p(KTPrYdl%~XDFj>SkHviuZ*ig4`CH&d&CvNDe^EE>ma!fqZ+gWM*< zr`17gm}e?NEmx@kVmBpcTJI<@sw1+qb0|u`1hZ>w$XlgTLZMWG-5-t?n^b%u)%A7s z02kdfiQYXk17{vr&4YTl-aMHt)!0zNG8&Vvyv7`JawbZ5mngPXkTB^odC(oF(F8V> zFo{mkNszWAcV#hCMwUGYFtHIpONZ;amRW8d70ZM{p;l2_J7XjMwi~g3sf~jA6D4qb zJOm|rk3zYW;cTH=7W#7199o8Yp(q-_!#Q811@A?nG7P@$y>$0T6OLAcO{_@raHe=b z^<*m_$;EmzO3_)Pz)8SWf|F&KxXRFOA`sjcw>Oz>;N!m*-cspQ z(?A_QiAJ|Knb)k>`q9b#W5NN1#<{ukBJXy`>vNDIw%}>(3@?yCrfi%i)C>a_ql6vc(lO|k*mcmX@ zFK6oP!YQLmAJ`lTos&;nQaaf^`X#j;Ddm1ksQ~ptS%Z0ZuUjp1#eG$0582q;iKTuq zE)}RJ^qxhYfXAHyLZu+=5gJS04Ru9Z&KyIbQw-HbKMF^-@VHc?t83vZ zGoef&c>Y^d1}Ab2)w_^QNm3K?efC=?pjgUvw|NT%$Um3@jY(pUfdo?SiUM$ ziMI)Kp<0dQEH*(zpm+X96DCtYPBL885DYTuOqw~@K_suRmGD7vc zAUHyzL`n+1_F=U6D29O4%)o%YKy(Y{FNse!V1ye7rrmr36N|E6(?l3LBq@ACX}eIn zKykA|l43^?)?kr=b~4QEWoBk^^ju22F3I*Km}i`@(2w&65rFAT1yTX_CQQHHGe=Px z=JtY(hvRPW?4>Cv`*nHNdFDBH;YflTpNBNkqAUr8bxxQhRPN`cPFBnr#+xys;sY}T z;;}*}nM?Ghs(Wf3rt#`C+%Qa3VUdbH1Kz35@Flh-Yc<R-Cw{Xyu{8nN?MEN{Vk#6W7)6_ z8Y1hjAkrlwj&}EW4MD!cgZ|>*s;cz%+dUnD;ZGEWJyN?$#-lF={|yB`8~w%VNkyih zMr&y>WYPS$qyF%c{FNC8|HWAT%lRQ4Rhly6DC71rffO zMMNNv|A7R4k$PI-|7JM!nucFrq_b0q~J&T|RdQ?D3B}?%q3Yd<9 zkuE(f{HFmuApEG_o>LM@!*N9=rM%_y=x;BHf0`n@7Jw*SJhR%DQPukbI|x5G{xtkW z<&gmD2TMpwjx#}?LPaT-1%-!8q@dpj{C67v4yyN!=*RqNeKgfPR+?4K9`c_VV({1E z{{kJgbo=fSJq9ko~izHzya_Jlywjwv_tw+n;SFGDSf?H!_Sw0 z35`aB46a8YBO`w$)zR>lL*^^c_fW{uLkBf=^}dM-8dZgvibMoYV@@Rei$I-3s8NUS zw|rBB^5}()_Mz5?HTp;DtRuHyWM@pCao^u)=93UV1>McfjeME1JO#=XxnfN4U!}SZ z0b#*{H83#HGc*Ktbi8BlqzETO3T;9?rK{}hWZpT`BeJ=?_3ZBDvnW8Z5hvMCJ2_sMhIL4gI4x-O4ZUt{_PMY;>@WN10&;0QvIO&7kayc zOiZvwp-(wQE0Br;p-le$%MU zVTWcjhZ*`<)U+&Gu9UyGV*aaSz1$>U73K~a%m1?`qv~If?*WbO|BoVkB0!DH^|Dg* z9J5_HoV>W`$GvZiJ#gfvF`d4kAy6Eol$h!$zEcNrMNIze1V5H7;*;XS25CMP)}ore zLw}OT+8;k|L%>D41CQZ9CLr{@t$6OhASpK3DDMYe8J-4!ay&5{43H%m`qqFbCML#b zRZg+VW~&HI3nDqGt7ckV?f*T2{N)k>SVN$sMzyE_RUQjVjLdL`Tz|ehRnz<^OwIKY zCl#eoWXTy*9DCJ;CL=fcs4*Y2+xGmfmw=@7Bit*DF>!>@Z_79-D=m$X@;w>Sivx(B z4p-#Z`j01MD$}H> zxP&}EKa=~GdxU^8INn)0fiqf8LrV}TYoM1(48{g)W*cR=H{iTHNqGtT@!y`Vgp>u( z&h2jy|C*Y;Bm90kXfT%eqw86Ior1RY%|<6m<_StV`XNrTN6Kj^VKB#ArdvUvqj_Wp zi#`BWyv!vxzA~Y4r)tzP#UmZ@FFnPF$!|$D?^AWT85q7Wcnn~8B2qhL+su_2oPctH z1q`M)f*fhLnL!RPJkUPGKybl7^#%{j<%Q_FmL-doi7wt5qp~}Yj>uRF@+AhNW`*o_ zY-oxnb7JR$H|_Pe!J;y`AunY?XL_`g*&~jB(Ipt3_d`P1od=eqLU+Ye6wQI8933&R zem8ROG960c@M&hc$kuM)`J{vUaCbvK%8e4vUQ6B%=193&0&#+X_l~h-x6!}bg~nJc zQXvbE_hGL!Sym7}sfsHso;s%$y12u}U_Ilu;?h8WR55T#0RC$k*dZn%DDYVrL}F<2 z;k_xQ;xdIEonPb7Z(@*L8$0E@(L7;=nnj<7+zla{iu}g8iZsZeM#1NB;~=B2Dc+&O zMIRFmeImC(F1;VY@)oENk7PeXsh-0Ym;8xV-zzi|`lh6D%7z$DqB$tUM54;hMROT1 zh;}T}NmP|E(M;lqUJaU?h&^w#Kf8}G!q$>0o2(F!sTiM zI@dx*fvN{O1m+?jM`(nAgam6kC)i-Bep|l)CAj|!UsnS{gQF|eStnH@I=a#VOC5)u zZImP&1k)P4>5#j5IQahDDZqN#9-td5);sW1tw|r2tEn2OLp7&(Ax-as%MC}w%iHd7 zg7R6+QvRla%j{ZR0%xTQmi>x1xJgN;@uQO#s+CKeNojwe&h`p$!~Gh>!4Y*_wK^oE zsY0+?t}hQ3+kRb6=2Gvt_JW-htI-PD7G3j1anH_ZneN^BiV+3P{AnTIbIzfFA#ryq zI&#J3lEWa;-{7z!Uu1s`;SMZg5JIcd4h{9J$uGGLvO1SHs27EqgEHUl{$8ec0*uvy zRVsjDtWw!H^Ru7VmY2++dA0 zkl2Z-hb~@EbRc3Sz2jq3Gp10S0Z(NG9NxQB35^b4&$NLlRakL?1^_uI=rP*L;P`xy zer?IX<%>397N`+K&zHuPtDT`-GMsF*pP*y!Gf3CNT?u--cc~0L|VO`ID{{clW}DIP5Q}6dxo}sI)+7HJ6G#-k5$7 zmErt-?dKHbhq-9b0v00nW?WlNw5$$CHd;IuNbyS18XpJ1W9du3M|oYp%k%Y0sAdkX z+yEJ^ocICX2CaUR(Jk0Y4(O6hP{RcS(G7A?1)bi8)^4aPfM4TZIVe9G z!9olgC>)#}_%ZHuLOcMl4H%bfBM4qLR>cC#RD(RDodzSHOGmweS+9r^OBy{rKquhq z&D2zLz(totUWLSDyYEg-@D6qFnj-l5jy3k4acP-E=Eyb1Bg<4GuM#~T>vR_QUXaw2 zsV8UyDPGoyX*Rkq#Jt$S=-}&@S@2R5fLls!LeAsxAgQ2wv)?63T6c6GT4X$g@_63 z+s`~^ER}bF5nJQw zJkWT^T^Np=z5!AOn95)?p|;_H6h!`o`#k$ysAQs$LqNt2pX~XJiploWyM1p8lXQPj zUm~X1AfD{*XQc6UqqBkyA;Yu+F>JTSsWJBIH( z_5>la>Hw?{%WFwyUf&wwO@7xyA28qjt|3OcJBJn|q(izJX&C7k z5QYXBK#-wBrEzFPNq8y%k+2W;NjvmbV|yYZbUf=Z6=^3N#lM#jZPaaCIv+(|R={im`^+3`EC$YBdc z@OX4xnc6cGU%Vn~%sdNW*9b=#{aJkH8R_uJH3U?Z3xqr0Cof!h7gRuypMXVUfimN zHz^hL_V+_LBMCm2c}q42@+N)No`0>xf{mDBy%$UUW`Do#_U36nL&uzeLL5+D%+oS? z|tHrw`kr2?N)w)%RJ_KGv1} zuj>efoG3mqYAF)K{UTEGy6C&4UT`un=%NU8CPp>dDS%G(%Ht2$h ze3%EVudgroXbrWnsL0@rK1-+?;m4tii{P8vKdTTsqnal?re=-@*nYgS@6GF=m!tyr z4gpiYJTy%x<%g4u=q!R{958tm4)5bWxHfX?>&<1YPg*`hgN7-tI8d2sg=Rud@qcf& z3B+eGOfoRygro5!2z+As1=6BxBI0|CM+&Qp(Wx4fSlz{XrJ9!rs{J88{YQ8Ox@Jr@Q(UDwmJy&^lQ#Mhj< zMEek%^~RpV?Zq<7WYGcsZS&)7cUNz}zRBAn9wCQ&rPDchgZ)o6861$RL4gAO`y-On zZbl64OoIx%RyxrrIUc;*&~w$P7)%>uSX5cmg*^GNDn_xDT#C-nq0NzCmM!09`t;_@ za&a)x6`pdHfJ#STu0bOVd*VI>Yn#_%!F0GZ_r}Qjb6h_HBh}nQv{0y3J@cf3gwE33 z&?7$j7Quz2=}MrEZl z4F8;?rpyDqtTUlhp}lWrGDmE@fh5N^m6kIk+@KYWN04>cnA@ZRcz3~;0FIl*>YB!J znRTV8HznKB3?c1X{6c4pZ^6bd5f|a&y)%mb1pX%#)|qfhnRej3+9s(h(3PK~<|E># z!Le#Vb6cSwCXlmXDiLfmd%+?Rz9(XG83RX1M18l24Q!F@3c9i9x`G;dYUv>q9hhf0G! zl>=4!@AFW|hYiqpUnO;>^OMrj@(vii^R{K@;8H;jN&3AwLlgrbO@j z-dX%Vb)`KOecf^iwd~Y-c&dt5Iz`PXOI}8dN|A2D0q|L|K{@93x>N_uJf(fpr7~i% z3-VniIQt)=y~yhNoULjuN2?$=%nS`Y!o;an3&Ly$+&rUzE8$$JaAkR|<{H?VzMz*e zH83Bh^166wY|vnKR)}w)g%mR^-@EC9@(z61(Oq^RBzbo|h*ur~@9I~nfZ0bUf4?Q=UbDd4529D3|Mg}zXwzQ;vC>}}qF&lG5izIf_TnO5 zgyyn)+Gd=(Wiv(MfYWmKV|gFZQeMXLn2LjPg}f|ixR z9h=GuKi2M0)@j~y(Q#VLjJ!+0eV4etjJ+Y%n#z&%D3%cLlZcQQ?1rkGFPf;?&=3fj z1JZJgY_)X76xxw~cNtaxV~}jK-bgz%@vOJdr6co=!RxWoM(T58HwV)R+s-2SX690z zsor*7atc3aGQeT^#N9LA=nvb`cM7|NZ&-hXr=}pjjo#xzH=#$oN!h>Sld}LD#o%d9 znf?Kg_P*Ka#(c$C{kezRj3A3QyLu33V zGpd`L+r`zD7M`}LDb&G7Q%5J>n}qdvLLK|2V~WMGl}PE!a|HfvVodpaz-o*sPzX?iR?ymiA)8*S0~ERA0M` zk6nmZB>tXrr{QTQ_|T;M4N0E_sE-RWHs_r4AxZ#>N{?RT|&uhy4U#KrUy(b$SU+&`Qaer*kY2}?*M zPolB}F{p~Pu3Cl?XWnhRR&KN#q`ql-f#2=EisxzawL&E+>Ar$=GP%e(TWojsy&R{P zb<$+Pwl}{ed8^i@A_7o$WEiQ&;AR)cl*1sAEI5Shk+LM1LyQ${mmRiz^p8)GVIeDP z2pT&2(fr0;1Si55+4L1SSh;MW5lzF#3uhlg%<6l=H6+hUm0wu|48i2iD*QIAUw{ZG zD(zT%oqa&^sxMm-Qs-Y=lz72<7N9t_g|c|sRDhvg0iAWl1L(un6ZxgFACE1YdL2ml z=n;o1XEv2>s)0s_+m`zc)3XoI6bhYkavk2J1F?}w-F^{bv&UM7(vNiUkCfk-!B+1n z-@xNSSj>$KpXFBC)i;amNo5AadorJ!#+8= zElcmly1l*i*E6S00a{P=(Qe!*+^2q6mrm~X*`LGd>?XqxT2_8Jr!({Fa}Xsez8#vR zssefXK=O7U97NtyV`(KCbPKlpJ2y6|XfsWAH%=KQQDlsp>sbA#VNx@o`~*84r`9U2Ou3QKLzWeG4} z`vF_#GcVVr8!zSV^0J5HXb&}d&jnG&1*e7MqEJTp(C zoWrNB&9_U&>T^rGb$V&!ZYjmEfA>!Ll08d$*{H+7o?_~11`DgJA!MJCzd)YcKEjGH zHKz8dU8NR7Y#D=``nBNDdbG6=*~Z2ilWq>Tg*KGNpbLU^mzNh+oULF`$2#lv6*sLf z$a$!9YG;BI?tQ#`ZKqn-8uhgdLZd_)^Bq2#QQt%Qm&#meRfk3ochiHZqh_ z5sV?EpYxu*taT0=b_0~UX*PzoBnp%!$L~{PnrSYol-hVSH7&8_HhfdKTk-$wZTQUj znUxD;Mdah6dR6JPTb+(n%c|vp4@y>kWDIV4!1>7!dDR(P0Ieca+?foZilRvqpTp&k z;@_mP5?AZ}(uW6KTE?matSj&G3yE9#47MeaVkmJ@tFPE3!elASDbK-suW&7C;On%q zUSh0jX+sSwmg{yNX+nz=4LgXu-T5Z-9Lek~zFbpgFV;Llf?OyxKeMC(`au{&@)DA7 zQNu2tZ`fK;rv37Cr&IPh)%|SK&XfVU|_ltKtyI-2IDQj0^@&{_{ z&u`vCMI1T1Z#D=V9Ly&yTBIn$vItNm?6g}k^@TJQ8(w@ znKdigKausuW4rz{ID@M#RBKG= zqb@29Ds5@pmX|S_ZvoQk7|eTbLh;PS_yEiNxne;LTzXhE2l*IiFQ3AYyF5sT_%xx-#{R_L1;r6E)elewl@g+)GtccW+ znVm9=szAtNShWta7gdaMB1gn@?V(H%#>SjY-DqQNYKatuqDtgH%WQJqotQ!k8Ve9v ztMYjHLs0gj9(*v1v4@DGR4kA1?TVihI?Amh#Y@%~K!j3}B228RZD(0JGJuUE-u&w< zBfDC2+0N#C%E>naXOL_0B1$cTp8>mR#4(-c{KGYX%q#D8aAl7O?S~F74NBE}J2sE3 zr9_I%KA7_>aG;;Zhbk_L)JQH4m&$dbr0S4u zJ?1gS50hT>%$%myv{*I&w%C`Q&6C;v1fEiC8M{v@YPq>cuIUR$wwf2Lp5ec{=BGT% z7mo{BeCIRYe@xwaCY;8!py0OFQ^6bs=R} z>G{@i6-xZIkDAMT%S&1tn;cGQxTG}9#~k3mmP_*h-d7!rl-uA{DIMO|c4Ri%XKHzs zH)=D|=ytoy@;Plp-oE-QKvv4O^R^#{M#-rJY7PYNv+~S)mzM#}XGLPJIJjx0LA_=M zmX(&iF#jJUpa5A(RYE>#b&0QZ`}84V2(`>_Wjs9MCV9T4xzxc)DvP<1?uHp1c^hl&oTr=CdtD?AeT6hfroiBLI88Gpu+yQ7AgCvOTk7mi~yn3{ucLFj&hUp4V+jG z9Wlv?+s~j^>muBq6)33lG!#Km;%#rVRjzvNs9{@n8}YF|%W+qjhg_-ebxXqUJgF>q z>piCuS}Pfz1L4Lhk8Hk~uFW@^J8`uDj|EpP~5@!H)8w!5_VoxTO@} z7l-&HoKk)ob06hxt9wBRB>RnON;u)>uYjvF{FS4Gg&U5vPGsL+$TC1sodPNjqOu-D z-pA@{YbST2yGxPuuI>BLS_3kqmm9)e!+vzG=cS_D}3SLr`(3FEsR)O+xhpc{rsy7Ly8y3=&=f(efx z9is{5^UH@nkNFDmm6nbE)tGIEvMPsmAt1V9Gn+}M%kEt3jI@E3O6~ckQk&C9Lw@Ua z^s0La)zmtAO$+R*#4XCHlO)~$+> z1Z8@>C~~Z}rjOKL0e)Z?9+(Tc2(t9XMvc#;1=$B2x* z@*DU!9V4fStmMCd42>ENgovE_N%)rFfWSpEZx(;Jx>MyZPyF85DoNJE!ze)4Z!3ZiGuCq#s556%>N@mUSy2lV zG(X4A#~XhlK_q);GoinVx3?rn+`~|1YHI3`OX)wr$2~JMr#U|WP|UoErUg8t%O1!0rTza!ypY*S@{lfN{hPJ_82@`vv|drvZ$|2PrD&vB-SAfJ=NEgJ z6aPE-eNoh;PFxzqIAf}y;LIFODJ(3ETwgQGk3;`>^#83MrbZzlC^+0BM=J%^+u5G& zt@7_UJfvYebFZAe7izkf6y0a>8s}y;RvJ%!P#&B!z_u z6&!3$%&m-pfF#0`)ge`sB{8zqm7t($k$@@l*abb3S0nwLs*w|rgu*`}sT>b}39u9t zMWb$o`LeDFq+tc9u7;XF3W$iPr9-zWeoh3NaDH=m^&D%z>)^gyeK^fZcQpZ0s5gZU zPA^6Q(g@H}RLMnT{>T_Ec?b+80F0199GOLY9TNoysvx%Jo9fD*%bHY{k^p!+eyjTO zV`H}(Sb*rG+{Siw9U&hGP~{MMgaa_4lR!t$u~r^fS1>>o3>rj$M4CZ9q=AG(G^C=C zjZv`^=?PfiYb}5X9|$Jl{s#{mV!tAikQMFvIXKdp-+W+J|E9n){6$cCxdsxo(nmJg z2Rei^TSn!6gIYj4Bh82?l@Y&B3_~L8-5broTS~di#Xja^wc<|^LB)hj{gqNClVB-^ z?HCqVrEkQ9YX0{iIm8XA>)hs>x%0?RJ}xozyx)k55Qz(Qpk&k!-tTl*A z=xGB@kHbBL^RnuKIe;DD?I@BAoV7P;19`1hNoVL zjPG7-efy~9*Gx=K%;og`PSEGG?R{~y1TN`@71YI&ilRW$4+!P)VXRTuI)l0b!xG`+ zGT+AssKj zh#jql3BY}VNdun(?&|2uBk&pW00Dvx z146X(-w89BCe~Rb_8T{gLwpSigLUYC_A2Pmii5n-?XRybB9%zK-Ie5J>P8K#vy)v0 zb?xn4{|U?XX&K8E>&B2OCPwO-{1G`Y3|OR&5#MT%3zZFd{S8N(!pl+L1%)nP61TID znCGJt3O-6DYAbY;Ag&utG?M{qqAua1KPRdT#0rCEw}cBNgUMPkybYWN_c~uQ_cmh7 z7MvC;VDpv%)pi+mcNeH(jTtT0{z+|wU?8#jl`jXmBcLUq7b-HTCjA1h?vXL#%$0Ss z+jT!SW!~YKVKySGpUz+u0IFv|UL#fx&rR23NWD?xL>d)?PP&rF8Nare7X(W6x*I};wAI}#8$#kZ_HU?)Fd zhG5PG5b<@0c0Yb7U}QoFk^rpgLvupd+HCmUK+;^OBtKR?(0RDVKb0IxU=zb#P3evu>mSLXx1631oN?3`QXuq)*?mE(D(A8AxCxMG}zli3Gk6 zdKpnF1tbw=#AivcUQ#>2aYk#2aK$f4AWnt0i}B@QN{Ol}T2EtUg>mJtno(c}%89$1 zv3-t3|57&LP>-?_JDXoLIkN9ukK^r+U;vajfSHDG63%JZ2>?q8Dl{On`luF_te*^^ zodw%_q?(PsF(j}jk{d{GCe_LjbfX#Tue%7om-1rP37*?By(n`-lv>Ub?_-HZaukMoxW-axq^%jd*i=c~; z;H?44Le(JR`td@+LLEY>-=sx3MTtdi`>-M&B8($KKU7iNQV&sLQ9n@CB%xEOQs61K zmar7ds5dIcD2yrTl(fq~e7R))kPuN5(FG%3s9>BrjFl|z6lEiDS6Fu-eb9EWx8G4J zqKZ1BH{&#eG6PzAQVME8WpQg^Z{aviH62st%*+tNNSqcw1Q_M`-rRG~BIlWT$8p;H zVasB`Ioh*|PiI9st4ya{r^rY9B@sfoSC?4WutF@b-&FK&k3jT5 zU}5dvp^z(hOSm)4AuLXuLM&?*3sw(XD+65H_8;#`q=&akR!eqDMi22Se=aF9Z!|AF zU}gT2UZ)YVpjLp%i-eQu|pP|P%Ku>5VC?iEh+IECz1bYOZ zIz%;1)n7GmW+XfWHIb|`Yo6<(tRc|(>y6D(8ygkdM>cRaS+*+cQR{`q4~=ujW-h=k zk;m94It!oX9Z#AT)*N!VWn2Ke6PxFKlS7KzfTQ_6qy41g`o4p&b)}94S5kYhbYK_+ zsiJhFbOhRX+K$?YEw4K!2Pqqk<0lIwiIU5bZRETTbFQW>XHI9gdLAU6$LIUE(^p3B zY94&oOy`?!Cl?-v*1K7!9cxd8u??|@POT`s;K-T-f-uZSSm zpl-mdz@YvJ{#PLK;K%_&fwK@RaI**|km`_#K^(y}P@FhY@X-h>xQ`4!cx2FKS+KHj zncP>lJvYQQ0?tFAkdSd}=r(-{B#m6QZI#26iHcNY+)<2CS@}y@KjZTuy<1=%ave`!(6c^Jr6Wc+2q~M6#Z9>kUj@XW>8;V7;R#dN&%ZYpO z>hX8_F3ZQgj!An^yY-{2Ic)}~KcD@cb$2(e=C3q&k4ANwx=j7=0^P=5F)1|7RqojUlPw2Ru8fW8AedB4Ta#nq^7?@d_MVf<}T?QPrG?uEguykEd zwhkQ~&N0tnRQa~)?}kJg-x@EmE3rkeyQGFDv)eE&*PHy@8#T|UQomQ3ww_$9H5hrS z$gI#_YHysenOoJVeff#-8uN)8)OA)JTRr83=yZ7L#>jQ~bXGUhw`4WHrO0SJwIiTo zMO9JNv@EN%?9Tk;lXl>2nC0WG{L-3z_nClUh4IN4mzegDpq+x(`9sxE*D42^NAXVe z)y5*Rn0O;@Apvcr_~G6xB1-|Q6FV833qCC8?9}$G(=~h9+AZ4=t2(YacjqJ6j>ty% zM&biGq&&3zP0ntX&ooor&S`oO^W2ym=aLWI^>XkP@ZNa6M^kqDx=-H2m!m$z)J%F- zEtht!wcQDqYuC`V*hcQgIbHkKsrikm$!X4#E0I*wxq~tv^>JO;PL~Id`UL8lIf2NARUOtvt6YA&Q zCGXFiA0^wetR8z?zBL*%IGp#imAeX`Me>_@6+iAj*W4&S7ZnND@h9@7`Z7LEU9(;v zb!?Px=zo;{o`4U|Fh4P^0;F>W?w0~Jj&P1It{At{RS+K^t)EO2M8rq0} z=(_+vvrMulNdH32P;f>lCu_jufEnBs3g>fY3>Q{Q^rW zk(~bm5#}lYCxEOBm!YjSoxYK+fiaz%wcYz@ARrz$uJ=c4V<&w=H)|^!M=m#BqJNFx zdVl`)nx2U8UqhTMd5HkB3WUP84#tG6bS!iXM0_xWgoHc}MkZWJB4YorzyHNcWai{# z$3;)?>gr18%1meLU`o%($;nC2z(mi)MEgF1*3sR@N#Bju#*z5nll*5M5o1R~2Xi|o zb6Xq2U-Rl4*g8A$5)u7c=>LBHEvK=Y`F~fkar{TEcLnKxQRo@z80i0R?srz6UvIe- z%-xKw03znr#x{=cYw$60uygSIi{XDr|6TGoR*nC%axneD`5WaQP9FMS8vLfwzs>cp zx9|4igW;k7Kil)cn1Gf=0s#pCNs0)nxB(xpfO}~f&qAD=@|tow^}CjuVAMR&AXn8d@eATmYOO!ar7;~YPst;ds$0o zFdcJWE2|v>p+n1kuTBMkVHEs8{{850LXvAE=B=;($J>7mZ~=xW^ZNLIBmZF%s6*AO zUObkA<`(&FntwgQsXbu-@BHN5dJPL%C@x|DtoVOinB|wOzl-uO!l;~pXCA%=4LTYc zTDKLOu9+&I{Qq(MN50&Hh~*;>+ae;ymd?_pX{a)l#>B+Hsg(`71^z7?wv+(0QZksiq@-jt89ipXT4jY* z`LkW#U)BSLVQB;+2Uu9?H~x>5H|ToRnzl?f|D$3w?Yl*4f*1Jy^aL=>^e4X_i;kgY z;J*p)iv-T}qnkYa?LGzDcOTu5Z9)7^n3xBFQ^Wa`cB1?rNB?6HfevuY^4W|iod5Bw zAuI#H`u|5o7W-EPji0EJOD|3OY;mj_H4!i9{+a&z7-XRPwI~pqihlc)Jv+C^;7Dqfp-nH-mhQ(^sOB! z6pKf7>8wROP6I|&*9Cfl6bT1w>VmLZv|6lyf7oDlNF4fPJG`M?ZVOP0<&X&FO&`Kr(Esoab24$ zgV@+5#XsF-HU<*SZ=wpqQpQPuOpZv%M226Py?TCeDTW`YP^dNctK18LK!|w zPJK&!D1|CAfwSTzQ*%)jy#lN^9+GX{9}@rjbH%{%9jU#!?%`sk2VKxyAeVgqI zF3{ns;8|QGy3@iEuWicY=EdA- z>F{aNY(%#Hb3cEuGaf8K{-5@mNCSz^)@X3m2@oPZw^*p*{yLTk?7CpNSn}fbdM2-b z^{6`Uye+Aa~O$J zg#+xG1w;Ni@7IuV*FCxdAL?TDy-Lm)lh2FexdTrzDLp7zIz>-j1P*uEi&5{KAqtNV z$|6HDKnC8*_yN%7McR1E1C>7pe8PR*eKV&U2_z5dQu!&G_6gzJ$|(F2du31_i})fQ z3}RpsxH^eVvYxszWwIgQqxEsm0THTDFSo&Uv;98WA!OZxlJn^_iXg#SBf?SBS#aC6 z^V_X+T0VXMIdU)Zy0O6Vq&1*JA5yNbsHc}5=>-D5XHd{PuQoj|iMU9Npl8#VIzC-J zq`LBjp;;NK(Rv}^TySpqWTHz;N_$3e^{=cs0S87WTdjE_V7yOdWJC$Fc>T@Udr|Kw z9G)X95!kXsmZ>6#m`c0faS=w*gk|eYAC$2*A4kV+6T19yi%@8>F}RcgZ%sBUVC~c$ ze9ilr#-%DVyhWO=yGO74!ny9ECq5D72ZDbaDa1JcLxB}W_-z)~w=|sSoFdsiM_cbG zb^I<=3f|~&`ZrKr+GJ~Q;f;LujlCh|YNr|Rw8xXDd+XU)z~z%9CLC9J7B8rHOpZ`2 zu<*tXVY-25dW}?nw&wYY_}jNO-7$Z8GaN9kvTD8E@w65v-mmg6=y(BRN$i#^R92tZ z@UBv=8}DxRZJCiNce=9*(OL-&u<$*$DM@g&C-S^4NknPCPnRfP%zoEBzsq&H>P01J>Yb?Ec+ce>{ceeWB=hEHa5`)FlB6FN&VEW$ zpOJmx$`A&=nLzo)7Ik%G&g$9uNrEhd=J>)|&b@{d+2Z@L)^!b7K&ynVU6P>FX+(fK zel;{Q)qQSU=PqBcM{#Yxq+Inqkwp9OVe4zrTK}PY;sot9R@Y1EyBR%ovixSs)C?BB zEnrS8;w5%v1#BIVhCLh7ex+Lqi?cCgv|J^k(XNG(*I@_xH1}iRaV6gEqE6E7q$Wc_ zMJvE`?G?_k^9fUM48<%*vh14$jevzex+~9Ry-~2b-b|_Leb-Yc(MGokE(bDr@}mrA z^@3GzN0t0{ZhHP0p@SpqH#fwxU7x^Pe~1DjBck^4hI@}gdO(8%sn6p!d^5KER+I6k z@8AH&D{*KQWro{E(`vf~haYb{u=m_4-KEUkg2VnTe;o|XQ}3D8M78WEG36x`e*24> z8R`B=l$?l)6%oYpJ&=d1HMFCCebQ*k$9h9Vp1U9U=Lxy!44V_T!hVjhJhwmGf*v1$ zF0b}Mo3`%eI^%|7zu4`?AwWUF1*n>!-CiS#M&=51s|A4i;s*BjV4WIc0XTdcnd;F< zsX$>+mEz*l!l#;{w$oJIcH_J9ZQGEZ3goL~p3&KPGs4!-8Jb6$HQ#{fJ=KS*2 zf-=rO_cBL^+&}xdG0nLA>nu%|wL*(~kMOXLej;No)9m!5IEYelyfOtd^N>OO$QniD zC=qM_ttg0O42$c~AqL6v_DxT8RH4 z{%ClfpiV^#VnB)Uw%(w)^o3J0Ly9IVTK@F`g^Yd;YR(TG#p{uXo)}@#QKq`U3eZ-i zITU4+cEPLja1XnUW;s#mO#94ouX@7Ut6yxk}NR8M>1Vm7MvK0ka7%# z8xbY2cHVg_zJ(~+s5%RKp$yV^DGI$rlPvh9{$$@kIDH6u{LPc7l3;nbbLyZT4ce}d zz`;EOEZtgtsQ+|{AlOstIc>6NPclykbHYHej|U=St`Ss#_A2BG}@U)#KsP16WWY7 zV)x`NFR*E@p<=KOH?q2QFFc&*8HA96l`U0cI*0HCH@vq9Nl^6XL1@Pbt5vI~3N4U4 zQqIB6*{ujsNtK93Nm@PprS$5OnhhEbUWtesg8%szvh(#Glp`f7lG^V}8v?qA1*65i9$ber&DU~I3{-w!)Wgj#Vdcz#v(6XvgU<@j*CQ;~ zN8Df=tI9QY!+TH;4SD%p5Nlo+A78_0dd7J}^P4)1hmNIOoF_9M&Xa&V1-e%4&>43C zNgPUaAE*T@;lUrEIjYZqTu4p^5Mg~&2e!iL2E$lS`J^0c=rdx~H2HVRTdn;Iez z#h!&)nxmO`^FpVK;k6c{fzpbt+JpL}V8xPmUJ*?ho(kDt;?ro=A$q*D+&RXqTK|5 z=lHy_^aO7mjH_uBp#^mMqfW>5I~bovR(fxb`Nwu@xE%P;Jw$MY6HfnNC!rE;*J z9Uig$c9bJ4HAYmg53`|zR4Fu;qUM!TP2F;!=j+zdCjSmK`KuP-woV6KP2ktO9k zUn<=FZaZ)HS3WUyoNOm>M>4$bG$Cv2b-&2-#4sQ-^LZbE^IYG(3CBw#JsV9_PLhyK z^{;0T^zEH>2Ds2JSDP#ti0nLUtUjHlyr|#^f3nnVm;P%vIiM>@eywtu25h<2*4D1? z?^C4OF-qjRuc}&FsXL1Ya7Qu{6n*)U$8?`7n}N=>SntB^(1P8fW3Hl-R6w0&lr^h* zL@~!SBPHQ|c+Xg{hb}QeJ!fhC6JNiFPZbrPvJN(N+jvvOiARRXQ#qq?!;W>|xV-2? zQ4GGgrW~D0tz6`}3lx0$vK%Rv0__wE5i)8{3{xab=f}K#niu8_Qm7<4suOghg25}w z368cr*(1As*3|X;@I~J+I2Z$SgA$=l3E|sipo55&^3Y;lU2CFyXEIdp0x7gvj)m!wj8m3Yk z%BC{Znn6*KPPs2XwtrA_zG?8_PlBtPvdyS3s_!!25Lsq_rM-8;iV;8S_&{2MoW}g5 zo`xsH=sczccZfy-?D9aOocywB`}ZCS6!-~ImKvzQe3I|5sYm(kn+CuB*woD+ecVx* zl&<$TuFc6V{@^1Br^fNjWtp~do4>FHN7Xx+yt-t$BBy5hX*^779T9zdT-g>Hnj~HZ zcwPD-kJl#tM-*o2u50xil4ii)Kd)@n?R|)I&`TVGX^`PRKDDW?vZ(vK)7r4N5+Sej zCx&UZdPkZ%VnuT4>xW;+T3VxG;Goq6%c>n+V0KLzr0Fe zXAbs0DYofp@ZUU#AKy_mK0qt^ZvtKPyS~y}KRW&@Y;py7-{+bIjV$|1@Z9b~WW8z+ z^UM~Ezif|I9Qr<&hn~Ia--KqCcf3s>&3q^Hhw0vzyg_|dO5H|Gc?AUp{j zRn8Rhposp7p zpu7KRrkFxA0C*#V5dRieK>1hK;*h!f32fLBb?pa-xRx) zcZvW1RAsRa|6P;jC~zq$DFqc3u(BBsECTR0x!;1ye?(Xyf3kaF0Sf^E0e)2FdmN0sqRn~( z=~zD*9%d#hNQ<%F?0fLses`2`J`I;AmlsSWG^Ob4IkoY?UFk%KOrh_q6eiz5!VEzM ztsLlZT>~L-jqcb;3QA0S?Hv!3_#s*rqir{mwmRRb&l`1WeLmlN*RujtAe!^NbCBd3 ziv#R+7*y-8OzK2>IS+k9U1M{=&g`e;qL$!-42zGbZ!EK?dulN3{{98Q( zZ{ae{U$isO!&Y0Nm(FLA!Dpym;IpW-GLU z@GAoK8xPj?iqkTOz6Q+p0YP>^$kFXA%$MSwL;SI%fPW;Kp2-c-MrF`w$l4=`(9+33 zPd;)QuO-CPR;*CPoM8UcHxqCrL-}t|tRAsGAa^l1Cj8s#1N>1P+`2(L@Sb4h&b{Qi zuYSG-CdB2=8Pxv41AnF)7SMO&j;@oi$YPWOo=?Q@@4iz~gy7?Gxdim6MAb4iyg0B8 zf3O{vipX2GzowukS9?6z-GqRY%vI|mipg*VY8@U=Zcd9XtTw;7#Y(RFzO}{pdl==m zz*NIEw`8aHpQc{0n9ykYx1&(6K6^2#(R_-!)Afe)NLH*Ig()T_yn(lbjV=JhzVbm9 zO*sHLQtnpi1j^Kj)mE8urDdT-HXONbIK@Bw2uyvTO-z!Y9@!*#4a{NB@*A}Z==1s- z^vJ}|7(umLc@t!L6{u!x6FS$*vC)mu&3a@%i-y?F3FE%i8b z=&U&&h^=<=rF@1zppeuFDk3B3j;8O5&3JTe%|wj!{v8l?8xwc`I5c)FrR&UB8nUE6 z3BH^W*}4xnxba)sTR|+>A1M2EUh<#(fh&z zO(MW;dlZpSf*|x@q{;Axe(j^(zBT4DX?y=;`=6MVB^!vjEZTI*O7qr&8?%dM2_$h5 znf4jC^l|1aAEIFYlv|gq(z*>JgN#I3edQiCiK2X8#j`@S4vH()w16 zc4QGD^%7jyEY%=bx8E#YjyEU#o5Wb^&}S}{hLv{PVmqfbDzAo}daYyGkX1MPwO;Np_(bD$!Dr~K;7T|S~!FV_+h7%L|m_Iv=ulQJQV}5t<4N~=x5lc%; zi=C@$sHS%M0+nrUaq*x}@4`Y0J}ujF>3i#ctahQXV3M1-UUiAWr7Zl}n>9C^0& zlI(}ZklR@UM!dyrp6=T#)V2M$v5KWQNYGP!4g{T$OVyPZ&Ad?E8I#l=_zyO%Pxvw# z*SIeDkv$dOON9Kd42@Yo`tnin{Q}#?LAh?fEur>r4BD;{QXjLy-PCoDl$oy(5)nl}=rm1b4;u13qc5ao7>_tZsE3 zAwz2mGW_8eb8C1oVVd^R)OLdpXy^Xom^NN0Q3GDGpNA>(Ns#i5mX0o@tBZfJF_}8ZEa}2G)u%)w$?M>5oL7eo1VZwOn^XIvzeVsvoMMxJ zE?R6unQveAzQMB3(fr7^RdvSi^@|w?Fd`zNXLo68S+J3b30tIqDks-_3E&#DFl9`( zj=PApRSDhq$+jKVSwbA{2-_tc54i1jwCNv$T#R2$RoEq)u2{iC1W}fZ^6dQq#?1a? zbgq}G;U2WI4IV$$=f}OO*_iNecO_<(AX43n9iDe4$i-A*xOIzKx|GLrq{#b>n>JR< zTwtsId8tOe%h12O;B%c{e!f<3Z92_+B{meU$NpJNpwLlh#3J>`{IiZ-)$fYs3RnZD z0MDUsc>i&_zhezEhhHUv56t|3L+zgwU;L|%Y=k=dyCxJ!{8cSpDt9CNUZebiE$@p! zlr@OD{gFn0iSb$M{Z1}12gK!1aPv;Fc%Q3+_S^s5$xUqksxP0>szKpNbCpW;>K3p| zIUD~>InU6*WiyZDv!!oT@1?$NKkerdzra9H7#JA68o+0B#0A>lmDSP!F{#566&1B} zaM6mok4C4G2EKd$Zp3v9)b+HP zvXiJQx8Jybl`UEj?^@FbCVr>;3r#gI{z7clrkMl3N87v0)|uXskq1^LiA((N$qg;H zdxoqVI;FCad2Fk&`vZv{HS;%1yeJXxX`qXu1G z(H9K&jV{-cmNGz-6A=O4*ZuVOHqKNI=Iwcvq|A-8faNj=p_(?j=SG z5iq2f#f|enOwhK`yRAwz$Si0mgz{=DN283X5x1U_6&Z%8Ox(SOc9p}%D;MM%$m}=W zP}EMOgA469(+}H9TeIRn2Xo?V#j6{Xc$9N4Gwc@XkM!S2jM8>)uP8F*M%PlDut_bk zdCoQ}K6)ueO9>P!E%|;n1X?lMyZcgJ`6b#49e1S;{7JrFM-*~Y_+L``?^3;QnwD&= z+p<-V$W+|^`SCmAjtQ-Xlknae%!gezuNuadL&kZji`AyKiyy}l09vy0`S=J*?w^VV zjd?U}KyTU3aNJu3Dp$B8kFrJQuZM1Fmi=9OfHr;nm~g57NM?DWhQVnDcs2#0|dB(qqpP)$caZaTl$h``1|SCW=AQ?UC=jf4ZPi zueEC*8&LNyigxKO>~@$HR0YyCnlAtQaI6%1_qY0nWS0_Ri*_;2rlBLUM)HRS{k2IM z+#j^PV_{YRYCd-?W|(TW5~SDFF$GJ!i=h$F>fJYwUfHUEgeE5@vs}fTHWg)hfuh-%)m64=61Z zd7Npy;~^5UohC6I0xnvW8Th1u9l(r(PtwgW#!hr@fM=}WISx09@%vQ<3mMMk7kYmfHwzl)+p>P_c1L88a7fqnff8l~y^r2hee;N&D7QnA-#lJA~njQ#VuNU$$b6Z`(u;*9t&mY} z)aq9N84VR}YyI6o1*p(^0sY%~4pL%_aa&?Mp5>R5u`ku7x&OwQ>@5^hbaY?p9ZkD=1r8KO!=2TSu-}pD}NT@KdAweJS;ChqT^{c97TN$eC!z&3|uY%SgrZO2#d%1l_@{u1m1^raK!0- zZy<)~QFI^F3ef&kNOLZNW`!+Ye>x8@3~}wBSosWEqZyF-d<`h)X+^L zA~bGzPQSFkAwoFrzU>HAAW1LFjkANpV#D=09k$oBx0)j>NXOj(q{O}q}4K4ZG`fkY7HV5XF)H)Ix zh&m!;Ty8H_)#&lQG6M#-J`{9tMov+t{1Y`M@Cs!2YzxwYBe|5i1$e20F6o82&6oQ0 zbjau?ex6SdLe{KH)CI{ALC@E4Nya+@JKMl>DIe|P=snA&S~@OwMfWwHaL31`fSbD7Uhl6E@Nx$) zWo;3~teeT82Y6l2*X}Xd_Gn%TFoQfkGQMqd>^>A5jLnnIBnEA=EjW{%(wAxNXX1$9uEu-v^VwG;Quf70up~xxGCj97G@q(J=pDwyidwxc^tQ3C=QmH+d!^ zSpv@t(vQJ zfn-{O?~TP_AnvUE<^1J1+L>Woyj90V_+^Pvm-_SIch`skYC>~WCMSLT z=w#jN2Uhgd+4Xejp-eK(iUAu*VPMOT3p%*;N<9Mu&`Yre`j;^*QTzCQKgQ8?i z1`m?utl_%+Hny?8a%XY|lW98+b1BBBfhxw6xf#35%fQ97=;V%%%$ z2;GL@U=(NA-P;}ICJYVKxl>||V@CD!-OE0w9ug1rqxwR&cR*o8P2q5%TrK-(|6~(i z$Vl}Osa~>xP_4gYmrp>kx#c>q7fFONO70bcAL9d|L01-JSH@`^Pzw@gI2MCj^Nd9O ztn_eH!#$DjUWo43ytwNdK@OC^W>$UnF15YqxjeWHoJa%ZZ;$Is9NEr8nC*!mq)k*w(5I?hZ{96D&WVC5<}1(vF0PL z*T!dduov;Nq^5USuW?InGL6j}pcz6evnrg(=Z;wBGGs}|fK*Zxqt`dwa1L`6q$aqU zoT=Isd!`i_+nENEKZ}PSLuIAg1jj9BWH1=#!>}i^m1yfDvecPB@vZK8BrQ$*h|zc# z2CnrKqItgdVzfwCZzY8`OmZwFG~*dQ{vzkdXD!r4+p~B4C$!>{+E!bRiNRC0H-$ej z%U7Oq?G}{DbkXLo@`w~`_9vRWusJf1Siw}LZjuDA^VJ}qVoCBmZA*aS>C@F{U$N?T z>G~#%!S-+H%&2>6Y6zj9wt2QwlcWj*(djxr?$1+Uw$1+NCtrI7sSAY6R56~fc7T>E zOwE}hro;$+rnw^cIIPX!Y$wtKrNkP|4P~&m z*)&PrgNQzKQn%eS1h><6#dxZe3m;pehQ24)*uh@(PhofvPxKNJIHB4Pq=x2_#3c<6 z)`zKhHIc%`uFhi-tRT}#l2WU^x}c%@NQ)cje4?mX>>ys;DEYMbO}3V-zW`0^YSPDw zD<$w017XKQQ&?|dlBdRq|Igg#n8N0dw*zqnCC&3R8QNFwAQdM>-}&eW}obw1H5g*bJ0 zTezYpx5@O9ihat=>6RuAeY!PcpxMCiu$b z5kx)Uaw$ zii%W9OQop0Y(e=W^u-k$;eB+~goB8y(Hk3%qOKRzUL`N`tavE|7lPOmk}-5>e5(~J55Yas$r(0?p(c&Pl+?)?e8y-{Gr_8ujSBXn_bYR| zFb^{K4gZIi&!F`3L!D}~M{|=cQ%bO0@Ys=x>}$~qwk`NJiV&|i%lxDg+jfy8EKc+N=f+*kBp(%LQ5zW_zB%U)v}@Xu<}Qaf9p9 zFGgO_cOm#fJ{S2BN-bvTkPCH}H4+jO0u;-t2e>z{R zLtMObHW*-%x!+8sLjKPehb)D^F6pNCI&rjeeLeY;Dq;l+j3`~CCl=|JQF}UdFc)XB z_$ArM=vfil9HW~}tH&SiXbH$@ANn{{^uDba2Auep)(Yxnpf?w3!=#xKi_0JxJY1ea z73P+3|5`h^EgxzaIoTE2#jBu-?{I$IZ7A$jW22+yzD_ikeSB)z9VI(Cfi2CDo5gN} zy&@K%(^2bDAsQHWt~TH91IhQa=%=&Nn(JNU?`Eav-d|NIP+SWfK7f>i30qeQUWvD| zhQBZ67MVB4@0Tqcxj{i%AIynff;s)=Qx~7Aar=p8Q?kQg7L)$Wf^il^EYkBy&y)Rn zH*mRzN#o4Jpu<&HU5+VmI0BY z;N-3Q$fA4j(;&AQtR^vG!?Sm7;&nDd!O4LrMggftikPQvLwwuJA*;0<=a_i^D5eC`qOl_J} z(Q#xZq$6eR-C5A$0%Xb$)UX`i>DRvK(t4-|?ri{MgYf!9{NQ3Ew7uu-$h|HrOM)Wn z4kc=eYLssBoLq^~kfY(Jo;*?NdaEN7`YYBwQzF;M>oQSSG~CsI0MIG=WNMlP^7x!M zV1hIBOVPOL(rm*E__F{=FHpPnbGpny`hb!Gg;jaBr&NkzGAXsWA!0OP(xWP`s(i0_ z)EpgF1zas9^W&vFI(WQTQt0u0!m4@wMX+6+#ASO;gRYL`n z$Al(@<_Ry{9(pkKteoiwc2uGT5UGM{N{TjQp6i|{2RO?p;5 zrDKWse7dSAHx)6aA^`VeI1^^!=q2LRt?Ds6rG22c4-S^@XJP_6ZQN2{zmTMT$@6kB zvf-GUv9#+uzI(yp=q9nRhLgK)W+dECXP&K_{;#heqSaoNwn9R){Z~CX-#?2FPv^Jx zS3U9bpIeC*Kf|`<4;f~q_Enb+A#P(zLGRt&Cej$NxM@%L;FQdmJCk~7$X6jnS#+Q5 z`=hcG{n)rm*P96$nUfUCaskGM4uPX0s_vRLXPHU{q+4C5UT>>$1<+*xyt|W1RIV(~ zb94Mt$;JKjM=1(WalPC*jt_G!Uv_wA5)0a?7NWBR;(fj4N5QBM5LJX65KRezny?v- zBDJ5eF~14v+qwEnQHED6%wXR|L`F}ANcG>O#46MHD#6+!@+y%q6fdih?ukP;B+)a` z)fXEp6!%aA9?=jouA#9H65*N{>Mfuwn~ped52!6_GfjpK)! zuKh1PQfjd>lEx`6B1-BNbm$X+S#b1Xc&rGdOA{`2y6o?I7G?xv3Tp>;WK|mIWvL9s zN7J!T{Dyr0_@i8 z>OFSBN$4^ox9Dj@H2YONS1*XT_?*aTq`FiPCa7K_I7nU+uE-F4=GGv|9~}0)bh9aV zW#i}x`JOD8V5||K;H~uuFtV%K<;%MAtP&&;fJA$wCz!{iw=B*gokpi=>&L}#He!B)i<#RHIboZF7D(vcuHMyTHWU6 zT#^1Jsz?XE?J5QL;DL0LVfBhz+jOllY2wr`lCohToa*!^kxlKVPl)gT3c!DCN+(4n zY;cYG;YqP!kddNSW8_ZdhQjurmj7pwUk&|g$D(M`+zY3HK_Kzx>W|3AmwC<-dzVqh zM`}7y&QafrGEmkrw!?4vX81B^Cb4#U+ttVnl@c`DHXRs!%=$z)*@$CyB*_%^%*MeQ zFC9X}q>%HC-~h#7bi+zsT!_>$+xjB5y5-ABP%b7dM-f9q!?M-{jsU7h& z*3B6RM@q2Af)z3{R=P?k(uBPxCl}!;GV>&9-UoR)Zjv^pM%!D_ws)I*70}IXRbAe= znh$E1RbC3TOr`IiZbRCyShH>K85#7o6I2`ZX)ZgwU_L>rM_#bN3@6c;wMeFe;SUe! zZzR^rq}NlrvTrgwZtQ7hN7D_;q;%h*wIbqz$r^X7H1uArakv}NBakO=-n_ja@@l3+ z&i;J6BZ^TmvX4r*fj;6x8^6H`Pxt~Na<&$Q*p(Gd3u)U8O3annWS;>Rjqe@OyUqiC zpv0xs?oF^9+66}LiY1wW65dPC4;pF6@SNj;t})`;bGNuLc>fA-R7nu3W7`R`;y&!x zLBd^?AXzDwB_(#2@q z9v)!-*sZyZFDo(v-Sy=r_w*8ly1$3K|I7CRqoQOp*+6txe(*QTGa+*^Dil-Si0gwy zdyi#~dk!g$W~U|d{oeh{WB8dzQhOWYu2SKdX4*Q>v(>Plhv@hFzHdTtOeWJxe(X6u zash16LPuKlcDL95V|~!_UC$96}9#~-&01qB6(lzc8O2BI7h zJ}w~v8#Xc;c5n%^_60oeG+hgRV&rZrllObhn$lZu=C(h|Wj@PU{>iC?wc$T?Ei_b{ zGDLG2zCYLM^;}uVf>`2SXjiu9+13vEocix8o{4IZOXwh2@JbS3+RB5?kY=lNedStN zJns=uhG-czW}ZQ4U!%_xa(qT=qd}CRHg8_q(cM0}59deQA)bq^h8MSPGRcJOtdy{8 zFCI`=`4M@k*cw>hLJ+K9CWZOxUQKV2kGaxG4e5Sf^iO-cZK$WaH_4cMO|~!Pd=L{M z+acoP0<9X45Rmat&?uSDyIkm8>?D;9>6yDHWMTq6qvP-o`fg=4Z-+uK)C|b7Do7zL zV%cG_@wR2o&auZWDIyI^o*Fku1S3n>sCappG8rCVe|w@|_3|Y$NMJHLE`laafS;8N zQ3x6NCdl`Fsr&)ZX%(IHOs0R9k_>1)@H>_UEwK=78(Tr%H2D@qK(=M4g1h-8M*pxt z{(L2>kg2XX;e&GRk73*@)>KF1K_`@#vVtEhd~^urltc5?Ze0J5y|)UkYe}L7EsL2M zZ83wz%*;#{Gc$u_F*CEp%*@QpOcpb<^v><>+im;Cd`?VE#QW9J-c>s@D=RZ=tt@6d z@tB`kVN7u(PUWfL-Y+{$FYVWjai7$48I-?&bl)p*l9jXf9_GpeQ9PeP((-j9eBNgv z#)z|9ihj*X2+NTcSNF~I!ftDSK?M4`GE^x9{W8E6q!nvK@Uy-u1{FY(IrS~9hmJ(U zouZzFr`&rBvviOAME(ivi&t8}AWq3B)k=ZK7&}feliW@|lIBq!3ggeE_a6)B3W$?U zuv9|I4-$|z6Jj%ncGo75ZbS+c$(Eu;_i8*0(v5lryR@4}WfDA$BuACY+L%fiZjdYF90iEi*@p3bAXb1(}4DR*mB%@L5fl=f#S68lzlmWytN7+;vLrZL@_@k`DsLZ4}5Od=gfzf0njU~lJ(}* zgweg}BIJv0Z2V0fgU?h~UgXNv{bstnS0SGvZouWwaGRijjCLHf`2I$gz%m4mG3 z!|jxb$;)lJpZ3b)G2LSg`0LqtbdRSLVUB2#BbJ;=&oLFU71>@0zF1`PTl_?Px!9pf zo*aT+Yw;6X@;1LQBht(vuI295wl24mF*b2_3FvnhDe%W$LDk^CyygxB3r1Fppx_tl6(Semqs%9iyUF)= zP6I^@kDcV1Ww7h(Ne`;%oGd#;XRk()qxD6c2j7MwodqP4HFqOybyeSI#8$6wwOETS zFqWsV^U+S4h+VAp857Qe5(yLopPL_b`QqYa#lfcO zd!c3EU}BOZb}AorkSt;jFJmRVJzVTli*0k_dnuB)uKQCfdS)@NFSt1 zt-alG))__$i!I?-InC(MEY!ma+sw0_9rz?LNmJBw!%E_H<_nlI-1f*lcr4kuUoSk9 zPZvPqYj`r;wje=9R6ABJAX|xd@=HF(agAJ>IJ!M5y%r9;&sN>@!koE(xb}X+qD;rx z+D83;w%`!Z8-I7Tg!kJ2W8~8=@HMF_!IvVmAw#vN@HXfiH$Nr3X;&}Gt{N>8SuEc$ z)RL@Y2AK71pBN!VK59Pcpg3*{!)8M_md2scaxBN>I1z9{L9e*W{C$mCHxB2J?Ch?Fz; zz#?}y<&8l78j?7zoTr_Fw(~QkLcAkRrzV1I6t1Ydb8A8Weao8e@fuQIxWrdmRpjPq zRX&57mM@BOjGuCWu-pr&+YPP{8QxfDFKyT5eDj>4ya5D{&+@WirCfpH!JG!E5FXE& zGD7MR6PdJ?a`zzyDDnO&{GKZej-jMd7&YYitPyTzudM%lor7q9mCtC^()YU`=b?+M zf>A13`btAr#O{|WD<7eD95Ug|^jAE?A0)$kJ}O+GDSehbafH!3{fH zH#{PTu9}_V#M(cqJMmWFSx(sFnZ;b<{Tr^6MGyM3W`&IhbMZbkJ|0v>MdjHdFnkHI zUdjgyIjU4CMe@f_Dq^qlE8QBqD)D5K6wQX&fu61m%rXy7?S3JwywNF@{`_Sk{df}0 zPw>ff{Z$N;@kmRJVf+||qk5GUp%&%>5t5ZZNfSkiUP@KvHA8-$@DI%vZXTaPWt#11 zhC+u#rxEJ&5PvhlJqhUUX21+6tkM4dBdZdnPZ?9Q@3!>V$87Ivo~)`i5hbuS+TKaiVPc0da3?KQUT8UN=8s&V30* zJscvplM-XI!grNky@J{WZVPNda$ZhEBA*%^W~bgAsczH$u$n$HV5OcX$Vgi8fyOF> z!KrFhoxEx^)~m_YO>9#8x-hOkPNF_H&u|c?9?b=OkX1fh?T(tb^=voMOfEwv(cA@X zh>0eB7b_iTxQ_2wl`4ihj?bkoL#|zrZseBdOO~C3uNJ08iQw8C8srp!5aq|jfL6oM zS$J3bn%^%qy*med5rRt9ua|3RFv2RGJSbAWaCof5Ef;^~5RH+V3o@TG@3I~uwVN15 zKbn}eSISK}F_;J}zW2iE$vz{!OU(vH4o)*N9?(Cd@;&iOg`;8k)p2sdXIf_AYD?i@ z@~>N;*TD=|E$eumDITk{saT*BI1f|DA>Fj`pIL=d1ta_~r^p7!66rIfZ7yRi*YZxj zLW=IHm$A_ooDf%8#c(l^=@;*MU~%qh4qe9Y8URqn3g!0Z-(!ulMhkI4>PA6IPv+NY zn+&9_N=NmmV$I24?czr;4$W^Rf^$#yvyF(XG8M_cYK2yFfPjKa5KzZbE!{A@Eg|-A z^P>!!4@r;^mw22io9&8u$1N0#)7by4ko>8%f3mb|o;cJig_*2&hxu)-&12V-biuN` z-k_j>KxMsU{7mcO+K#Urqsi5!OFqp{CBq$?5YGcszZ_D~s2*h7jkFd5uF%aJKW-_Q zlzQ3MnMC?qIV-q7v2f#%VRozeX%EGD&P^B1^V`@fut)Wj#oY3=NBKjPx z)Hrq-cy~ithwFEdA>uWZU6sunj_8*Puj&~{RGK(LYfCxFmQK}g>*wlK?v#u>4APsw zD;d7-HDf>{U3Nb#(7$x4*qq<49iQyA=kaWXx^i4>Id6ytzxt1?99#Io{|D?<&g;|s zeQsGuR%KK7I*N?fhf*QSNxEp8ES{jw$wm?X17r-AwdrO(twZV6OC~uf4Lk#-@9c1n z3eLo+gXUx5>pkc)H9WVuh1~TxXg#fVlTVr1U6$OLXny)>$oJxl|gO<3Hd$6G0#&Wo*PmDFFB@QJI$-S&dxatjy5P z6!q~n5gn31X}DK5Vs7C2ZCBZxG0Q26TcW5;$%xDKz$qf`6JKlfM z>;U!;rVUqRBLyfK07({?CHzq$9Hb1kewMyS-nXud(crvBQr;lS+|Q1!fmc5>(EdDm z;%{ss-~m9@FDJIE9Pk(PPd02|%!&G$wA-^b$p3}0KmpLow>d~P(TwISg!*B<~l77mO1_#QZ7YTaZ3wFt$KTH>Jg{Se{diF z0;#G*ej762_ws6g{f5o1vrGC^vhV27hhKzL*xc0Ar9-EAIWP?T%NT%3B21bvR;riX z9rXvmDYJFS+q3qZ2>3#f<#i0F`u}$k{7d;Zhahnio8j~@k;!wRoPlTx$+<1i*^)au zpA8G#Eb9n{Q&; z)-5@<&N!^Tx9g@FoBv^3H0l6;L1?Z9*Q;k3))z4S{)oQB>xMatKKbv-JThJHB$E6V z;y#?lJ^9HUo4%0v+a?y`V~(j8T28Zw9*ao@J6m*Ud|^?;oG);rp}4Lg_5G&LPg1 z(I>bs31)?g9*#!Df_N-D0oAsT%b*-OYpY8!#9xDuiwdUlrASxZ1_T<9_p%GAWFuHB zQ8%O?AurV)4<-#U0{qxVFea9}>>ob~;=KX$czpp!YlD|$6MJ}QyQgDVM9f{Vh)q zG?LM^03~c^@k+NDf0lAp{ejhBjQ5I(D9icm_%K~Arvqn;^=2UOh=T2X8Erw`Uagl} z18mLah$`?B?0xBhlShp$Zz9x&wReIv8Qi+WgFXL>}XMrDr6a+%UY1?CYuD9^Go2sO8#?*ob_+b-E#`0clz_iur^Lk zAMF(A&lB-wPv)h*&%r(1_%2-RMuuiPU-)@{LuW422)jL2`9x@pq~+qWn#sHw@`nwN z6U(Zk*_As!HbN|lGgLbCz+q^rZw5sB(w#KZ+sLa`+P~WUd9V+TNB& zOic@`)#G|{!UMxsfXN?=rYh8+<9>T0rP8cK&~AHX&{TR%YA~s96MVei{elo}hefUK z*jL*$Qe}Jl@y7H#1j#=9X}8#(^Jq_`%Hs(9qIS23trZ>3YB2&*Mmw~YaHY!Rjlu+$ z&&9FZpj4|4$vUzF7tEgAD}@ah(si$x`OoH7oqbRM;Cg}P&n{e6F9y6f%kIrArmjAw z583SzK{1lMZ~DRL7E;t*#pEooLZ*L&eI6W{9i^wtP-RL}Sk{8|RK3*b%>&%)=8J~PW>F?WgkOp!f>nmQ`*sfc6 z^M4#+=`pI`Qq;qWsfLP1!-NkN_eH$t^v`ZvCd{>T=6swKm>d%FIL3&8^34ID`)(2$ zbV~q<1*9&uz~-4AuZ1KILrv&ZvkEPMuY}+-EID}3`9jpq&uPQXjwd#>gwD7L^@p-) zhyW`Y8gkCfYo)kuuHQay(~Zd!$hm=gAT|B=mE( zGj!iKyi>W9D-KDLrvAb}4hwaWq}^(Ew{wEkw@Lj<_JB?Wa1^o4;uuek=SCIB!KFTd z{-k--;)Dvls z(ZQ8R>QL8qx@ezRoEO+7@{X4S{-99868=Sqj!1YhC|zn-*%niQ!Dqti;vSEJKnSKE zsB&Gwd#SP{uGVS<=1yteo2TB0o-_0J4Wo2)xct65D^~jMN!rZA2~{A2*J%YQ89!YQv;!;%Nbkl(@kmK11k38q@j{ixk> zdB`5vnsp-jt2LH%e2uJqjB<7?OZ>|3X)gkMNlNT z!TRWzuwLVNpU#mA`w0xV8uAq1pZbgaS-Q1RBAfg9>eM`Yi9U-ekJh*D&s^hW&TwYp z=0>UI=-q72OqwyX%x3eTOtW|vi+Jb`hd~oYZvla4nBPmD@4NQmd2}OcZ=8;pU^cXD zxY@Oq;j1!fJLltTY*)rG3vrVpwG%4*h17+VxF}N;&5KfE)2p$ZH<&;gMsDj^?vEnI z_gQ*I^E56GfO{rSTWcyEres7D6V@`?Xoar@F{2Jql4eQJQP4h_ZqYt??s(mps61Dy zzgKO#6A@bq>n$YknNq#I4$#tDmSC!gR}TmO7NHhuej75|70#0=)vChq-;=#*%>qA4 zBq}yD;xD;lZIP15tyuDT>>v`WUItcfsxS+>p_v;Y5Hn-Ypn`~J8C@>Zb7*v2MINL( zq&J-Rc%gG9aj4cTkf`V;jN}Rdt5UX1uNk)r-J2F~CLZ&T;NmbnuQT=JPU#oY$rK1Y zOU%e=0~a9c=X(4IvtB^IzaQ^%93F00W(q>nW&zR9{X{@D?XX<_Jm+#2jqCE}i@Df# z>WngP71{#6_8oJ>gL)xX*#l6F$VcD2>k_y;oOKmdYeO1oIqA!yWZyhQ*Y>#_+OIO{ zGa6m2!wrZs(fs1Ti05jOtDMjwPx`*$=8K8<%;!N}HkMe6?N!(}^Yt6~jB%G~b$20; zgIWXE`G$~V(F?> z>CBkW@5?@`-w`<9hs_V19(f(imB4LK_1_vcw)g9>^PcNbaa%NrF5UP%$WH%0#=W8jRI6fbFS}PQP&n2qh5HGnDbT+sB!}49%l(Wq7n0fv zih$&^l8(@HKN4`T2-93C5|Oit7Mwi?p+foh_rO2E%%fdm3 z%N&TU8_k4(YsIf@1a^~vASfb)SH)rD<-~kK17B)~BN*_6Y?`?e>3AmsGpM(jwgnKpJ9A^#16Gp+gNAFy2g8Hw81>X;z5LmJ@o(Cv8O*-2`<|* z=5I1riKa~bil`PapTuQ}6qDZ(+|ZE`?9hUU?$rLl6KpMXWO-oc);8zjsdo2ZrGl29 z*fgWzfMu284m1qk2^h-h-G5yLPB6c=ICG7!`J3}ua226dZ>s)TcSFaiVCH(FIIjHV zTBL;S26Cp}Y?PJAvSRzMjTU?ep$bP(YO1SiuT64>S%xJ6jnbD5eWIh^J&H1Wws`sY zfVQ`{hf6&zWGF*yT`VK0*D)I$TpXynBP-{0r>3GPr6dYW#E3?ubz^c9((#j22)PmM zm<|z1(uvvw2YkkZk`@VZK)_;a59YY|soqzrn9OmRJA}&~dbCt-J~J?*?ZV5XPlff0 z4;Pco4_#-#Sw+IOmA611roM+Je@V=_y}*;_qBZiqgEeb74hU`O(;JH;qK3R5?3AY- zZ-Z+H?||B`y@CI6`&3v`WwM1sttO@3_VG|x>T>KE|A)`X!FSJvowf>Z|CvLf4i5bO zu2GcHb%2DBwdNu2UT+Dx6Q8fSuhtVWrdrbPv?3eu271n=`=u5%!<%Dlu*1+anO#t{ zaJ>uW-HyjL!r4ZcH?N+x`t1eW%#Uwg?xcPWwr~F06V=C?u+sTl>twYF84?oGWWF@{ zX7Yw5U{`c!C4pL_*5LM}W+Fi=2NhoY$$FS`*)gdUuM8!T;}paLkA;c}YbRx7V&J%@ zqq9vQ@7Zb3O;Woa%ej>iOb!!zE8bjVPING!GNp0u|n6<=cz75}EGg7(Z#d=PZe*jZmzu z7mP&y>tC3io%E1`5;+-$mHmc?1DFkvJ}nz*Q;zSM7?y|vM~ zz1Tlcnx?-z#eGj}bvQ7rHK?0qL)Q+h*l-8lU#QbLV~MqSzE#<3P8}emUdZnwdsm4V zflFNv|4Od0i(XQ;NR&faFfup>e2x>8!cbkKVmQR$%9SBdl0rzo6@E|uHvcv#uH~PY zhkcoce%H1C<(;z@Ci7QP|0V{ABKn4MnYt-;!BKUyR4TLXO1i>dh0= zOYCkl_Iy9fek5giul>st`{=}#$(mp26~kn%xi;s*wOu1FGtt;PngE{5N6Ln^5XxON zL2lEGxgm&gAGntA&pQa8d?{ePZ$Q%)E+!#{lMmT~$}oL@w6(koA4LNtLwOpl(%;#x zezL!wx!Vp#WYHf!nV`EkipZ;aum8O9YfYz<2cnQZM6tWs_1Xv%pH$@dsRU_`L@LE( z;$3%+BE2tRj$+sFB36ZWO?^>1Yl208vs0qZtfXq7Rs(K`(maSL z?hfbW@;P&{#u(j5+6xrl>pl@?&%h6B{P0;!X7dNPir(4P?X=}*_@d{A9&?CTpT?Kq z!a0ksdf4NOnRD|fg#*D7EA(tjDGof$M}m2a)$ehq$MnKT6F|QB{BG?c>K1v=RdMIm z+P9Qw=h~5cz;2uE#&3aK;9sJXqeH@8ECd6HZ5VZU5Z`|xEI%ZE^zl>rnp#}0%{3KV zybM!c`Zkh2G$(Bd)K+U3AiCl4Ugr0W`K#MbwN_&H%z<8Q8Ge~%+K|D#Fwb4HkI~m> zoxw7R%S-df&5~f{S&LOf6-{T@m>QrtxrTdK!YaKPeT?_a6$G=YVUw}WN_v@W3GDZ8 zDr@c=-`JoRHhG-k`wk#8H_J0lFRqTQx3@?$<5qyzph~&|EB%Zb--G2ugBS+6bi(aU zc`+mn1MDLTz43(MI)DLP4?nPsJ|l4(BOcYYBgk;Fh7BDRd9qgD{dWi6%tereEWwf# z3x_I=9a)pU>D5?BJK*ZP@+GmMYAC;S8WSm(2uwKT*e*^D*epIfH2}%tY-QSl&CPYSk-LXez1o2}u^Z6q9-+;V zbD?j4v0%uT%Jg|v1)deA>rRz=3jdIn9@UBqq3 z_M%278G@(@RW|;_OoA0^>;-dvHip4|!%WnRQR%c4`LOvabGiRi7F4!^lBi|Nbk^ud zu+9Gh{7zJ%7EFBXw7t!(k|oFQ5=Dd&@Y~n0#)7hTPK(vvbM>jY+|2Vm=sjZj&581Q zSYX_pO8N|_<6C<_F_jSjm)!!{718V`oFVRD#>CipoA609D zqn~zrd;-_r3~aK$ypU4q-V_^vzfSnzMmTB|x_S>`Vxw&BLGOs?mHb4*4_DqDN6tf# zFU`^V#grmnGCiU|WDUqum%_;N8dg)@Sl@vf2J;qT{qZN|Xa*ZHe)|m>Qupd&{XVS$ zTh7H{;tXA>$QKlp72BR1Yn{IP*eR-F4E)wx%)9+(o99Ipde7qCw*zX%3e<=`g#z84 zam}JwOh>FaKFx3pMib2(C`XLno2jwJf-J&55G}7fGss8pTy4kH(Dv+pT{dBAIre9v z-}IqU75JHvE**H6u~O2Lo^#&izh5`2MOu~~`SOvLvzgQ5sy{RdEj#hqGrya7AvynS zBLW87#%scdE|8{oSSj>!b$e-$wG!AW785L;uVu?xQIfj1S9LQs^sD*cb>kwL%k{mE z246<)c1oqkB}~|QrR*L$;mWCi?YQy@cT~uAN^Ku>g=Y}BJv5?9$(~s5Q1O4yFaA|A zFR1h)pcEb}K91&c-v&-a!iMei~dQJI)0g@%LUR$#uIN23O7MumM;u zm_lSDpP|xbJdv&967XRPZpNU$;Bv5$-&)&X5@(Hf%{94H zl9H5kp>ajHFqzK^Cv^o5L{?B9je39W@OV6#WM9|gM<=b${m;z6~ z*#NiLD8goEt_UU8cT5#W+O)V3^&A;1)9FNblZdV zI!a|)?T{$hl3Kwh%C+bGDb=P|rP8HV)Q;CCu){t)O{ZrQ&h*`GcZGeO;h*2h`lJfUO}NO0$L3)Lk6+$=${ty5>P%Z6z%Pyb{ogt z8)E8+zNl`V#x|IsNViGEJQ@0(7unE6V8jn9{;?|g%uAdT^c;F2dHJu zOEoD+TzPKz2%hv;W&g|T@X~=s#93EcThqm3hW;6Z{BeR7%|rRbn$ zkCV{BdoB&av(LIp-gbVi9s!~s0*sGJ8KuSUdlE|IV zd0N^sVoRM$;eK<7HWW~brq#v1aK(M6Yf3Y8uCMolPCrLDniZyk9l5`MGm~IC3_plj z1e0;V4G55f)aiBup=y`fF21dA>6&dOPY&8%Fl%F`DzpgEXKB|9&ONIFS#<1fH>_#z#MF9VmL>^)JD9qs#L_ zi=tFqm3S@Ja6cS12^&4mzCeG|_`Nq2h*H+D2p5Ija`ub_J&rqRA;*~kbbh&%(}Sjs z8?l(7%z3QkeI@tbv;9jUth*q^oN7v9Fa=)(c)v!Dws{(zm|GG5Dw$&Stl`+X;`I;x zzeaI@ZGzWlM&{el(P4v3lhCj41Z+Ceu0SLu1urx|!T!)X>nli7X@=KxoW>tY+l)g8 zB(B5f{8oRx`4_1I2FV&euW_r6arMM&8VlL4JUVoA1E}^O(q2*{(v z@tRLWhp!ECaI`HHdU#qNwf|U-tjQqbg9b`Dl*VfN^`u!9z8jmzFq1U=` z?!Co~a%&v3``0`(iK{HiZ8VmrarZCx=(kX>R- zkrZOKVj3ilWiLH~523p-f|qzZWc0j3TN&sjRQCP{SNgX+3Zi*!u->G_5WI_7osSbS zURER;^&gL2tk1#Hlxl#r%sR^~lC(fdq~$E-jp6wsv0#bi>hyQli7`4%N#sxaVj|t9 z=!(hlELbqO<~%Zi&1c>_LyzK#=kqH zA#b27G>7rlR(z;YP{}efpj@5Y24jr-dDpL0Q&m8R3u+6C01PeOyYe z{fDIs1X}JJ&E@fCb-oJWCq|eBT6ggLsDz|h$JSB!ftXiH*{n#u_ovrMl4|CscV@^I zsOj3IDee}|jnLPK&xgXA?Z}s=4vKaPzb}$6TAN`q$^y61q7cE+R&Cjh^+QSrNZ9_k zrX`gXjg#I|#YDyM6)uRGdFAnE?b$BRKlTwzD0^Q=GW+(6Qgy3mdo|WFyF1<5Y=#nC zU5+N;r`r3~ST~ZwM3mdVmBHbHHMsI3O+mE2?wh-~?IRsx;e;D=L=L3i(sVy=eL>J@ z$z*;Hy*=OD3WwgBPNpke6AiPP#VV7s;%qZR8ZTQy+dqn>7uGwoP1a2yN=5!n?f^fc zvF)f??wHA89A}xffLJmwg#&0q`oTu8UlX4;VNV3WV126LB zJM06s6q1rJs+9;yK50dNXCq}Ev@J-!!P09sQ3h6+SjCqgUnPH_Yzj35c+!{X;$xN$ z`7TakU1flt680dXX-G7GMBl#lJSLK368#M2(Y(baxn?n09$AN>H{8eROUuOll_z+W z=C}QElg{T0tJmJ5`h+8E zo7^_zR(1u2>O1xKSHs!dz4|7Si|p_hd5^6hQ?8l*utf!p&9G4?MHP(}Qh3ul90QK8 z>Y-+jYlR6zwm%8R5(w8kUKbit@>iy{Zc@xoS)w7)TddkqA|BUf$g z#xu`+^r+Woc3R?WEWg}s{9FZEZ}bxJ{QmTzC<9`3VUPKx;q*h&+Ve#w&s(6QP~T!^ z3s{e#PX3rQa6nx%WMthTc!ejpFqJnjCG?d}0#G{l8aH%HUhE*yQBks3-(gDzv8jD}3}<4UAZaqBgrD0ac?7D09`IBXaj?NjFOZ z!lpOHau!E?-IlPJG^{+Kxh>oHqGpxmttSNhm>}UTIh&S{DkfUgX$6&L_-A`>$FF9! z#8{UJqKC|B5uZ&_9VP5AEyMX{f|lvr(x(ksQI}|1N_VVRmDeH&$EFxM;7Zw(Y3XZ* zNAFFJ!S(XRMnhsxkReYbDSNxkH#(Ot#|(4|uht4>48~NMe^Y}^HB_Ml9xfcX1v56_ z@Kn6SwciQ&PMqI#2C29MJ_Kb_4w$qkI~Goxzao4-)sTmvto*soRk*2tt~5nYf`lo= zCW8>3?N0L$trSdj+VtLzI_mx{W?nPC2D{GS5urm|-Lm{+>Cm$3mWLzH;uSsc?&Y7| zG!IE^$TZm3>QBh(_b-TU&M9AJJjx8~CBLhCSvt0rz`%Jd{`5W{ugy^rjn!_?fx;T~_}8xU{|aylY5@tjg4=!7)t}*$%wOTtzSpkupFv0- zAZE^(Zi$TlBNnr9gQ8R8;<~472>Cr??4K7PCmIzCp|WkD03zc9aNl8bPSe+4XIjYZL%1>f zMbxpe$&r>S=^gz93_Hw7A?gRis2K6)Z8mEI0t-_s3hWQ$t?1tPKb`&#;-nbmBG29G z=;)}Rs3-&#+6a9^(qS%}Qo)Swe0^V15+kx{AKPS2@U6?;f>=ZO(Std2(qs*vNErJ& zNv|5=)+N0f*CC+2<*%WnPpl&EZWvr3Fmoy_pw)g}UFoy|lWfS4j$hBI-^wv$Yu~_( zDDNv7qTbbeQkUQ@<8sw1%C9b7(YG@?W{%<fb|oG6V}B3Vc}BMO33-iiZCVZdqUdQsvuolE$Cm zR2=eOk?<*l@-KDvzml)cL)uP*+nbGnRsLTm42ZA`(*80etHi4AU)A$h9Pa&>A-!|& zq5V-b0>u3kZI0p1EX6CQ0aGS^6Z;&y%u%SnocH}Jmi7UhSQgB5?b`9}W*CyD6J@<8 z`4kH>(bB@>MHZa6dHlnqQ6NITi7V#(_)#l7&D$v+JJL-?By_$#y|NcxDQL!OipCiHKi{V%}hLjw%_k-@B`>~azNJn|J`ToObK58Kb9D>7LK#u;?gOklmY(s>iRmlG(l;{ z{vY3*0?;cVA)#)Ss<^&#N+}u|TDMuLgO@sg-KegTb=e;w9SOC>3?F<+GsdF0~Ko{OYnTJ%J%5@BP9VU3b~<;8{~TSG2{3v|f4tl;C%gYL*G*P{ z@#2Bwsr+Mfz-=AX>P+Qf|F|b-;1j@EuwMK{q4{_E`1fFn;egSLZC^DM)1~hl?jQb3 zy7})vQ0bIDoDy_U2B%D!2tGQD9bb0t+=4Y##?q!qR|IzHe4E%wIK553fWk7+J}@+H zT(f!*FV0!O&W#B4YPm=#Dv>6=Wx@Keq++#Xoc8DFb-Z(=B{79=A<$KhA`B-niC{>| zIYVK7Zms|O;LM&B0wT={*@{|R6MV

8g9;tJgwW{l_FnuCotuGN@a(B@&YYvQ3>v6{@M91eNH|2(3kTf3RA|DZH@KrEj=-Oa}E1+LAQ$&mtSJ8 z6j$S~J34a@CFmd411Iag&h+evf4%WxS4L6UE9upwR)|Xo$uyea)7s&Fh-{_dC=%rG zc1F_jeEr&6cM=&*2*%5BHtm)$FLSg@y@ME(y>~3$FyTB{X~6Muq)0?b`RolAz>n}5 z3_12A&wPcak151kt-{F>;qlW}&i~B08lvZpnBv$DRnhVEahWA3U_9D45w9IwBdm<> zbme}ERXAqxOO{Y;DQ@O3Fkpb7<|u6nH;hpeN))(6w}ivl!!K__kuUcAROnoI;ZN4u z!6GBPfi&Qjb>dPA(Uj*T=7~o3_ji9R?#;%~#twnFqzw)Z@=99)c_2;ZHs=lJ#?;le ztFyEU0Ros zK&5<@g=DHR4m6rh4=RyRkw~NzBSW1{T@+fsCjXwsxum*e<*&einfJzSdOJ2W?mu=m zf3zE4-gZVF=`Nlt}k}uc&D^Lgw*2x6pWf6k2I` z$=G^vw^BLs22ry1(f6zo5uAB+fOH03lnHL`lPo&-AA2-JK5u+ODHugbxlDL)ML0GX ztYBlS>sno6Pofhr3GE~FaRX6~kO|}4^CD|w88xEScxOzWw@&BaKHX73XrXzP!jc4;YvNQ@Hz`rx_a-X*G7;czU~i^t=|wrv&8w}&0p5?xkh>NJ06 zQtAHwSzo$9;jPv2HTtJ6xk6HEUv9C+JL?Br>vk4IZz4Is&v;1l4&Hc(p*dO> z!%29hknDPez=)OVn>7ee)EqrlJi_S2aTd?7bf8Ui*G#m*2C~7mG<|*uJUR##A0xIj>Djx4C?tFEHrFLJXy}ZUq{m zMC(LffsqMw$IxYONO&pPc9@Pi9hB~J-k{|lBia8k;CfoCo@@Pz-0@Nc20L*Su#eY) z533JrqIPau`e2W%{CH#mR7LwZf7pi>$}F+cZxbH9?ZNtMPvFd$}j0|&^Jb=K6QSZoyB<= zOfOmn1Iz=bELy%VUrXpb$BhSRCby0!w7wH_VD1P5$n)IXumI_rmYaJ(*h8 za;W%d`KS_Af8Z`X2=ZEYWVkP>l@2liD|{-8Pe4rBzK*6DF*y99|DBsZGh!@{qZA#0 zK*4RPIc;=a)xhA9k5MuPGF$hiTu-h0z(b=Jjwm49gM%E59r%v!8OR-B+i(}db=38) z^@!qb7v^&H3fZ_{EPb&Y(9QI~F8QvD$SisVZ5ppx?a4#a?8QjZ!40M>Oxw&{5}DYM z5_tLyr#E>(ib!(Xx?couyT7wzYd>*q@UY6?N6VRAF^a2K;_+?hs1;7-dZFDE4;z;p7O%06cxA}*U!z8 zW&l05pPzQY*C@aF;za}hbJZGvG*`;Z`RHEh`8>xORR`_QfH$|dfv$hqtXSLfKqC2e zVK^JXsJm5AvAI65G1*LGvYX;DA+7%25@Y5%4n-bv76YBi;t_p{{~ zKl5izm`tH^nRqm50a3;?j|=Ivg;KCpOtp)_x&%jT=3Y=-b>21hJ{pducnvEs&FSK$_p4AMIi-aWH{0DH5!=U$s_I;x2&_p_sr48%!N{BssnsRRbw(c%oSwqj%GY?PmBsLRfz$#2;Z`lg19oeOm+9Bn~cC zD{la0*Iov8+W62b0iY1Uv_sZlONWOt1Rrt33)Z>dJK1uJhdhnxGsTx{duwx-s)&-DG}2=Bi@ly2F*GL| z(6__6O*Lk7-ceNqj6;NcF}}P-ncI6Vqv2xt4d&>9Q{2HvE)TXdZ0IoPEuGxk4gkv# zR(qKPy7nMLB66Pq*pO$u1o2YC_?ZsRO0hpK8gZ{g9D})A-lyZjtv!wD~hc$y4_Z*HykPKkLwrDxr6nW`I6oj`Zgs#hgJK zP4tto`l^r?u5J8I=9+>jWjN#Ok zYbFbwKWURR`YEUg6F!G%(%yJfpd7~4JZ{Rrm&k2P)L7L8ZQ#R%wANZ(kFRV7<24+r?{Qo;@Q#e{<71rH_7CwL*C>VX(m*Lt^jR& zrO@NN;pMZ@fb)%u2Ytii4c4hZEVLHpO3_Q6SWwu1q^_VIK!Tkg#3|%_(yWFn9B>0xlHvUL%5F9kOgDj4IkgjTX+fj-CN^m5?}N~ zyL-J$Ys#r+eeav&Rjrk`N!d{5!Rbi+_HQ?1Vi-JUa+*#f2p&x5EUm0oAoEnM5MUzA zUaDk-TlJ^m(0SOliB_Idr>hlJCU-XDh~)Zf3{QDJ(BevKq=uz-a0(K=$h z?0eTUpEks}Kj>*R92|SNEU&_FKbIXn_M$miYN@iB(yKk^`sn#|C)@x4hu-eb(4DWa z*p9S!AnXkaPA8nwjjTDZqB=|{xfU}~sS7{Db}jQ5=}c0)UraWVPUSjkV0eaJbom)y zETpk7Hrt}spH2v!s?uso!>c>!=KZtf6Hdk41B5*J{cPgAlC%*FV4bM313Z} ztc@m`ky~u1gao(KEW|Dn5G~8c@OYhTe2<5}7#!rd^xRKS?XWa5pqVpYs9lin&}39yj7#+LI~t%638*KQ;Z@=1_O{;tY4081b9uUT(V1k% z#*A&-wr$(CG2>*$wv8Fvwr$&aV(LSJgf48sqXa z>x`A^%XCh{Dz}0h#P7Wk(*c$%PwzUSFnGpGowKN|`!zFIy%r!ROvWnO&D*IT*PGN4 zk1)7o`v-cBZMZkf5@k(igu*fZ?T~Hnv;;3Md6E3D3q1h?B8yPxO1B*y+{%56@{-+V z9V};)O+e>*e67PU+{N^mSW4IOe)#Zku7)7{y+Q5CTv38HmlwjiahlI5hq$Rn$S7O#WX8TtFGMJ$@NCXw}_@Rjrq$Wbmue1qBRkUV$q3!c(n zwgSR`L)eciMOPZl=;q+BLMe(lhPb`4px_E~W#4ce$5V?xlgE&x7b&qZkz+I+9N1gj z@aEw)qUB&a;&GdLZb$B5D5*C_pkOF^LuB6#`ER{*P(62Xx5!qsjA>vNMe`& z`Cr-M|Ln=JD}IlG^Tg|w4vo*qf9Xgm!(=%N&`g?9LZwqQ(?Z6fibf17&gnGf7eznv zEQY+s462}yn8$0ii;q|K?gVIFM2DlFy;v)-tjy=v!ZJ1Cvgz!6V#8VZJ=%SiAZ;jFT^-Bluw!eTHB6M ztMW{;lGKsq!ggi5uahpVEV1oGHs??|Y%79Xr3qxkm^4i-w57e&oa3Jy<9jtKUmr4W z7fN05QBYZR&CsD0XX&wIk$N(;@JQY)8k^Yegsbj&;Zr|EhUvBfz+eu%)5*xR|>tVBt5ek!QlBp6>@%7MHtqD(A#r&g= zVW80%Qc<<+>4rPB;R(X47Z)O-WVZVU8Z!kmX&b5QU!YTaYG6tY7k2D#Um0Nuq8&Z= z*XdFqR(N;&wPfYx5J^GhqtuY~=^c!iHmkCwyb|m>Ro%_^7U!cXt*R1DiwAKB6&H$@ z^n?#<#*Je&Qt_fveH1Syr-hpEjWwYUZRjdr25Do-d74K3mJ0z^c2K;}hMY&fY*JTom;r?>R{D|F0a$hz z#LdNVsfR6#biZt9TY+PED0MuUU>%{;ftmke6qBr4G9T2OmsE+l-X0_P6^NP4CmcAV}ElJ#m_< zX_dI`hZ6N6ODgJ`yzm(f$s2W-!=aOh(En{4_8XWi=^r@X@(RpbF)b62bnEuZ)bN{T zBK|KllN&aF$4N)OjA^!iDeWI*gZ3LvS z|3S(b`-TctNe5p3+ba4yMgH<1a4{6+3!D9_ir^!QJJIIeqbDtx^kh&Md)z_Y7ceef93o9^H&T0 z-4BbBXr;|_W~CZK$cy&xxb*+6ApQSGYz_bSRO8zwdUBX3QNqR39HsS-0cnT$7kzf& zgJI*hE|TQ3J%3?K^q&KF0J^teI;SC@y~Q-+uXD_SvT%8`AUQ{T0gilGv733#J>c-p zx=)Z4*}>U;!10Bg5f?LQlk{awC4YGPI6*o%3%t>|D6{>PGpYmSqLs4by4HQN3p5J= z!Nk3+>4)1$5=}mi7wyE4qwJuA*Tx(4OW()#zp0o_mAEmu%SHSuP~whC&h}W3@3+W` zU-5husQTAuIfo8ikDf6dDJdp_JU&cIKK4{_7pvgt#I1+Uju7b7j*jX8uiVTa4ktIG z%|e5fW{aoO&K%a71rpW)z?P zWnp`42HEJD0_XEVsA$ga%u`T1I}X<2oA+`Xu>^f!?PXnzi!QeICkJbX4-Tu}1ETg; z!VNlFccn{x&Er1e2wg1k2l>b6CfuRsw>7hj_}f6Cyrxxoo<@QnUc&jZ%5v#&k z@A07Cm2F~_xO6P;FNvz^037jii2_TE2R^ZE5}sPSPF_E&&ThQv$Tl;$F#JnEH?Co; z)MQMy=sZ!UYy$Y#q#6U;1pnL{JpPy7a3Y|Pq|*FY5~a{KvE)#8$%M=2iqQJw8Sdiw z60L{$ue?b~0V|yb#SUti!s@I4;TT`}1F$ks#-C(`YYUT~sI(g7HL|PCB)83G)1W54Y zu3(p(F6a`q_}02bNcVE~Uo!qLX9_a@VqX>gU$lJH`O9lwo*dL0^r?kZ5sBxGW(M=2 z@yb;?`+T|wJZ4uw2evO5aP7oeWBiNN)}7;F$Bfo*j|M2eLWS^j^vacsHCmWOSI0*0eg~?`QIOGt zZY?iSDOLA4`oH)eQh8RIYlI(9Y*G1k_w33KjUD5?OP=c#!XRNJR1%j_aVrNkIyJe?O#~mbjT0TH!1nbWSA*s3pJf%XwX)&faZ8T3%huKN~f| z!+mLOikLUlGZ~dP>^%0UD18X1aP=ax&Ko3~I8w5n*dCxa?%wob!hPn6$iOjYrp5E9 z^A*U>&B%{^hU$HP=ftVbr7A+0`0#L3z=y&hI5ZOc4mQP31dBBh4-0#;Q|LP5Tr@Gj z0te_Q`VW_S&1-vf)bS5J^kM5I3xd9=Mv|!@GaKYniHHuc<<2hR+Ekd!7z?`|JGePE z=f(kXigJD!<=_oLi&LE-C3NMuwx^C(lpM>B66S%cgk# zu?fabVGZUZbRW3wu+f58_sR0=M8v1}rBzGiXw|airO5D1*n9gf5oP5Ox-OgZy(HCl zBn-jLG?}bz+wnv{GA&#ix;8{0Da zO#IZ1rN*y}7#6I9rqL1M64q{rF6bJyyyqNEn9q!{pmvikYlX)~tu5FN;~D>=elg&? z)Q3nt2{%p`rn9b2&k+-^B+YFqle8ZCk2mBmZcgZ%7@&tVj*#;wipQ?uaWwKgs`m()6 z;_%Gn(;y+<6i%ryY%zj59kFGZ1*$i37b+G>dNBe26@lng2#y$dXOgGug1PCW#EP>L zretr~1E->B@`TEQ!$LsTF0z%FK=MTaXxQluM80=?P!e{|r_E@f7_WFS{i;|w5QDw9 z#oI`sO~s=R0xrcigTIflu!CwjYJE!zG9rV)=SNjj_eG(PnE1;X6OrzkgMK2WWapS1 zE=Lp8Q&sYG95$OQ4~%No`vCs<*yo)`r(-7C$Q8>N0~K!UbXG@+%-&_~=yB5uT!D4q zs^#zT-FPEqUaLc>S>9pX&PSb6r5vGE(T7zQ@qCKYQfitUr?l&Ys`{Z3Pji25```+S+>tj1|+4*$@~A zggJgMNXSm@OEPO;b}`~s+ixb6`8+B)WG`$zq}$yhtS6NKk9a@uhUtVgM-JY1UjUDp z+o)TX()$5B^+&TL>Rh}s2V9Z&gIyW}_1=sFdHV#-c6WJ3Qxl&`8X)T=eqZCSsr+_@ zqj!t3*TPeGlWiC^w!O;uKio!HPM}Nfy~2uL|8R}W$&iK+s0alQ4^-n-+>FB{jSdc| z!FC;HKNEmBK%B-nl67U{0ZZP&h1V(Lqhdgjvoi8ue}>~$^3xt;nue2u6I<=@efkx@ zAno2)&Fv2hN*8P3^rw51>4ti&tby&epbpG_OkRILH174)9EYYj@V8-k9pE`BSvxJ9 zvw<~gHOAMClT<2p(b!PA`aW?xDk&Xci^E&}buD!}+z+$U1{Z1n97tZR?o+I%uwGB@ z{pHI$)&53u0-aFnbA&u1yG21ZD;mUl5+tg|6Ox+l2&~L&kc5np;#?$?eaBemTA6To zC`{4CbM}ZA@l5(qv}n!Iol|bJXr5?E`)tdx zilbne&=Q`W;qtW~0V4tCYKHI9RHG1`yrck>LV-CTSwTd8C#-F(g6z@=y|t0%|F12i_wDiogc1ayi*%>in5L*0%~s+(?X@2ZSSA)$gC9f zCQHcVH$g-b@{toV-BQ5%{O%FB=DvgGLBbjeHAWaspZF$2ECRbr+dEO6KXI;oF}E@& zR~X(*4ldRzablK&9!vcW)feJDjj2C=P(XJlCVW|6eC}`>H(>m30{H~V;Q?ts?hFaE z@=MyE2639Km;VCPikr`g$HQ>Ux}X?7$yF23oT^EznD|N)TI#(+oVlC@MUo)L)ty*{ zr-diNSBZFLSP>uj+^iBG!6Vz-Z|=pX+DIDyDaj0KhlVG7X;&NEm*X&K#Lr(GRE*B_ z53RVQV;T+bE_wdJDve118HiT(aIOE))oRXu<0oE<9Ns{ZP-FIjB2fb%wg?lIe)Bh< zGeekgth)YQK|X#m1HGw!l9Ns01}$|WAU;?T`N=U%VRByquOmJ$$kb@X#wD zsW@r@E8RYZBo^=dIWL#j7~m1dXoNktR23^M82Vs$FQy$+YrgfMD52{&IOTyXpO{^& zvyP!!f`zH`hLCS0)Hp!wbg9jeyd_vmd^S|d8FuYXRLA-e2Ii9^^0(Lli7s|QGqx3= zR9LAUbi~$8w9SE7bEy#pyFK^eoz5TA#O6$IwL_ALou`7Rq6|RuAcjQs2O`&yx zoTsMs%NfDJDgs z)!kTE#uv4s+E`aCo}e_tgM9O}`+~(XW?N4T^cp-3Sm9K{VX`+!AR9ado?b*C4S3$i zgOI+Q7TNZ4_4u)S!lRC5k|*oM7yTY4ai3Nu6-{kSk4ZVC-H_&i*~`o;+>18aJ2emd z5q_m8Hh{G{)((+sZ>k-X;rp1!iaYpYfZ>a-CGqy|wD~r8C|PAdk<)%p{B(`l8~35r zb_IhO`~olhT8tW{LP{yPp9jvU)=@9TXgNt9D5l;mvn8|mjo-Ayrt74a#3xMxU@?Yr zPEoskxuDcdSL~(A9h`zHId3(}E?+bs8y>(UEdZbcV7&ZzsB+inf<~^foAp|0w@x2u z8jT@wiCqzj-i^k|qx-Vc9K@6E`aXb_`NJQ;$DI9}vPS%Six~!dw(PrTn46Vb%A}RE zRF2?YeJ?yi)eWeA!8Ne46Ak1Q^1t{co(;gZ4z7#Vu1@53L3#XqBr*Js?-=;m?>{l< zxaYzLP)_0D@UOZ4kU^?(+i7eP$jWXeqGp6C#;W;1=4jo_ z`Rkict7O6fdAnBoQQ;L^>q3X*$^ART$1e1ENAMyQz?}();Vc~g?NzhG@ zDgyaF2|*jT+CFflg>o@2{5K-B!1VlxeB&OORjvyhmqWwK{*rycd;IJ zvsCj$h@a7Z?6owm%%*BhZ!nhv&LGlhy=$rxH8%vQrQpa@r5b7SvVMs__=>DNbV6J; zK*zktq+aj(!ByNvr~^j=nf%fHQGvM9Y{+Ri)eal=x(zP0tGDubieGr0tcM4;u7{+A zgf2G3dhq$0P0ZXbUveE7W;YMlI=(e@ly)r<5nfa%y=vwsBc`5l9=Fn(f|3v|lMsS0 z)YN_?@-$lOipBkQY%nhNL_pULu?x#Z(5B%9i5F5D-JGj>E?Y;HDV|27vwBF(aCgwqR-b64}{$Y=&a28XdZ6# zNpv9b(aYKoR?93Nsjk?iUI9&MohwD_w*%XQ5tj|%fUJ4EzRz!esf$;n3)f!5U+Y}R zH|*n%*I#65#*7Klq-m9tT`W=wP0rgCMe^1ZIxA41%7r7eVz=6Vwo3?c;eAgOgqH## z+_m`;g@uz1fiyJ93!S%{3n#ZO-Wcybu8*@M$SgExohmsVed$h&w+=OYrx~X|HpU~i zK&JB!cU%iP5i5t`&rXO8QflGB%5R$e0Peq@)8bl#tfPfQA4 zQn}^t`K0@D=F8tr5p8g{;>8it|9w}3@?KKPl%^6NUMhdzginXZyx$Ywe9#hJ2iRSO8vgo+d`_Bw{aJGkxuq zWpfXs4N4Wq>W6p%!YEr+fY1#W?qs*4t_`BMS+VjkP%^8%IPu8>7mkct8%5p|)Bcl! z=?F_kd2rm#spAXg>NbVQZ52)by8J-B11@TjhT4jqQnxiD;7BfQT4Y4$) zLwdB?drRwt>m}oT`Vxkl04;ok?{Q_eJ2|r7!u2Oh?ytalt++Auq3O`Zuak`jl*g_c z;*VIJ4U{(~*RKmxXE%>le@C)-sNs5^s{_+>eEXdkF|fX}=ddazeP0 zfN2_on{I3yY<$ywZoLnv-v|lcS=iq%F3l_RS<{xV#g=_zuteqX+nxz+wKsZa$8P&t zTd(*})XhjYZthUBBc9(etTV``VATj)tr@{MEo+p9Zm;0uI(Ar%bKGEGsj~ZJi)YtX z#@iH~bRHNmw|%fdrUVOV-mx*&YI?qUV2o8qDd;b9Z?|bdI;PZ?auS&ZoSjr}^O8bZW-I(r!?Z9)WH!ExKjyd-HZ?trj>#gg`m1QXs`Y^_93=yRi0yTWl-ETK}T`rV#+%>6dDb)THmW>?Gp*_ z_9m>}rD^?*w9Y+*q^h=Q71`0je!rKRBGV@r(p^>I(Qja%5rWjd{h@*rrG4TLMeo7@j~5ubf{?}yC%8)SFsuI1A4})Pa>!zStCo<0g~w6;?OFw zhY&{;y6)d=?GolIb;uI+s^4M8Fgq8uR?R+t2i5xp?TwqyO-1g{IhIMZA;IEZ2Y6{> zTk^g0?3rx`&n==Dx8lptrUvMA`=Ec&OonCCZ%R6?>k>0f3O9+_J(%*|A$36ePB2#+ zP^6k#vX1%L|2k{F5WO!n_w53??zqmq|8wrK7<2FrIpM_d0=`Bt5+E2sLx`DXUB8yn z4dV5xeK4VAvQ|mfnz;7F*P}y^2;NiJ~An3(*k3WfR=ZbRT3xP25gbwNH5==0py-7wDImG?>z`>?bVZ^5+L(L zEif7F=)NZ@-~OrLZo26|SJ5rF>hhy3c)s?W4L+|V(1zIpl?}0}z<1qBJk^sUetu(v z8)^!^G8H0s!(K}7rds?>8OhK|YW2k&u)6$bv%4J)hvw3L*nQ>NBM19koLnociKd%h ztRPG9RKwMZ_QiXv|Gbw;Zhva#J2GuT4tg^77IN@Vl_o{)qp?Ns}Z zIB&DxCDRRPW~*=Uqx+V7JA_eTpTDW3fq#ad*<@WGSar|4|m`&mNcp182*0K+kf zg~*JTU=g<=BBwRSh4)+C7DqHfFWS|ohx{Zg1zs;0K$ZJX4CFf*9rWU4COfgwvvv#Ht3Wn4B;GDF+VbX3*(lOsbWkg&SP$*pRZ|$&i_PDKr)u*1 zC&dV_D4ONxu}%+nBWX;5%^?J|qY(gbJCVt$7(^x;?D4(8`g=`j?K>XLgqrF)zGf*d z(N>F207#)6weVMqg!!fy93iPrM0`?nTuclW5njIIxvI0m8}ESU@8T^5#%x$}SRBp=#tmWuht9j~Fq-#y1igD{&mR)p@cSSm;P@o(TCT(};p2 z>bqn=d*+WB*ZLbl(IT{SUDz&)S`%Z^dqQh1zL1wR%eo_=^L?EiZujBG4+(o^ou=zs zYWA7lJFhG&8H;~+f$$9<;dSx9{E48_MZlY?A}tOb6cSpXr!+Oz=8W!m;lVClmr;^U zS7KGMcjC%0iIsGU>sLu(Vv2!Qq%BO(x-zGvi^wyuYq6*hvxO z#X?a{6SZ>!9Z&afnY(?TGeGUW+BjuDAJULs7ro<06qGh4E20M0V`hwD*^}tjMwEJabc6DQFQnr?i@CE2LDh5Aur5?L{2W)%fc#M<&Hez#!4>*ddzmC3dXI2 z(l~}!4dMDyz(6rBx>^`x-_RvIH4sK@n3O3!7qZhk2@2`gm>ed6v7(U2&woP zw~He^Emz+>vT=JutiXS5kj~5!L!}#116!R}Uj~Wm@!Nxw$uW&|J+iV8R-bMM*u7E( z&Q@{YKsuO-G~LIPZazf@M&kyyM=tq>%fiGr(F>T@`O-U)be4}u?vcb6<(Xo_xe1$J zOo;Mhf)GK^o0X+_q9n!F*I^ai@8ju(dp+zD|8sSCKGIOvpSihS&hJZt$WJmuGJR9J z{!X0`484XlJ3=8N#$rx;W|xLv+7Xj;^k|@8j!JnIAK&X+{+p-15ngnpl<0YHRD)m( zd?!lct44_}RE~o2?zu$~p8*=12Wrh^OPQbn@dO66U9T~7?bmB7#jZG|sP-qZa_5Z( zb5e|$^cpp>bT>>{*{O9w9Hxi)==%nHxO4`z9pjq@{JKhm12UcP;cLpwGJh0VhAiPt zn-X+tcBBtDQJCLEt}equX;ST`apn)%Bdn}U$t4)t#qNnnhO!F1GwCKCnnq#aC9mwB zWW4t^OQKi*Kk(jacfJ7GT!{*t(O_uuk$5s6{!0c5@&XSYF0^j9+NfuWpEBu*8=-i{ zq?!YN*!+S6r~UYM=Y}W4ETP!%YjJ4B$Lce|jW0$;Q`JSnK*MMlqctbdmEW)6l3UkO zBcB#7^U^L1=S??dV43~+@V11j2O|`6_Mwl7E@Y+*=9==r-;C9rfg2x9P=cBY>hpmA zCV`hSyu+O>`TWB9wRpnuPV1xc?e~!a?o)P3qBn5*4`ho0IhND~oucfEZNboH9DlFY zP~xV9o>Ck0O1ZlPAjK;HygMNYV#5%aW`9t0Th7UzTe!A*u07?{p1K|{L~21_7d0iC z_jO}E`aSt*_Bt*urtHyS?AB?^m{*wf03q7+y(T8Zai}K7I$$!1Kd*KLb9iDj$%9o} z7yb?67zvLCL!DO4v+IzKa(Xsj7)sx1Zkk!(c@BJC)^FnkTe#@RUV5CpV4C*B#%6;V z@^6_6*_8ZknK`e#Y(-Lq2Kejxquab+Ix5CSOU>1;_POltyCM=fnhvs~nS8y>wr2W+ z8w(?4JzctyHSRFW%bebFTxQ^N2PC^|AySt;S^Sh{$m{kO_Foqq`v&`Y|0SW91UmgU zmD$9cZmUf@qjiuRMpW9lOV4#I26>(S!T~N_c#_x_JzWR`;FnAmf=X{W|J zEzxu*(1t-UMAcbfC7y61pd}lp&Ih+U#4|J`Q*ae&!LGz~dy^TECaN0+ghh1YgI5U) zk6`=4d`HQkL^JiRPfZj-oiF?5HEni-jIRe?4xaAeX$CLJO?8hb?r>V~K+9O1Qp)Uy^Z56rqKu0@oN(M7Bho zIAZ=X$O1zr&^#kz16}IvtpHdoa@dEMr0_FpC&BHaj*V*uD3UW*5-Ys$^-T34NK@ZX z;KkdrW2asCalpWkJ|ujKG4ZJ#8QVf(*Zd*7G*t=7c^6uvJsCDvgw{=5Ke)CRHlYpR z3iaV6o;<0fsByV8$&Rlc-QuRvd!I`VOj9qBdrz^D0-!8dZMVR;XLsftjiwMbg<%rY zrdyQ{s->6|(c2aLI6OD|QL?iHJI=$OH<_4tiHw>MId|?CZp5Wm@~dC8;o1)f7LPkT zc^pr4cJ){R-_$R;Ej=Jx9m$DwB?lzM2c<(sDSZ?C{NR=~o{CUeejjrV?WA|SOA<(I zXgsz*G4c2X`zxo$9;b-i+o8Oo_Y!lvL|zdq zINP-sa88P9hLPw(vL&E7Hrb(+C3FYS<6P6HX@&aE#C~3!@C4I*uW|GRMnXDDD@2{P zbfYaeun68!#J>Iw#7wlx^_ZBX1a4c|n8c7ou2|6;_!=2lUs-=dBh%Rf-*Ut`4Tq-1 z;}!xT*qVvB5>DxKK-ccBlM&wPX#Mb8*r}R7BMd3UKDj!s68{!LYQzK&nbBmAGo%>KF?}&mmYRk%*qLOi@bYht3$!{Qq@nEwD+G)Lu?nP@D5Fgg z`{byUykJI^dB4QpNYBFv?7UPTMu27Q?*+&+7gFq?sInrjTRVorLRv%zw5{N0d`29b z5ySG{EQDF&%zi`wzrX>89T?FQK#YqJcNN->dHP_wJu<>YRJTyBxX~D5L$0?V15v*n zvx33&&}T@R-fov=0~QTv45?JMlu!*D&aP^fn0=3o!0wiYG$t~9Az-GNaziFuUX`Rb zFoKAzQJHlizxLe+3W>HnV&ljKBLh!cgl`RO3@+clOt8e<1ZUPed+ngy$5a4G`Uu%a zF>pr`3tTS+Pa=V~@l3Y6`+0!bk_i2;x}Z!*@+EQjA5)ni%EC$lmaMfJ}DHQ4==Y4bl~)nIaVmY^oTf=g;O_>raKq zE!K>7C8f=3a%91}J)<!c-ZzJSbM1~o%e&4C zksIfhPAI;9rq$V>6H@{)oUSsf_`u2REsuV#=rjf&jat2M7r;4bi(_G%;HR$4C84`* z`lJF5)zGnNBD?CbN^O#T2UfvHKIXYeyAw*0Q zzude5=Wx|I-S~ZGvu9PkB3ew>GUIy!B5Gkvf~ISvg?{JORK4n*%V$X(NV1B-K5;G0 zKP6~UkP*vvtT+V}NiJhkOvIaIZT`=_>0S|<^iwl|UZQ)9PnyayI^$Psx1(8w)->K5 z-UHuPg-ERjl5{IQ!mEZO-f#eR7AI;atd)Wkld@<&Qx`zxn$M;ocL{8`28MjYy|kzv zo;m;QFccw<13Y^`Ih#u8JVb0((FG+OwElTU92kFmh+_zbifJgc=tH7g5+^uofx#HW z@L-rDhdp6c7nircDZP6rJ?BVIHbHAjhT-Cz?0l>D=W4YM26F4ZYSd5`b9YiJ<6O8Z zSn0Xtm8kO^J5y6Y^VhU@oj(U#gQ|Dr&A^?4E0GSg>Q+F*oJVH8$PYFY4|veUWGL+Q zuHeIbQBFoE8L#ak2NCqZE)zwjoS~7cEN2h(VcLl{$E~yS)!?HKtrm>rK!{=^xslKm z-%D6Kz9HCrnH%ZWpR(zC5vV+48B-Pm;gKH8nj2G{P_$~?jjgy>QNdVzA5DQ7()!d= zgr7NOoyGY-6MOt(e)y@}%L7a9_<;avpogEPk-!8crbcA#9tS?XqA$+J=?ZU`LOUGD z>Gdr|OA4FO$M3&_&uGs_#dGPJ2%-RpH*;eL2BljvX4TDfqJ8K#BPKzHOMS=%){BQ* zwK{Q(s`Vm*YQD$Puj`omLPrKHQNBsPz-|wcZywwrd8ss4<%H%0EFITJyCL+HfS>_T zH1C_m{i0v673rDXRwsOx&+5X_6oVinm6b~3VZ>z4@Q&C##?Fx{OapG=c_JR{^+)Qp zKD7Xx3`hYXQ_GQK{_9J1k6R>=kR@BL@4l4wy>$IpkKbQUusIm~d!>~G91o`v63cr< zGu^;7nPvT9kY#+2DHG{)o`o(6zFMcQz=_fNi9N-YkYZZA;p1hXPnQ6#nNk!XB zZFELj#{}ul^K_S;cn0|8C`P8Vxuo7f!WL~8*K46XF*>2eX(@9kS2hd2H;mkO*CAwE zGG9DYAHAPL9o!_$wG-oli+5HtYHr}5X>beOcL->?$Al(hKdVj$xf7xLwk(yoKWLn% zTCrD=N$X*#?9DfqYg^m@2*@+e*4-*F<8b_m*IFjX_IH{)uI%$y!|P*hxZ(7fV%-Q$ zenovPp52Ho-ce0gVSle^pD~!@hPGfz-k-tx0@rGz+Aa2Q9v=#w6|R4b*V@jNvj$Qu z)(skK{=#cgKyWZKuCe2hwpxT>4>h+6ETm|f4VuAC30jK59h@qKD9mw!{2(08{|YQY z$9TYn`mklTd#Cv8(CQ%gXb{IRqK`6HJZv}c_(qq{(xVQgbQsCFCa0vLkvVT%>wpKx z>q2DdNN2*Q`Dd%&8xK&c`YI!LaEEx&&DYz-cFN|?Km>p6Bt47bFhY<%lWlbLG4_{< zFTy_rwp8njkb543fonD5DzJYo43hx<0|;5NxZo9@jp=K><><*_*B38K{up0|`HA_o zVP2afa@HWG1Avom;RnXyWJVBWQzxsa@h(obd-lWc z8L{bkeRq_~AeZcwPK`C!UKfdr==isK(=Qn=s_ZvZPE&?S36w~18%2r@uDut|3i9N` z_Is7Txj7eLOk&R>TBz8^LX>pX72#rj{nZ$W$L-=KH8^VJ<~^M9qvA_ACQdW8$4};K zouIDQ>QFpB;@KWFmVBP!0#2%VFZ~`vPhAE6#X}2cQSLPxu5ze`icA>cZP=p`$M)|} z-%hoVu-y+So;MTBffz_sg3&-)KlVJV&+HHnxXkAXAvH-Qat^myKk! z26@;k`P7`-mUl==3fh(T}&II9mmqZKkxEbvFafEMzjHz<6Ix? z{No)+(U-TjKVZ^w{D6Y%ixc|AGf@2hoQERG1D8bcKkIPICTWrNdvURQNcDj7{Im7~ zN6h`Pxa=C#6+Uck+72`8ZB~eQFwjv|5yvb3D~zf=?3xWlhN4qSi^_uz?JXe%(3yBI zLd~8L5u@5buW0Bs7^;0Ey;H$)bp*^2W7^{l`LP>gp(wH4!mV+l9>L(YrERI&H{0~2 z(tzWeV_RG{qS^b@AxmD+EzjSAjIEu!YLu{ZZ^~fP#dI^%elx4hYV&xgO?*FJdF356 zkKNd5N;lq;80TO#$&}yuhfa6IlZ9R!e0*@(UbvWA__N%uH)AaLx+XaO`F!y96X-R% z@k(!;(@tn1RfJsq!{+t=_9It87s>(@Mi78|E(B|o3NFD^W>X+hm`KpK6x=j+8AhW$ zpf)uFE`KILlkwc~@KzAVZP4WrntyP7=_XjT*42_u`=|LC38~A)w7&UEd)cv67iy`H%{Qp_EZy<(le@t zJs6cGH_uwP+8m9^L%V}qLO(P96i4lE8d78IktVS@q?gCHOHF)q{2cL+5kN91hbas zs7bPzE+g}ODjm*f+aigl<2d%@kIkf@LPAjrR?_KV4k=Cn7p5`oFv9dO^;AVKY7LJ zstOmSLiBb_JsG-z&$0ga0dp%METG(kqi)q`piJOM_sLs*_nkz7cVmHooV*(8@b?2aC$LJZGun_p^J(Q+A0+`hd z@EOe>2y0E4+Ra3nWcBHvka!%h5YM6y6x72e!k#Xq&m5R88W4|imO{&SoE93&lp3+6 z=|EPiw^9ExKm}p7{N>~AqJVcCcwcYvI3dHt)tIOQ19LQ?HN-C%$vc^==j&)?^n+B5 z7qs)<`Hpu6VyX4~oQ5ve%`{|L_ zg39tVE-cdI$)3oDk&Qs`$aFN)N!yO;gJm7_^tuB^A#=Xp{1>7gb57)AJTTYgWtJg1Hj<-wKf9J+8)_KJl_%$mhe(&!3tY*EYY_cP6s?>xvne zrUDz+Poy&WD-uCrme^MEWOxkg(8E)Cn{B|C9J;m*UhBw6(iP^VVtaD6IwPxRuBQ$0 z8}D8yn@U!i-TKoS(LvQV2Jw{|Tz3+2)WrQ%x(Lf$zH&yanfi>#i0l?S#c4(iJk;H$ zln@ytPRh4O9chG)D(hdcdI0r41*fh%H^lT`DWmQ@Jhb`rM}2mLZlpAxr7xF7c+PCw z9G4^|E8V?gBKGHb|FQJ_eX;ropn;@CTgLGZyq1Bpwg8=md9&WxTfvbPb6M<#BP!5K z>aifj*F>d7Z?QavcNE`3NgM#5;lyjvzdAG&49FRwc$w09_qFT-V@DPSG*SXmZztml z2joKe0Li(OWbPxd%zv@;@ku?(Jc;$v^tJh0nfUeul-MB;X2;(ynJp5CWzw||(i%ci z6p-S6ihvCEup7>rkCCjQdzcmqT^)ZF<7zu|s>5%2zx$>`N$!;Df+b>7S1*hgl06`j zcJMi`F`2{~ha8T!MGWPp0QvQqTcHymWj;HP*7r8W4l3NBXGa^1iK$Q7(*{ZZNREB? zVZObn+6wp!uN^s3SFOLN!X^(Q$yXj1ozTRv&Y~U?%bL7=uu|5VAo2+Luos}XCh`ux zP8jiWwGg^{ZNVzZ6Uu%dxr3)9w#8G$1lC zMAZL?!$ALRY#2MPSPA@j#_!=T_^ka6I_Pyfx$yv_zC5fek1Ixc1~mQU_|u)QW1KJX ze&2lo#3NC0uSl11!#-I`L2CiRysgQ=;tg&w7Ij2Tv%zwLT0t!bUcK!X?`=~?u`~M$ z{A5eAFZokerJNL2cs1o;|5Y^WIdJed_MC)T!?nnOLh2*%VXnZ3`#YXa(+;MXUe-OP z26Ti7UJl%d_bi^Ea4&doG_DAXNgvd}le}+XdR6|)j+V-7dp41`zO(9%Hqd%kr znb^Xq^iTG-wPOf#a=<>7E>BtuZ9Ic&20qB6NMTVxk(7R4bSuq|9*G-|w4j)_0kz&_ z*#6$p(|L+&7FMS)bMOZcnM`+fxi$W;BWw`_=R_81k1X&S+rcvSnP9T;z9Z=S4?t5a z_iYVl=C64RZ0f3`T!mS%R*w4HS44hX>w>iKO^f<_{(KXuFlkf=&bd=Q?=lEqrt@M0 zU#W-mp=sUEZa3|j*c4g4bzrHf&T@)+cO5K#PG>tCy!?&d<~u1Rdb%QhVqokmy`A)t+CO8vD z+7CRk$=d{OG_n_wyYoCHW(W6Luli%~+w{7kTzUGKy98)sl? z6IcsW!8~ZPD(Z@*%@`0mCHY8lQCt~_e%W8>Cd9t|;|Oy@0N-BZHT5wD_Zhk9a|S&= z8G4y+d_Rg(>a~DjagVXa-$$f^S1H~h&HfZ>)rXv9(EJ?0CJCS}?loVCr#px(v^K{G zTXTG&dS<+HNT^aT4X^hU^9M}DnS}0&5T6lZ<{4=jX~X)M+=!f06T1&)z8H(>$kpEFd83WNoXgEa2kN~B4E!TrZ54!V)5rtAR;tzm z-aobqSfItq==9>5fNzG4{Sd;=l;7QFl)3Q_G&&jM{W+zU7ATF#Y%qjNvFASoqR|h= zj5A5pKOUaXA?14I47>X~U6^2q~%)ivdL9Ie{GZ2|Z%SM<00jj21{T zCp5@gz2gAzH}Tp-BHSeq41_z1YRPayrI9x>Y$dao#Cut^GIJJa_E?m*9^jQ<^1y0* zDwzt@3$hxEFOn;R$(RXEWj~WO3NB1(uJtBB+K~Vt z+cb=o3E;n${rujlpf~;$MN(aaabI9fL6wFi_xG~|HY{e%FI%%Hf8b-ho>v~6kDnw% zk`|*3hsYx^T_)Phw_!#~Qp!PQT2M;M&wjP&EdNK4Ja9oI|B34jIaNBU807!1#y?Lb z`TzILlrk`7Wp)`&^@HmFdG;T#eP@U?{7#GB8=B3h_>b!U|L^_phWo#)U2??L8?(xs z8@B$lPye(3zI*$|jf_5i!s1J+b&cra-^&HSzx^!3HXC|5Y*n^R-CM zf0Vb4bQbvEl{fJ>RK*(jf9CuzgF=4+QQp91fh8PO{C^lL%Ib@}|5MEhqQB)8R7o`O e{|{|+^97U{3BM|^p2PFw`;rim6|NH05BOgn;45kX literal 0 HcmV?d00001 diff --git a/doc/user/project/img/protected_tags_permissions_dropdown.png b/doc/user/project/img/protected_tags_permissions_dropdown.png new file mode 100644 index 0000000000000000000000000000000000000000..9e0fc4e2a434fbec05d2ef4dcf52ef355589509c GIT binary patch literal 26514 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 literal 0 HcmV?d00001 diff --git a/doc/user/project/protected_tags.md b/doc/user/project/protected_tags.md new file mode 100644 index 00000000000..0cb7aefdb2f --- /dev/null +++ b/doc/user/project/protected_tags.md @@ -0,0 +1,60 @@ +# Protected Tags + +> [Introduced][ce-10356] in GitLab 9.1. + +Protected Tags allow control over who has permission to create tags as well as preventing accidental update or deletion once created. Each rule allows you to match either an individual tag name, or use wildcards to control multiple tags at once. + +This feature evolved out of [Protected Branches](protected_branches.md) + +## Overview + +Protected tags will prevent anyone from updating or deleting the tag, as and will prevent creation of matching tags based on the permissions you have selected. By default, anyone without Master permission will be prevented from creating tags. + + +## Configuring protected tags + +To protect a tag, you need to have at least Master permission level. + +1. Navigate to the project's Settings -> Repository page + + ![Repository Settings](img/project_repository_settings.png) + +1. From the **Tag** dropdown menu, select the tag you want to protect or type and click `Create wildcard`. In the screenshot below, we chose to protect all tags matching `v*`. + + ![Protected tags page](img/protected_tags_page.png) + +1. From the `Allowed to create` dropdown, select who will have permission to create matching tags and then click `Protect`. + + ![Allowed to create tags dropdown](img/protected_tags_permissions_dropdown.png) + +1. Once done, the protected tag will appear in the "Protected tags" list. + + ![Protected tags list](img/protected_tags_list.png) + +## Wildcard protected tags + +You can specify a wildcard protected tag, which will protect all tags +matching the wildcard. For example: + +| Wildcard Protected Tag | Matching Tags | +|------------------------+-------------------------------| +| `v*` | `v1.0.0`, `version-9.1` | +| `*-deploy` | `march-deploy`, `1.0-deploy` | +| `*gitlab*` | `gitlab`, `gitlab/v1` | +| `*` | `v1.0.1rc2`, `accidental-tag` | + + +Two different wildcards can potentially match the same tag. For example, +`*-stable` and `production-*` would both match a `production-stable` tag. +In that case, if _any_ of these protected tags have a setting like +"Allowed to create", then `production-stable` will also inherit this setting. + +If you click on a protected tag's name, you will be presented with a list of +all matching tags: + +![Protected tag matches](img/protected_tag_matches.png) + + +--- + +[ce-10356]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10356 "Protected Tags" diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 6a8de51a199..a1852650cfb 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -20,6 +20,7 @@ - [Project forking workflow](forking_workflow.md) - [Project users](add-user/add-user.md) - [Protected branches](../user/project/protected_branches.md) +- [Protected tags](../user/project/protected_tags.md) - [Slash commands](../user/project/slash_commands.md) - [Sharing a project with a group](share_with_group.md) - [Share projects with other groups](share_projects_with_other_groups.md) From 6f15e89a6b83dcfef897dda414325b85090e2c40 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 7 Apr 2017 18:18:58 +0530 Subject: [PATCH 54/54] Show Flash message above Tags list --- app/assets/javascripts/protected_tags/protected_tag_edit.js | 2 +- app/assets/stylesheets/pages/projects.scss | 4 ++++ app/views/projects/protected_tags/_tags_list.html.haml | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index 624067a5a09..09a387c0f9e 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -43,7 +43,7 @@ export default class ProtectedTagEdit { }, }, error() { - new Flash('Failed to update tag!'); + new Flash('Failed to update tag!', null, $('.js-protected-tags-list')); }, }).always(() => { this.$allowedToCreateDropdownButton.enable(); diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 61b973e0aa9..717ebb44a23 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -782,6 +782,10 @@ pre.light-well { width: 100%; max-width: 300px; } + + .flash-container { + padding: 0; + } } .custom-notifications-form { diff --git a/app/views/projects/protected_tags/_tags_list.html.haml b/app/views/projects/protected_tags/_tags_list.html.haml index cc006ed8a0b..728afd75b50 100644 --- a/app/views/projects/protected_tags/_tags_list.html.haml +++ b/app/views/projects/protected_tags/_tags_list.html.haml @@ -1,4 +1,4 @@ -.panel.panel-default.protected-tags-list +.panel.panel-default.protected-tags-list.js-protected-tags-list - if @protected_tags.empty? .panel-heading %h3.panel-title @@ -21,6 +21,8 @@ - if can_admin_project %th %tbody + %tr + %td.flash-container{ colspan: 4 } = render partial: 'projects/protected_tags/protected_tag', collection: @protected_tags, locals: { can_admin_project: can_admin_project} = paginate @protected_tags, theme: 'gitlab'