From d1f2b09fadcfba210c5121bd214b910b9f9809fd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 1 May 2015 14:52:30 +0200 Subject: [PATCH 01/17] Allow license to be uploaded and enforce its validity. --- .license_encryption_key.pub | 9 ++ Gemfile | 2 + Gemfile.lock | 2 + app/assets/javascripts/application.js.coffee | 2 + app/controllers/admin/licenses_controller.rb | 54 +++++++++ app/helpers/application_helper.rb | 12 +- app/helpers/license_helper.rb | 55 +++++++++ app/models/ability.rb | 36 +++--- app/models/license.rb | 93 +++++++++++++++ app/views/admin/licenses/new.html.haml | 20 ++++ app/views/admin/licenses/show.html.haml | 116 +++++++++++++++++++ app/views/layouts/_broadcast.html.haml | 5 + app/views/layouts/nav/_admin.html.haml | 6 + config/initializers/license.rb | 16 +++ config/routes.rb | 4 + db/migrate/20150501095306_create_licenses.rb | 9 ++ db/schema.rb | 16 ++- lib/gitlab/git_access.rb | 5 +- 18 files changed, 439 insertions(+), 23 deletions(-) create mode 100644 .license_encryption_key.pub create mode 100644 app/controllers/admin/licenses_controller.rb create mode 100644 app/helpers/license_helper.rb create mode 100644 app/models/license.rb create mode 100644 app/views/admin/licenses/new.html.haml create mode 100644 app/views/admin/licenses/show.html.haml create mode 100644 config/initializers/license.rb create mode 100644 db/migrate/20150501095306_create_licenses.rb diff --git a/.license_encryption_key.pub b/.license_encryption_key.pub new file mode 100644 index 0000000000..68f241b974 --- /dev/null +++ b/.license_encryption_key.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Hxv3MkkZbMrKtIs6np9 +ccP4OwGBkNhIvhPjcQP48hbbascv5RqsOquQGrYSD2ZrE/kbkRdkIcoHEeTZLif+ +bDKFZFI7o5x0H92o9/GSvxHJhQ8mkmvwxD7lssGShwZEm8WG+U7BZqUV/gGmCDqe +9W8H8Fq2B0ck8IXjbQ4Zz+JlyV/NHZTZcs69plFiLKh4N6GYVftOVwSomh0bbypP +OB9WnLC7RC9a2LRrhtf8sqa2rRFmtyMMfgFFzLMzS+w+1K4+QLnWP1gKQVzaFnzk +pnwKPrqbGFYbRztIVEWbs8jPYlLkGb8ME4C84YVtQgbQcbyisU/VW3wUGkhT+J0k +xwIDAQAB +-----END PUBLIC KEY----- diff --git a/Gemfile b/Gemfile index 27abaf3f5d..d0a434c8bb 100644 --- a/Gemfile +++ b/Gemfile @@ -203,6 +203,8 @@ gem 'request_store' gem "virtus" gem 'addressable' +gem "gitlab-license" + group :development do gem 'brakeman', require: false gem "annotate", "~> 2.6.0.beta2" diff --git a/Gemfile.lock b/Gemfile.lock index 79797f9803..3f3837e48d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -209,6 +209,7 @@ GEM diff-lcs (~> 1.1) mime-types (~> 1.15) posix-spawn (~> 0.3) + gitlab-license (0.0.1) gitlab-linguist (3.0.1) charlock_holmes (~> 0.6.6) escape_utils (~> 0.2.4) @@ -706,6 +707,7 @@ DEPENDENCIES github-markup gitlab-flowdock-git-hook (~> 0.4.2) gitlab-grack (~> 2.0.2) + gitlab-license gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) gitlab_git (~> 7.1.10) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index bb9da14701..0f1b142d0d 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -114,6 +114,8 @@ if location.hash setTimeout shiftWindow, 1 window.addEventListener "hashchange", shiftWindow +$.timeago.settings.allowFuture = true + $ -> # Click a .js-select-on-focus field, select the contents $(".js-select-on-focus").on "focusin", -> $(this).select() diff --git a/app/controllers/admin/licenses_controller.rb b/app/controllers/admin/licenses_controller.rb new file mode 100644 index 0000000000..5b94869ac0 --- /dev/null +++ b/app/controllers/admin/licenses_controller.rb @@ -0,0 +1,54 @@ +class Admin::LicensesController < Admin::ApplicationController + before_action :license, only: [:show, :download, :destroy] + before_action :require_license, only: [:show, :download, :destroy] + + respond_to :html + + def show + @previous_licenses = License.all.to_a[0..-2].reverse + end + + def download + send_data @license.data, filename: @license.data_filename, disposition: 'attachment' + end + + def new + @license = License.new + end + + def create + @license = License.new + @license.data_file = license_params[:data_file] + + respond_with(@license, location: admin_license_path) do + if @license.save + flash[:notice] = "The license was successfully uploaded." + end + end + end + + def destroy + license.destroy + + redirect_to admin_license_path, notice: "The license was removed." + end + + private + + def license + @license ||= begin + License.reset_current + License.current + end + end + + def require_license + return if license + + redirect_to new_admin_license_path + end + + def license_params + params.require(:license).permit(:data_file) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6e86400a4f..bbf18f2267 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -213,9 +213,15 @@ module ApplicationHelper def time_ago_with_tooltip(date, placement = 'top', html_class = 'time_ago') capture_haml do - haml_tag :time, date.to_s, - class: html_class, datetime: date.getutc.iso8601, title: date.stamp('Aug 21, 2011 9:23pm'), - data: { toggle: 'tooltip', placement: placement } + if date.is_a?(Date) + haml_tag :time, date.to_s, + class: html_class, datetime: date.iso8601, title: date.stamp('Aug 21, 2011'), + data: { toggle: 'tooltip', placement: placement } + else + haml_tag :time, date.to_s, + class: html_class, datetime: date.getutc.iso8601, title: date.stamp('Aug 21, 2011 9:23pm'), + data: { toggle: 'tooltip', placement: placement } + end haml_tag :script, "$('." + html_class + "').timeago().tooltip()" end.html_safe diff --git a/app/helpers/license_helper.rb b/app/helpers/license_helper.rb new file mode 100644 index 0000000000..0a5b88885d --- /dev/null +++ b/app/helpers/license_helper.rb @@ -0,0 +1,55 @@ +module LicenseHelper + # better text + def license_message(signed_in: signed_in?, is_admin: (current_user && current_user.is_admin?)) + + message = [] + + license = License.current + if license + return unless signed_in + + return unless (license.notify_admins? && is_admin) || license.notify_users? + + message << "The GitLab Enterprise Edition license" + message << (license.expired? ? "expired" : "will expire") + message << "on #{license.expires_at}." + + if license.expired? && license.will_block_changes? + message << "Pushing code and creation of issues and merge requests" + + if license.block_changes? + message << "has been disabled." + else + message << "will be disabled on #{license.block_changes_at}." + end + end + + if is_admin + message << "Upload a new license in the admin area" + else + message << "Ask an admin to upload a new license" + end + + if license.block_changes? + message << "to restore service." + else + message << "to ensure uninterrupted service." + end + else + message << "No GitLab Enterprise Edition license has been provided yet." + message << "Pushing code and creation of issues and merge requests has been disabled." + + if signed_in && is_admin + message << "Upload a license in the admin area" + else + message << "Ask an admin to upload a license" + end + + message << "to restore service." + end + + message.join(" ") + end + + extend self +end diff --git a/app/models/ability.rb b/app/models/ability.rb index 85a15596f8..155e199345 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -5,18 +5,28 @@ class Ability return [] unless user.kind_of?(User) return [] if user.blocked? - case subject.class.name - when "Project" then project_abilities(user, subject) - when "Issue" then issue_abilities(user, subject) - when "Note" then note_abilities(user, subject) - when "ProjectSnippet" then project_snippet_abilities(user, subject) - when "PersonalSnippet" then personal_snippet_abilities(user, subject) - when "MergeRequest" then merge_request_abilities(user, subject) - when "Group" then group_abilities(user, subject) - when "Namespace" then namespace_abilities(user, subject) - when "GroupMember" then group_member_abilities(user, subject) - else [] - end.concat(global_abilities(user)) + abilities = + case subject.class.name + when "Project" then project_abilities(user, subject) + when "Issue" then issue_abilities(user, subject) + when "Note" then note_abilities(user, subject) + when "ProjectSnippet" then project_snippet_abilities(user, subject) + when "PersonalSnippet" then personal_snippet_abilities(user, subject) + when "MergeRequest" then merge_request_abilities(user, subject) + when "Group" then group_abilities(user, subject) + when "Namespace" then namespace_abilities(user, subject) + when "GroupMember" then group_member_abilities(user, subject) + else [] + end.concat(global_abilities(user)) + + if License.block_changes? + abilities.delete(:push_code) + abilities.delete(:push_code_to_protected_branches) + abilities.delete(:write_issue) + abilities.delete(:write_merge_request) + end + + abilities end # List of possible abilities @@ -68,7 +78,7 @@ class Ability def project_abilities(user, project) rules = [] key = "/user/#{user.id}/project/#{project.id}" - RequestStore.store[key] ||= begin + rules = RequestStore.store[key] ||= begin team = project.team # Rules based on role in project diff --git a/app/models/license.rb b/app/models/license.rb new file mode 100644 index 0000000000..87d132e8d6 --- /dev/null +++ b/app/models/license.rb @@ -0,0 +1,93 @@ +class License < ActiveRecord::Base + validates :data, presence: true + validate :valid_license + + before_validation :reset_license, if: :data_changed? + + after_create :reset_current + after_destroy :reset_current + + class << self + def current + return @current if @current + + license = self.last + return unless license && license.valid? + + @current = license + end + + def reset_current + @current = nil + end + + def block_changes? + !current || current.block_changes? + end + end + + def data_filename + clean_company_name = self.licensee.values.first.gsub(/[^A-Za-z0-9]/, "") + "#{clean_company_name}.gitlab-license" + end + + def data_file + return nil unless self.data + + Tempfile.new(self.data_filename) { |f| f.write(self.data) } + end + + def data_file=(file) + self.data = file.read + end + + def license + return nil unless self.data + + @license ||= Gitlab::License.import(self.data) + end + + def method_missing(method_name, *arguments, &block) + if License.column_names.include?(method_name.to_s) + super + elsif license && license.respond_to?(method_name) + license.send(method_name, *arguments, &block) + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + if License.column_names.include?(method_name.to_s) + super + elsif license && license.respond_to?(method_name) + true + else + super + end + (license && license.respond_to?(method_name)) || super + end + + def active_user_restriction_exceeded? + return false unless self.restricted?(:active_user_count) + + User.active.count > self.restrictions[:active_user_count] + end + + private + + def reset_current + self.class.reset_current + end + + def reset_license + @license = nil + end + + def valid_license + return if self.license && self.license.valid? + + # TODO: Clearer message + self.errors.add(:license, "is invalid.") + end +end diff --git a/app/views/admin/licenses/new.html.haml b/app/views/admin/licenses/new.html.haml new file mode 100644 index 0000000000..48c3943310 --- /dev/null +++ b/app/views/admin/licenses/new.html.haml @@ -0,0 +1,20 @@ +%h3.page-title Upload License + +%p.light + To #{License.current ? "continue" : "start"} using GitLab Enterprise Edition, upload the .gitlab-license file you have received from GitLab B.V. + +%hr += form_for @license, url: admin_license_path, html: { multipart: true, class: 'form-horizontal fieldset-form' } do |f| + - if @license.errors.any? + #error_explanation + .alert.alert-danger + - @license.errors.full_messages.each do |msg| + %p= msg + + .form-group + = f.label :data_file, "License", class: 'control-label col-sm-2' + .col-sm-10 + = f.file_field :data_file, accept: ".gitlab-license,.gitlab_license" + + .form-actions + = f.submit 'Upload', class: 'btn btn-primary' diff --git a/app/views/admin/licenses/show.html.haml b/app/views/admin/licenses/show.html.haml new file mode 100644 index 0000000000..f8b8b98ec7 --- /dev/null +++ b/app/views/admin/licenses/show.html.haml @@ -0,0 +1,116 @@ +%h3.page-title + Your License + = link_to 'Upload License', new_admin_license_path, class: "btn btn-new pull-right" + +%hr + +.row + .col-md-6 + .panel.panel-default + .panel-heading + Licensed to + %ul.well-list + - @license.licensee.each do |label, value| + %li + %span.light #{label}: + %strong= value + + .panel.panel-default + .panel-heading + Details + %ul.well-list + %li + %span.light Issued: + %strong= time_ago_with_tooltip @license.issued_at + %li + %span.light Uploaded: + %strong= time_ago_with_tooltip @license.created_at + %li + %span.light Expires: + %strong + - if @license.will_expire? + = time_ago_with_tooltip @license.expires_at + - if @license.expired? + %span.label.label-danger.pull-right + %strong Expired + - else + Never + + .panel.panel-default + .panel-heading + Restrictions + %ul.well-list + %li + %span.light Active users: + %strong + - if @license.restricted?(:active_user_count) + - restricted_user_count = @license.restrictions[:active_user_count] + - active_user_count = User.active.count + #{restricted_user_count} users + + - if active_user_count > restricted_user_count + %span.label.label-danger.pull-right + %strong Exceeded by #{active_user_count - restricted_user_count} users + - elsif restricted_user_count > active_user_count + %span.label.label-success.pull-right + %strong #{restricted_user_count - active_user_count} more allowed + - else + %span.label.label-info.pull-right + %strong Right at the limit + + - else + Unlimited + + .col-md-6 + .panel.panel-info + .panel-heading + Download license + .panel-body + %p Your license will be included in your GitLab backup and will survive upgrades, so in normal usage you should never need to re-upload your .gitlab-license. + %p Still, we recommend keeping it save somewhere, because if you ever need it and have lost it, you will need to request GitLab B.V. to send it to you again. + %br + = link_to 'Download license', download_admin_license_path, class: "btn btn-info" + + + .panel.panel-danger + .panel-heading + Remove license + .panel-body + %p If you remove this license, GitLab will fall back on the previous license, if any. + %p If there is no previous license or if the previous license has expired, some GitLab functionality will be blocked until a new, valid license is uploaded. + %br + = link_to 'Remove license', admin_license_path, data: { confirm: "LICENSE WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + +- if @previous_licenses.any? + %h4 Previous Licenses + + .panel.panel-default + %table.table + %thead.panel-heading + %tr + - @license.licensee.keys.each do |label| + %th= label + %th Issued at + %th Uploaded at + %th Expired at + %th Active users + %tbody + - @previous_licenses.each do |license| + %tr + - @license.licensee.keys.each do |label| + %td= license.licensee[label] + %td + %span + = license.issued_at + %td + %span + = license.created_at + %td + %span + = license.expires_at || "Never" + %td + %span + - if license.restricted?(:active_user_count) + #{license.restrictions[:active_user_count]} users + - else + Unlimited diff --git a/app/views/layouts/_broadcast.html.haml b/app/views/layouts/_broadcast.html.haml index e7d477c225..267eaa411c 100644 --- a/app/views/layouts/_broadcast.html.haml +++ b/app/views/layouts/_broadcast.html.haml @@ -2,3 +2,8 @@ .broadcast-message{ style: broadcast_styling(broadcast_message) } %i.fa.fa-bullhorn = broadcast_message.message + +- if (message = license_message) && message.present? + .broadcast-message + %i.fa.fa-bullhorn + = message diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index fda6880163..2aeccfb08b 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -72,3 +72,9 @@ = icon('cogs fw') %span Settings + + = nav_link(controller: :licenses) do + = link_to admin_license_path, title: 'License', data: {placement: 'right'} do + = icon('check fw') + %span + License diff --git a/config/initializers/license.rb b/config/initializers/license.rb new file mode 100644 index 0000000000..eda165b62e --- /dev/null +++ b/config/initializers/license.rb @@ -0,0 +1,16 @@ +public_key_file = File.read(Rails.root.join(".license_encryption_key.pub")) +public_key = OpenSSL::PKey::RSA.new(public_key_file) +Gitlab::License.encryption_key = public_key + +# TODO: Validate encryptionkey + +# Needed to run migration +if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('licenses') + message = LicenseHelper.license_message(signed_in: true, is_admin: true) + if message.present? + # TODO: Change + warn "WARNING: #{message}" + end + + # TODO: Warn about too many users +end diff --git a/config/routes.rb b/config/routes.rb index 81d8cac102..fc83a14c88 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -211,6 +211,10 @@ Gitlab::Application.routes.draw do resources :services end + resource :license, only: [:show, :new, :create, :destroy] do + get :download, on: :member + end + root to: 'dashboard#index' end diff --git a/db/migrate/20150501095306_create_licenses.rb b/db/migrate/20150501095306_create_licenses.rb new file mode 100644 index 0000000000..730c544c82 --- /dev/null +++ b/db/migrate/20150501095306_create_licenses.rb @@ -0,0 +1,9 @@ +class CreateLicenses < ActiveRecord::Migration + def change + create_table :licenses do |t| + t.text :data, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 6728c211ab..3b57047ef6 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: 20150425173433) do +ActiveRecord::Schema.define(version: 20150501095306) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -210,6 +210,12 @@ ActiveRecord::Schema.define(version: 20150425173433) do t.string "provider" end + create_table "licenses", force: true do |t| + t.text "data", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "members", force: true do |t| t.integer "access_level", null: false t.integer "source_id", null: false @@ -302,7 +308,7 @@ ActiveRecord::Schema.define(version: 20150425173433) do end add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree - add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree + add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree @@ -410,11 +416,11 @@ ActiveRecord::Schema.define(version: 20150425173433) do t.string "avatar" t.string "import_status" t.float "repository_size", default: 0.0 - t.text "merge_requests_template" t.integer "star_count", default: 0, null: false - t.boolean "merge_requests_rebase_enabled", default: false t.string "import_type" t.string "import_source" + t.text "merge_requests_template" + t.boolean "merge_requests_rebase_enabled", default: false t.boolean "merge_requests_rebase_default", default: true end @@ -541,7 +547,6 @@ ActiveRecord::Schema.define(version: 20150425173433) do t.string "unconfirmed_email" t.boolean "hide_no_ssh_key", default: false t.string "website_url", default: "", null: false - t.datetime "admin_email_unsubscribed_at" t.string "github_access_token" t.string "gitlab_access_token" t.string "notification_email" @@ -549,6 +554,7 @@ ActiveRecord::Schema.define(version: 20150425173433) do t.boolean "password_automatically_set", default: false t.string "bitbucket_access_token" t.string "bitbucket_access_token_secret" + t.datetime "admin_email_unsubscribed_at" t.string "location" t.string "public_email", default: "", null: false end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index a443f44f79..4832c73186 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -102,8 +102,9 @@ module Gitlab end def user_push_access_check(changes) - unless user && user_allowed? - return build_status_object(false, "You don't have access") + if ::License.block_changes? + message = ::LicenseHelper.license_message(signed_in: true, is_admin: (user && user.is_admin?)) + return build_status_object(false, message) end if changes.blank? From 257f8057a29ad37591b7a57b02c09902ac885880 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 3 May 2015 15:41:40 +0200 Subject: [PATCH 02/17] Minor tweaks. --- app/controllers/admin/licenses_controller.rb | 11 ++++++----- app/models/ability.rb | 2 +- app/models/license.rb | 5 ++++- app/views/admin/licenses/new.html.haml | 2 +- app/views/admin/licenses/show.html.haml | 16 ++++++++++------ 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/app/controllers/admin/licenses_controller.rb b/app/controllers/admin/licenses_controller.rb index 5b94869ac0..c66e7defec 100644 --- a/app/controllers/admin/licenses_controller.rb +++ b/app/controllers/admin/licenses_controller.rb @@ -5,7 +5,7 @@ class Admin::LicensesController < Admin::ApplicationController respond_to :html def show - @previous_licenses = License.all.to_a[0..-2].reverse + @previous_licenses = License.previous end def download @@ -17,12 +17,11 @@ class Admin::LicensesController < Admin::ApplicationController end def create - @license = License.new - @license.data_file = license_params[:data_file] + @license = License.new(license_params) respond_with(@license, location: admin_license_path) do if @license.save - flash[:notice] = "The license was successfully uploaded." + flash[:notice] = "The license was successfully uploaded. You can see the details below." end end end @@ -30,7 +29,9 @@ class Admin::LicensesController < Admin::ApplicationController def destroy license.destroy - redirect_to admin_license_path, notice: "The license was removed." + message = "The license was removed." + + redirect_to admin_license_path, notice: message end private diff --git a/app/models/ability.rb b/app/models/ability.rb index 155e199345..7962cae8c7 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -78,7 +78,7 @@ class Ability def project_abilities(user, project) rules = [] key = "/user/#{user.id}/project/#{project.id}" - rules = RequestStore.store[key] ||= begin + RequestStore.store[key] ||= begin team = project.team # Rules based on role in project diff --git a/app/models/license.rb b/app/models/license.rb index 87d132e8d6..eada388a47 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -7,6 +7,8 @@ class License < ActiveRecord::Base after_create :reset_current after_destroy :reset_current + scope :previous, -> { order(created_at: :desc).offset(1) } + class << self def current return @current if @current @@ -27,7 +29,8 @@ class License < ActiveRecord::Base end def data_filename - clean_company_name = self.licensee.values.first.gsub(/[^A-Za-z0-9]/, "") + company_name = self.licensee["Company"] || self.licensee.values.first + clean_company_name = company_name.gsub(/[^A-Za-z0-9]/, "") "#{clean_company_name}.gitlab-license" end diff --git a/app/views/admin/licenses/new.html.haml b/app/views/admin/licenses/new.html.haml index 48c3943310..6b0d69e1ca 100644 --- a/app/views/admin/licenses/new.html.haml +++ b/app/views/admin/licenses/new.html.haml @@ -14,7 +14,7 @@ .form-group = f.label :data_file, "License", class: 'control-label col-sm-2' .col-sm-10 - = f.file_field :data_file, accept: ".gitlab-license,.gitlab_license" + = f.file_field :data_file, accept: ".gitlab-license,.gitlab_license,.txt" .form-actions = f.submit 'Upload', class: 'btn btn-primary' diff --git a/app/views/admin/licenses/show.html.haml b/app/views/admin/licenses/show.html.haml index f8b8b98ec7..5cd6ac9b67 100644 --- a/app/views/admin/licenses/show.html.haml +++ b/app/views/admin/licenses/show.html.haml @@ -1,6 +1,6 @@ %h3.page-title Your License - = link_to 'Upload License', new_admin_license_path, class: "btn btn-new pull-right" + = link_to 'Upload New License', new_admin_license_path, class: "btn btn-new pull-right" %hr @@ -26,7 +26,11 @@ %span.light Uploaded: %strong= time_ago_with_tooltip @license.created_at %li - %span.light Expires: + %span.light + - if @license.expired? + Expired: + - else + Expires: %strong - if @license.will_expire? = time_ago_with_tooltip @license.expires_at @@ -66,8 +70,8 @@ .panel-heading Download license .panel-body - %p Your license will be included in your GitLab backup and will survive upgrades, so in normal usage you should never need to re-upload your .gitlab-license. - %p Still, we recommend keeping it save somewhere, because if you ever need it and have lost it, you will need to request GitLab B.V. to send it to you again. + %p Your license will be included in your GitLab backup and will survive upgrades, so in normal usage you should never need to re-upload your .gitlab-license file. + %p Still, we recommend keeping it save somewhere. Otherwise, if you ever need it and have lost it, you will need to request GitLab B.V. to send it to you again. %br = link_to 'Download license', download_admin_license_path, class: "btn btn-info" @@ -79,10 +83,10 @@ %p If you remove this license, GitLab will fall back on the previous license, if any. %p If there is no previous license or if the previous license has expired, some GitLab functionality will be blocked until a new, valid license is uploaded. %br - = link_to 'Remove license', admin_license_path, data: { confirm: "LICENSE WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + = link_to 'Remove license', admin_license_path, data: { confirm: "Are you sure you want to remove the license?" }, method: :delete, class: "btn btn-remove" - if @previous_licenses.any? - %h4 Previous Licenses + %h4 License History .panel.panel-default %table.table From bff1b8f8e70266cb1a1f86b58e09157cbfb96959 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 10:40:38 +0200 Subject: [PATCH 03/17] More code cleanup. --- app/helpers/license_helper.rb | 90 ++++++++++++++----------- app/models/ability.rb | 38 ++++++----- app/views/admin/licenses/new.html.haml | 3 +- app/views/admin/licenses/show.html.haml | 3 +- app/views/layouts/_broadcast.html.haml | 4 +- 5 files changed, 80 insertions(+), 58 deletions(-) diff --git a/app/helpers/license_helper.rb b/app/helpers/license_helper.rb index 0a5b88885d..00de8e72ab 100644 --- a/app/helpers/license_helper.rb +++ b/app/helpers/license_helper.rb @@ -1,53 +1,67 @@ module LicenseHelper - # better text def license_message(signed_in: signed_in?, is_admin: (current_user && current_user.is_admin?)) + if License.current + yes_license_message(signed_in, is_admin) + else + no_license_message(signed_in, is_admin) + end + end + + private + + def no_license_message(signed_in, is_admin) + message = [] + + message << "No GitLab Enterprise Edition license has been provided yet." + message << "Pushing code and creation of issues and merge requests has been disabled." + + message << + if is_admin + "Upload a license in the admin area" + else + "Ask an admin to upload a license" + end + + message << "to activate this functionality." + + message.join(" ") + end + + def yes_license_message(signed_in, is_admin) + license = License.current + + return unless signed_in + + return unless (license.notify_admins? && is_admin) || license.notify_users? message = [] - license = License.current - if license - return unless signed_in + message << "The GitLab Enterprise Edition license" + message << (license.expired? ? "expired" : "will expire") + message << "on #{license.expires_at}." - return unless (license.notify_admins? && is_admin) || license.notify_users? - - message << "The GitLab Enterprise Edition license" - message << (license.expired? ? "expired" : "will expire") - message << "on #{license.expires_at}." - - if license.expired? && license.will_block_changes? - message << "Pushing code and creation of issues and merge requests" + if license.expired? && license.will_block_changes? + message << "Pushing code and creation of issues and merge requests" + message << if license.block_changes? - message << "has been disabled." + "has been disabled." else - message << "will be disabled on #{license.block_changes_at}." + "will be disabled on #{license.block_changes_at}." end - end - - if is_admin - message << "Upload a new license in the admin area" - else - message << "Ask an admin to upload a new license" - end - - if license.block_changes? - message << "to restore service." - else - message << "to ensure uninterrupted service." - end - else - message << "No GitLab Enterprise Edition license has been provided yet." - message << "Pushing code and creation of issues and merge requests has been disabled." - - if signed_in && is_admin - message << "Upload a license in the admin area" - else - message << "Ask an admin to upload a license" - end - - message << "to restore service." end + message << + if is_admin + "Upload a new license in the admin area" + else + "Ask an admin to upload a new license" + end + + message << "to" + message << (license.block_changes? ? "restore" : "ensure uninterrupted") + message << "service." + message.join(" ") end diff --git a/app/models/ability.rb b/app/models/ability.rb index 7962cae8c7..0dbadff6db 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -7,28 +7,34 @@ class Ability abilities = case subject.class.name - when "Project" then project_abilities(user, subject) - when "Issue" then issue_abilities(user, subject) - when "Note" then note_abilities(user, subject) - when "ProjectSnippet" then project_snippet_abilities(user, subject) - when "PersonalSnippet" then personal_snippet_abilities(user, subject) - when "MergeRequest" then merge_request_abilities(user, subject) - when "Group" then group_abilities(user, subject) - when "Namespace" then namespace_abilities(user, subject) - when "GroupMember" then group_member_abilities(user, subject) + when "Project" then project_abilities(user, subject) + when "Issue" then issue_abilities(user, subject) + when "Note" then note_abilities(user, subject) + when "ProjectSnippet" then project_snippet_abilities(user, subject) + when "PersonalSnippet" then personal_snippet_abilities(user, subject) + when "MergeRequest" then merge_request_abilities(user, subject) + when "Group" then group_abilities(user, subject) + when "Namespace" then namespace_abilities(user, subject) + when "GroupMember" then group_member_abilities(user, subject) else [] - end.concat(global_abilities(user)) + end - if License.block_changes? - abilities.delete(:push_code) - abilities.delete(:push_code_to_protected_branches) - abilities.delete(:write_issue) - abilities.delete(:write_merge_request) - end + abilities.concat(global_abilities(user)) + + abilities -= license_blocked_abilities if License.block_changes? abilities end + def license_blocked_abilities + [ + :push_code, + :push_code_to_protected_branches, + :write_issue, + :write_merge_request + ] + end + # List of possible abilities # for non-authenticated user def not_auth_abilities(user, subject) diff --git a/app/views/admin/licenses/new.html.haml b/app/views/admin/licenses/new.html.haml index 6b0d69e1ca..d1312d14a8 100644 --- a/app/views/admin/licenses/new.html.haml +++ b/app/views/admin/licenses/new.html.haml @@ -1,7 +1,8 @@ +- # page_title "Upload License" %h3.page-title Upload License %p.light - To #{License.current ? "continue" : "start"} using GitLab Enterprise Edition, upload the .gitlab-license file you have received from GitLab B.V. + To #{License.current ? "continue" : "start"} using GitLab Enterprise Edition, upload the .gitlab-license file you have received from GitLab B.V.. %hr = form_for @license, url: admin_license_path, html: { multipart: true, class: 'form-horizontal fieldset-form' } do |f| diff --git a/app/views/admin/licenses/show.html.haml b/app/views/admin/licenses/show.html.haml index 5cd6ac9b67..55761082aa 100644 --- a/app/views/admin/licenses/show.html.haml +++ b/app/views/admin/licenses/show.html.haml @@ -1,3 +1,4 @@ +- # page_title "License" %h3.page-title Your License = link_to 'Upload New License', new_admin_license_path, class: "btn btn-new pull-right" @@ -71,7 +72,7 @@ Download license .panel-body %p Your license will be included in your GitLab backup and will survive upgrades, so in normal usage you should never need to re-upload your .gitlab-license file. - %p Still, we recommend keeping it save somewhere. Otherwise, if you ever need it and have lost it, you will need to request GitLab B.V. to send it to you again. + %p Still, we recommend keeping a backup saved somewhere. Otherwise, if you ever need it and have lost it, you will need to request GitLab B.V. to send it to you again. %br = link_to 'Download license', download_admin_license_path, class: "btn btn-info" diff --git a/app/views/layouts/_broadcast.html.haml b/app/views/layouts/_broadcast.html.haml index 267eaa411c..7683c15cd5 100644 --- a/app/views/layouts/_broadcast.html.haml +++ b/app/views/layouts/_broadcast.html.haml @@ -1,9 +1,9 @@ - if broadcast_message.present? .broadcast-message{ style: broadcast_styling(broadcast_message) } - %i.fa.fa-bullhorn + = icon('bullhorn') = broadcast_message.message - if (message = license_message) && message.present? .broadcast-message - %i.fa.fa-bullhorn + = icon('bullhorn') = message From 10e6aa62b9b85cab43e8860f729767057977f0b4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 10:53:51 +0200 Subject: [PATCH 04/17] Validate number of active users at time of upload. --- app/models/license.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/models/license.rb b/app/models/license.rb index eada388a47..c6f4b64bf9 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -1,6 +1,7 @@ class License < ActiveRecord::Base validates :data, presence: true validate :valid_license + validate :active_user_count, unless: :persisted? before_validation :reset_license, if: :data_changed? @@ -50,6 +51,10 @@ class License < ActiveRecord::Base @license ||= Gitlab::License.import(self.data) end + def license? + self.license && self.license.valid? + end + def method_missing(method_name, *arguments, &block) if License.column_names.include?(method_name.to_s) super @@ -88,9 +93,20 @@ class License < ActiveRecord::Base end def valid_license - return if self.license && self.license.valid? + return if license? # TODO: Clearer message self.errors.add(:license, "is invalid.") end + + def active_user_count + return unless self.license? && self.restricted?(:active_user_count) + + restricted_user_count = @license.restrictions[:active_user_count] + active_user_count = User.active.count + + return if active_user_count <= restricted_user_count + + self.errors.add(:base, "This license allows #{restricted_user_count} active users. This GitLab installation currently has #{active_user_count}, i.e. #{active_user_count - restricted_user_count} too many.") + end end From 6ff860d1fe08cf4098af9a3f71fc0794b1141a36 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 10:54:07 +0200 Subject: [PATCH 05/17] Show nice error when no license was selected. --- app/controllers/admin/licenses_controller.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/controllers/admin/licenses_controller.rb b/app/controllers/admin/licenses_controller.rb index c66e7defec..999f07dce0 100644 --- a/app/controllers/admin/licenses_controller.rb +++ b/app/controllers/admin/licenses_controller.rb @@ -17,6 +17,14 @@ class Admin::LicensesController < Admin::ApplicationController end def create + unless params[:license] + flash.now[:alert] = "No license was selected." + + @license = License.new + render :new + return + end + @license = License.new(license_params) respond_with(@license, location: admin_license_path) do @@ -29,9 +37,7 @@ class Admin::LicensesController < Admin::ApplicationController def destroy license.destroy - message = "The license was removed." - - redirect_to admin_license_path, notice: message + redirect_to admin_license_path, notice: "The license was removed." end private From 6d093d84b1406d2f4a9cb7a580c42c6bc1fbe4b1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 11:49:28 +0200 Subject: [PATCH 06/17] Show prettier error message when license file is invalid. --- Gemfile | 2 +- Gemfile.lock | 4 ++-- app/helpers/license_helper.rb | 11 ++++++----- app/models/license.rb | 14 ++++++++++---- app/views/layouts/_broadcast.html.haml | 4 ++-- config/initializers/license.rb | 15 +++++++-------- 6 files changed, 28 insertions(+), 22 deletions(-) diff --git a/Gemfile b/Gemfile index d0a434c8bb..3460105ab5 100644 --- a/Gemfile +++ b/Gemfile @@ -203,7 +203,7 @@ gem 'request_store' gem "virtus" gem 'addressable' -gem "gitlab-license" +gem "gitlab-license", "~> 0.0.2" group :development do gem 'brakeman', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 3f3837e48d..423f4e1964 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -209,7 +209,7 @@ GEM diff-lcs (~> 1.1) mime-types (~> 1.15) posix-spawn (~> 0.3) - gitlab-license (0.0.1) + gitlab-license (0.0.2) gitlab-linguist (3.0.1) charlock_holmes (~> 0.6.6) escape_utils (~> 0.2.4) @@ -707,7 +707,7 @@ DEPENDENCIES github-markup gitlab-flowdock-git-hook (~> 0.4.2) gitlab-grack (~> 2.0.2) - gitlab-license + gitlab-license (~> 0.0.2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) gitlab_git (~> 7.1.10) diff --git a/app/helpers/license_helper.rb b/app/helpers/license_helper.rb index 00de8e72ab..a9ed65e5d5 100644 --- a/app/helpers/license_helper.rb +++ b/app/helpers/license_helper.rb @@ -1,10 +1,11 @@ module LicenseHelper def license_message(signed_in: signed_in?, is_admin: (current_user && current_user.is_admin?)) - if License.current - yes_license_message(signed_in, is_admin) - else - no_license_message(signed_in, is_admin) - end + @license_message ||= + if License.current + yes_license_message(signed_in, is_admin) + else + no_license_message(signed_in, is_admin) + end end private diff --git a/app/models/license.rb b/app/models/license.rb index c6f4b64bf9..e36224cabb 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -48,7 +48,12 @@ class License < ActiveRecord::Base def license return nil unless self.data - @license ||= Gitlab::License.import(self.data) + @license ||= + begin + Gitlab::License.import(self.data) + rescue Gitlab::License::ImportError + nil + end end def license? @@ -95,8 +100,7 @@ class License < ActiveRecord::Base def valid_license return if license? - # TODO: Clearer message - self.errors.add(:license, "is invalid.") + self.errors.add(:base, "The license file is invalid. Make sure it is exactly as you received it from GitLab B.V.") end def active_user_count @@ -107,6 +111,8 @@ class License < ActiveRecord::Base return if active_user_count <= restricted_user_count - self.errors.add(:base, "This license allows #{restricted_user_count} active users. This GitLab installation currently has #{active_user_count}, i.e. #{active_user_count - restricted_user_count} too many.") + message = "This license allows #{restricted_user_count} active users. " + message << "This GitLab installation currently has #{active_user_count}, i.e. #{active_user_count - restricted_user_count} too many." + self.errors.add(:base, message) end end diff --git a/app/views/layouts/_broadcast.html.haml b/app/views/layouts/_broadcast.html.haml index 7683c15cd5..e01663e059 100644 --- a/app/views/layouts/_broadcast.html.haml +++ b/app/views/layouts/_broadcast.html.haml @@ -3,7 +3,7 @@ = icon('bullhorn') = broadcast_message.message -- if (message = license_message) && message.present? +- if license_message.present? .broadcast-message = icon('bullhorn') - = message + = license_message diff --git a/config/initializers/license.rb b/config/initializers/license.rb index eda165b62e..4f4199ae56 100644 --- a/config/initializers/license.rb +++ b/config/initializers/license.rb @@ -1,16 +1,15 @@ -public_key_file = File.read(Rails.root.join(".license_encryption_key.pub")) -public_key = OpenSSL::PKey::RSA.new(public_key_file) -Gitlab::License.encryption_key = public_key - -# TODO: Validate encryptionkey +begin + public_key_file = File.read(Rails.root.join(".license_encryption_key.pub")) + public_key = OpenSSL::PKey::RSA.new(public_key_file) + Gitlab::License.encryption_key = public_key +rescue + warn "WARNING: No valid license encryption key provided." +end # Needed to run migration if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('licenses') message = LicenseHelper.license_message(signed_in: true, is_admin: true) if message.present? - # TODO: Change warn "WARNING: #{message}" end - - # TODO: Warn about too many users end From 60323c61438ca68212b25ff78118a790a4d00c87 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 12:19:33 +0200 Subject: [PATCH 07/17] Validate that new license hasn't expired already. --- app/models/license.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/models/license.rb b/app/models/license.rb index e36224cabb..2d8edf1899 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -2,6 +2,7 @@ class License < ActiveRecord::Base validates :data, presence: true validate :valid_license validate :active_user_count, unless: :persisted? + validate :not_expired, unless: :persisted? before_validation :reset_license, if: :data_changed? @@ -106,13 +107,20 @@ class License < ActiveRecord::Base def active_user_count return unless self.license? && self.restricted?(:active_user_count) - restricted_user_count = @license.restrictions[:active_user_count] + restricted_user_count = self.restrictions[:active_user_count] active_user_count = User.active.count return if active_user_count <= restricted_user_count message = "This license allows #{restricted_user_count} active users. " - message << "This GitLab installation currently has #{active_user_count}, i.e. #{active_user_count - restricted_user_count} too many." + message << "This GitLab installation currently has #{active_user_count}, " + message << "i.e. #{active_user_count - restricted_user_count} too many." self.errors.add(:base, message) end + + def not_expired + return unless self.license? && self.expired? + + self.errors.add(:base, "This license has already expired.") + end end From 5a14f5a49e7a15a21576c1cda0fd3e06384f96e7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 12:19:47 +0200 Subject: [PATCH 08/17] Tweak flash messages around license upload/removal. --- app/controllers/admin/licenses_controller.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/licenses_controller.rb b/app/controllers/admin/licenses_controller.rb index 999f07dce0..f908a5e5bf 100644 --- a/app/controllers/admin/licenses_controller.rb +++ b/app/controllers/admin/licenses_controller.rb @@ -29,7 +29,7 @@ class Admin::LicensesController < Admin::ApplicationController respond_with(@license, location: admin_license_path) do if @license.save - flash[:notice] = "The license was successfully uploaded. You can see the details below." + flash[:notice] = "The license was successfully uploaded and is now active. You can see the details below." end end end @@ -37,7 +37,14 @@ class Admin::LicensesController < Admin::ApplicationController def destroy license.destroy - redirect_to admin_license_path, notice: "The license was removed." + message = "The license was removed. " + if License.current + flash[:notice] = "The license was removed. GitLab has fallen back on the previous license." + else + flash[:alert] = "The license was removed. GitLab now no longer has a valid license." + end + + redirect_to admin_license_path end private @@ -52,6 +59,7 @@ class Admin::LicensesController < Admin::ApplicationController def require_license return if license + flash.keep redirect_to new_admin_license_path end From e7154898823de2ba3e80ffa2920761da4cda2f51 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 12:44:06 +0200 Subject: [PATCH 09/17] Setup license in test env. --- spec/support/test_env.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 44d70e741b..42c63b4bbf 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -19,6 +19,8 @@ module TestEnv # See gitlab.yml.example test section for paths # def init(opts = {}) + setup_license + # Disable mailer for spinach tests disable_mailer if opts[:mailer] == false @@ -41,6 +43,14 @@ module TestEnv allow_any_instance_of(NotificationService).to receive(:mailer).and_call_original end + def setup_license + Gitlab::License.encryption_key = OpenSSL::PKey::RSA.generate(2048) + + gl_license = Gitlab::License.new(issued_at: Date.today, licensee: { "Name" => "GitLab Test Env" }) + + License.create(data: gl_license.export) + end + # Clean /tmp/tests # # Keeps gitlab-shell and gitlab-test From caa09e69513acbc097d6a9916c37d905dd6a5982 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 13:02:17 +0200 Subject: [PATCH 10/17] Add License unit test. --- app/models/license.rb | 7 -- spec/models/license_spec.rb | 174 ++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 spec/models/license_spec.rb diff --git a/app/models/license.rb b/app/models/license.rb index 2d8edf1899..c172591f77 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -79,13 +79,6 @@ class License < ActiveRecord::Base else super end - (license && license.respond_to?(method_name)) || super - end - - def active_user_restriction_exceeded? - return false unless self.restricted?(:active_user_count) - - User.active.count > self.restrictions[:active_user_count] end private diff --git a/spec/models/license_spec.rb b/spec/models/license_spec.rb new file mode 100644 index 0000000000..04150481ab --- /dev/null +++ b/spec/models/license_spec.rb @@ -0,0 +1,174 @@ +require "spec_helper" + +describe License do + let(:gl_license) { Gitlab::License.new(issued_at: Date.today, licensee: { "Name" => "GitLab Test Env" }) } + let(:license) { License.new(data: gl_license.export) } + + describe "Validation" do + describe "Valid license" do + context "when the license is provided" do + it "is valid" do + expect(license).to be_valid + end + end + + context "when no license is provided" do + before do + license.data = nil + end + + it "is invalid" do + expect(license).to_not be_valid + end + end + end + + describe "Active user count" do + context "when there is no active user count restriction" do + it "is valid" do + expect(license).to be_valid + end + end + + context "when the active user count restriction is exceeded" do + before do + gl_license.restrictions = { active_user_count: User.active.count - 1 } + end + + it "is invalid" do + expect(license).to_not be_valid + end + end + + context "when the active user count restriction is not exceeded" do + before do + gl_license.restrictions = { active_user_count: User.active.count + 1 } + end + + it "is valid" do + expect(license).to be_valid + end + end + end + + describe "Not expired" do + context "when the license doesn't expire" do + it "is valid" do + expect(license).to be_valid + end + end + + context "when the license has expired" do + before do + gl_license.expires_at = Date.yesterday + end + + it "is valid" do + expect(license).to_not be_valid + end + + end + + context "when the license has yet to expire" do + before do + gl_license.expires_at = Date.tomorrow + end + + it "is valid" do + expect(license).to be_valid + end + end + end + end + + describe "Class methods" do + let!(:license) { License.last } + + before do + License.reset_current + allow(License).to receive(:last).and_return(license) + end + + describe ".current" do + context "when there is no license" do + let!(:license) { nil } + + it "returns nil" do + expect(License.current).to be_nil + end + end + + context "when the license is invalid" do + before do + allow(license).to receive(:valid?).and_return(false) + end + + it "returns nil" do + expect(License.current).to be_nil + end + end + + context "when the license is valid" do + it "returns the license" do + expect(License.current) + end + end + end + + describe ".block_changes?" do + context "when there is no current license" do + before do + allow(License).to receive(:current).and_return(nil) + end + + it "returns true" do + expect(License.block_changes?).to be_truthy + end + end + + context "when the current license is set to block changes" do + before do + allow(license).to receive(:block_changes?).and_return(true) + end + + it "returns true" do + expect(License.block_changes?).to be_truthy + end + end + + context "when the current license doesn't block changes" do + it "returns false" do + expect(License.block_changes?).to be_falsey + end + end + end + end + + describe "#license" do + context "when no data is provided" do + before do + license.data = nil + end + + it "returns nil" do + expect(license.license).to be_nil + end + end + + context "when corrupt license data is provided" do + before do + license.data = "whatever" + end + + it "returns nil" do + expect(license.license).to be_nil + end + end + + context "when valid license data is provided" do + it "returns the license" do + expect(license.license).to_not be_nil + end + end + end +end From f00e1d3dda37ca4e02411b64b7e1e56c43fedb96 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 13:04:25 +0200 Subject: [PATCH 11/17] Add page titles. --- app/views/admin/licenses/new.html.haml | 2 +- app/views/admin/licenses/show.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/licenses/new.html.haml b/app/views/admin/licenses/new.html.haml index d1312d14a8..4aa5df5451 100644 --- a/app/views/admin/licenses/new.html.haml +++ b/app/views/admin/licenses/new.html.haml @@ -1,4 +1,4 @@ -- # page_title "Upload License" +- page_title "Upload License" %h3.page-title Upload License %p.light diff --git a/app/views/admin/licenses/show.html.haml b/app/views/admin/licenses/show.html.haml index 55761082aa..33263f685a 100644 --- a/app/views/admin/licenses/show.html.haml +++ b/app/views/admin/licenses/show.html.haml @@ -1,4 +1,4 @@ -- # page_title "License" +- page_title "License" %h3.page-title Your License = link_to 'Upload New License', new_admin_license_path, class: "btn btn-new pull-right" From 3a6b9ad85e11eaf7d986f9366eb5083c4d127ea5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 13:20:40 +0200 Subject: [PATCH 12/17] Test license blocking changes. --- .../security/project/internal_access_spec.rb | 28 +++++++++++++++++++ .../security/project/private_access_spec.rb | 28 +++++++++++++++++++ .../security/project/public_access_spec.rb | 28 +++++++++++++++++++ spec/lib/gitlab/git_access_spec.rb | 21 ++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 8d1bfd2522..5bdb717288 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -224,4 +224,32 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_denied_for :user } it { is_expected.to be_denied_for :visitor } end + + context "when license blocks changes" do + before do + allow(License).to receive(:block_changes?).and_return(true) + end + + describe "GET /:project_path/issues/new" do + subject { new_namespace_project_issue_path(project.namespace, project) } + + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_denied_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/merge_requests/new" do + subject { new_namespace_project_merge_request_path(project.namespace, project) } + + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_denied_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end end diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 9021ff3318..c5b5d94eaf 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -202,4 +202,32 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for :user } it { is_expected.to be_denied_for :visitor } end + + context "when license blocks changes" do + before do + allow(License).to receive(:block_changes?).and_return(true) + end + + describe "GET /:project_path/issues/new" do + subject { new_namespace_project_issue_path(project.namespace, project) } + + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_denied_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/merge_requests/new" do + subject { new_namespace_project_merge_request_path(project.namespace, project) } + + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_denied_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end end diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 6ec190ed77..1e2e31d002 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -229,4 +229,32 @@ describe "Public Project Access", feature: true do it { is_expected.to be_denied_for :user } it { is_expected.to be_denied_for :visitor } end + + context "when license blocks changes" do + before do + allow(License).to receive(:block_changes?).and_return(true) + end + + describe "GET /:project_path/issues/new" do + subject { new_namespace_project_issue_path(project.namespace, project) } + + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_denied_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/merge_requests/new" do + subject { new_namespace_project_merge_request_path(project.namespace, project) } + + it { is_expected.to be_denied_for master } + it { is_expected.to be_denied_for reporter } + it { is_expected.to be_denied_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index c2be64b5d3..af33c63543 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -231,6 +231,27 @@ describe Gitlab::GitAccess do end end end + + context "when license blocks changes" do + before do + allow(License).to receive(:block_changes?).and_return(true) + end + + permissions_matrix.keys.each do |role| + describe "#{role} access" do + before { protect_feature_branch } + before { project.team << [user, role] } + + permissions_matrix[role].each do |action, allowed| + context action do + subject { access.push_access_check(changes[action]) } + + it { expect(subject.allowed?).to be_falsey } + end + end + end + end + end end describe "pass_git_hooks?" do From 97bfd764b7db77d0c59ce89151f634251a9002ee Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 14:13:57 +0200 Subject: [PATCH 13/17] Add feature specs for license upload. --- app/models/license.rb | 7 -- app/views/admin/licenses/new.html.haml | 2 +- app/views/admin/licenses/show.html.haml | 2 +- features/admin/license.feature | 44 +++++++++++++ features/steps/admin/license.rb | 86 +++++++++++++++++++++++++ features/steps/shared/paths.rb | 4 ++ spec/factories.rb | 13 ++++ spec/models/license_spec.rb | 4 +- spec/support/test_env.rb | 4 +- 9 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 features/admin/license.feature create mode 100644 features/steps/admin/license.rb diff --git a/app/models/license.rb b/app/models/license.rb index c172591f77..34ed22aeaa 100644 --- a/app/models/license.rb +++ b/app/models/license.rb @@ -1,5 +1,4 @@ class License < ActiveRecord::Base - validates :data, presence: true validate :valid_license validate :active_user_count, unless: :persisted? validate :not_expired, unless: :persisted? @@ -36,12 +35,6 @@ class License < ActiveRecord::Base "#{clean_company_name}.gitlab-license" end - def data_file - return nil unless self.data - - Tempfile.new(self.data_filename) { |f| f.write(self.data) } - end - def data_file=(file) self.data = file.read end diff --git a/app/views/admin/licenses/new.html.haml b/app/views/admin/licenses/new.html.haml index 4aa5df5451..da5c6a7cc4 100644 --- a/app/views/admin/licenses/new.html.haml +++ b/app/views/admin/licenses/new.html.haml @@ -18,4 +18,4 @@ = f.file_field :data_file, accept: ".gitlab-license,.gitlab_license,.txt" .form-actions - = f.submit 'Upload', class: 'btn btn-primary' + = f.submit 'Upload license', class: 'btn btn-primary' diff --git a/app/views/admin/licenses/show.html.haml b/app/views/admin/licenses/show.html.haml index 33263f685a..8c516fe25b 100644 --- a/app/views/admin/licenses/show.html.haml +++ b/app/views/admin/licenses/show.html.haml @@ -89,7 +89,7 @@ - if @previous_licenses.any? %h4 License History - .panel.panel-default + .panel.panel-default#license_history %table.table %thead.panel-heading %tr diff --git a/features/admin/license.feature b/features/admin/license.feature new file mode 100644 index 0000000000..d670473522 --- /dev/null +++ b/features/admin/license.feature @@ -0,0 +1,44 @@ +@admin +Feature: Admin license + Background: + Given I sign in as an admin + + Scenario: Viewing current license + Given there is a license + And I visit admin license page + Then I should see to whom the license is licensed + + Scenario: Viewing license when there is none + Given I visit admin license page + Then I should see a warning telling me there is no license + And I should be redirected to the license upload page + + Scenario: Viewing expired license + Given there is a license + And the current license is expired + And I visit admin license page + Then I should see a warning telling me the license has expired + + Scenario: Viewing license that blocks changes + Given there is a license + And the current license is expired + And the current license blocks changes + And I visit admin license page + Then I should see a warning telling me code pushes have been disabled + + Scenario: Viewing license history + Given there is a license + And there are multiple licenses + And I visit admin license page + Then I should see to whom the licenses were licensed + + Scenario: Uploading valid license + Given I visit admin upload license page + And I upload a valid license + Then I should see a notice telling me the license was uploaded + And I should see to whom the license is licensed + + Scenario: Uploading invalid license + Given I visit admin upload license page + Then I upload an invalid license + Then I should see a warning telling me it's invalid diff --git a/features/steps/admin/license.rb b/features/steps/admin/license.rb new file mode 100644 index 0000000000..f95c6dd892 --- /dev/null +++ b/features/steps/admin/license.rb @@ -0,0 +1,86 @@ +class Spinach::Features::AdminLicense < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + + step 'I should see to whom the license is licensed' do + expect(page).to have_content(license.licensee.values.first) + end + + step 'there is a license' do + create(:license) + end + + step 'I should see a warning telling me there is no license' do + expect(page).to have_content "No GitLab Enterprise Edition license has been provided yet." + end + + step 'I should be redirected to the license upload page' do + expect(current_path).to eq(new_admin_license_path) + end + + step 'the current license is expired' do + build(:license, data: build(:gitlab_license, expires_at: Date.yesterday).export).save(validate: false) + end + + step 'I should see a warning telling me the license has expired' do + expect(page).to have_content "The GitLab Enterprise Edition license expired" + end + + step 'the current license blocks changes' do + build(:license, data: build(:gitlab_license, expires_at: Date.yesterday, block_changes_at: Date.today).export).save(validate: false) + end + + step 'I should see a warning telling me code pushes have been disabled' do + expect(page).to have_content "Pushing code and creation of issues and merge requests has been disabled." + end + + step 'there are multiple licenses' do + create(:license) + create(:license) + end + + step 'I should see to whom the licenses were licensed' do + license_history = page.find("#license_history") + + License.previous.each do |license| + expect(license_history).to have_content(license.licensee.values.first) + end + end + + step 'I visit admin upload license page' do + visit new_admin_license_path + end + + step 'I upload a valid license' do + path = Rails.root.join("tmp/valid_license.gitlab-license") + + license = build(:gitlab_license) + File.write(path, license.export) + + attach_file 'license_data_file', path + click_button "Upload license" + end + + step 'I should see a notice telling me the license was uploaded' do + expect(page).to have_content "The license was successfully uploaded and is now active." + end + + step 'I upload an invalid license' do + path = Rails.root.join("tmp/invalid_license.gitlab-license") + + license = build(:gitlab_license, expires_at: Date.yesterday) + File.write(path, license.export) + + attach_file 'license_data_file', path + click_button "Upload license" + end + + step "I should see a warning telling me it's invalid" do + expect(page).to have_content "This license has already expired." + end + + def license + License.reset_current + License.current + end +end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 6f750f235f..22884ebee7 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -203,6 +203,10 @@ module SharedPaths visit admin_git_hooks_path end + step 'I visit admin license page' do + visit admin_license_path + end + # ---------------------------------------- # Generic Project # ---------------------------------------- diff --git a/spec/factories.rb b/spec/factories.rb index a73ea193cf..8cdd0d2432 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -202,4 +202,17 @@ FactoryGirl.define do provider 'ldapmain' extern_uid 'my-ldap-id' end + + factory :gitlab_license, class: "Gitlab::License" do + issued_at { Date.today } + licensee do + { "Name" => Faker::Name.name } + end + notify_users_at { |l| l.expires_at } + notify_admins_at { |l| l.expires_at } + end + + factory :license do + data { build(:gitlab_license).export } + end end diff --git a/spec/models/license_spec.rb b/spec/models/license_spec.rb index 04150481ab..3be18793a1 100644 --- a/spec/models/license_spec.rb +++ b/spec/models/license_spec.rb @@ -1,8 +1,8 @@ require "spec_helper" describe License do - let(:gl_license) { Gitlab::License.new(issued_at: Date.today, licensee: { "Name" => "GitLab Test Env" }) } - let(:license) { License.new(data: gl_license.export) } + let(:gl_license) { build(:gitlab_license) } + let(:license) { build(:license, data: gl_license.export) } describe "Validation" do describe "Valid license" do diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 3de413f6b0..63771cfdd4 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -53,9 +53,7 @@ module TestEnv def setup_license Gitlab::License.encryption_key = OpenSSL::PKey::RSA.generate(2048) - gl_license = Gitlab::License.new(issued_at: Date.today, licensee: { "Name" => "GitLab Test Env" }) - - License.create(data: gl_license.export) + FactoryGirl.create(:license) end # Clean /tmp/tests From c9d1a9d096b283874f67bb76063e44eb1d95ecb9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 15:20:28 +0200 Subject: [PATCH 14/17] Move license setup to spec/support. --- spec/support/license.rb | 7 +++++++ spec/support/test_env.rb | 8 -------- 2 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 spec/support/license.rb diff --git a/spec/support/license.rb b/spec/support/license.rb new file mode 100644 index 0000000000..1bc0a64339 --- /dev/null +++ b/spec/support/license.rb @@ -0,0 +1,7 @@ +RSpec.configure do |config| + config.before(:all) do + Gitlab::License.encryption_key = OpenSSL::PKey::RSA.generate(2048) + + FactoryGirl.create(:license) + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 63771cfdd4..6d4a806791 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -23,8 +23,6 @@ module TestEnv # See gitlab.yml.example test section for paths # def init(opts = {}) - setup_license - # Disable mailer for spinach tests disable_mailer if opts[:mailer] == false @@ -50,12 +48,6 @@ module TestEnv allow_any_instance_of(NotificationService).to receive(:mailer).and_call_original end - def setup_license - Gitlab::License.encryption_key = OpenSSL::PKey::RSA.generate(2048) - - FactoryGirl.create(:license) - end - # Clean /tmp/tests # # Keeps gitlab-shell and gitlab-test From fdcf8732e3174767ee7079b4b32c94eefc752379 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 16:14:58 +0200 Subject: [PATCH 15/17] Use spec/support/license in feature specs. --- features/support/env.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/support/env.rb b/features/support/env.rb index f34302721e..930d43364a 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -16,7 +16,7 @@ require 'sidekiq/testing/inline' require_relative 'capybara' require_relative 'db_cleaner' -%w(select2_helper test_env repo_helpers).each do |f| +%w(select2_helper test_env repo_helpers license).each do |f| require Rails.root.join('spec', 'support', f) end From 359c039b6ee3652201d1204a3003411965fe9918 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 5 May 2015 16:27:21 +0200 Subject: [PATCH 16/17] Fix Git access check. --- lib/gitlab/git_access.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 4832c73186..f9c8225cd9 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -102,9 +102,8 @@ module Gitlab end def user_push_access_check(changes) - if ::License.block_changes? - message = ::LicenseHelper.license_message(signed_in: true, is_admin: (user && user.is_admin?)) - return build_status_object(false, message) + unless user && user_allowed? + return build_status_object(false, "You don't have access") end if changes.blank? @@ -114,6 +113,11 @@ module Gitlab unless project.repository.exists? return build_status_object(false, "Repository does not exist") end + + if ::License.block_changes? + message = ::LicenseHelper.license_message(signed_in: true, is_admin: (user && user.is_admin?)) + return build_status_object(false, message) + end changes = changes.lines if changes.kind_of?(String) From f709f76877a194e96176c4344d5485f6454cd3e0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 May 2015 20:01:45 +0200 Subject: [PATCH 17/17] Make sure test license is setup in all tests. --- features/support/env.rb | 2 ++ spec/spec_helper.rb | 4 ++++ spec/support/license.rb | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/features/support/env.rb b/features/support/env.rb index 930d43364a..1e041e4483 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -26,7 +26,9 @@ WebMock.allow_net_connect! Spinach.hooks.before_run do include RSpec::Mocks::ExampleMethods + TestEnv.init(mailer: false) + TestLicense.init include FactoryGirl::Syntax::Methods end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8fe51cf4ad..2030513bb5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -37,6 +37,10 @@ RSpec.configure do |config| config.before(:suite) do TestEnv.init end + + config.before(:all) do + TestLicense.init + end end ActiveRecord::Migration.maintain_test_schema! diff --git a/spec/support/license.rb b/spec/support/license.rb index 1bc0a64339..361e219e14 100644 --- a/spec/support/license.rb +++ b/spec/support/license.rb @@ -1,5 +1,5 @@ -RSpec.configure do |config| - config.before(:all) do +class TestLicense + def self.init Gitlab::License.encryption_key = OpenSSL::PKey::RSA.generate(2048) FactoryGirl.create(:license)