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 e85bf4517b..1e5d174a19 100644
--- a/Gemfile
+++ b/Gemfile
@@ -203,6 +203,8 @@ gem 'request_store'
gem "virtus"
gem 'addressable'
+gem "gitlab-license", "~> 0.0.2"
+
group :development do
gem 'brakeman', require: false
gem "annotate", "~> 2.6.0.beta2"
diff --git a/Gemfile.lock b/Gemfile.lock
index 3ceb8b47a2..c464328964 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.2)
gitlab-linguist (3.0.1)
charlock_holmes (~> 0.6.6)
escape_utils (~> 0.2.4)
@@ -708,6 +709,7 @@ DEPENDENCIES
github-markup
gitlab-flowdock-git-hook (~> 0.4.2)
gitlab-grack (~> 2.0.2)
+ gitlab-license (~> 0.0.2)
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..f908a5e5bf
--- /dev/null
+++ b/app/controllers/admin/licenses_controller.rb
@@ -0,0 +1,69 @@
+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.previous
+ end
+
+ def download
+ send_data @license.data, filename: @license.data_filename, disposition: 'attachment'
+ end
+
+ def new
+ @license = License.new
+ 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
+ if @license.save
+ flash[:notice] = "The license was successfully uploaded and is now active. You can see the details below."
+ end
+ end
+ end
+
+ def destroy
+ license.destroy
+
+ 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
+
+ def license
+ @license ||= begin
+ License.reset_current
+ License.current
+ end
+ end
+
+ def require_license
+ return if license
+
+ flash.keep
+ 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..a9ed65e5d5
--- /dev/null
+++ b/app/helpers/license_helper.rb
@@ -0,0 +1,70 @@
+module LicenseHelper
+ def license_message(signed_in: signed_in?, is_admin: (current_user && current_user.is_admin?))
+ @license_message ||=
+ 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 = []
+
+ 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"
+
+ message <<
+ if license.block_changes?
+ "has been disabled."
+ else
+ "will be disabled on #{license.block_changes_at}."
+ end
+ 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
+
+ extend self
+end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 85a15596f8..0dbadff6db 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -5,18 +5,34 @@ 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
+
+ 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
diff --git a/app/models/license.rb b/app/models/license.rb
new file mode 100644
index 0000000000..34ed22aeaa
--- /dev/null
+++ b/app/models/license.rb
@@ -0,0 +1,112 @@
+class License < ActiveRecord::Base
+ validate :valid_license
+ validate :active_user_count, unless: :persisted?
+ validate :not_expired, unless: :persisted?
+
+ before_validation :reset_license, if: :data_changed?
+
+ after_create :reset_current
+ after_destroy :reset_current
+
+ scope :previous, -> { order(created_at: :desc).offset(1) }
+
+ 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
+ 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
+
+ def data_file=(file)
+ self.data = file.read
+ end
+
+ def license
+ return nil unless self.data
+
+ @license ||=
+ begin
+ Gitlab::License.import(self.data)
+ rescue Gitlab::License::ImportError
+ nil
+ end
+ 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
+ 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
+ end
+
+ private
+
+ def reset_current
+ self.class.reset_current
+ end
+
+ def reset_license
+ @license = nil
+ end
+
+ def valid_license
+ return if license?
+
+ 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
+ return unless self.license? && self.restricted?(: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}, "
+ 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
diff --git a/app/views/admin/licenses/new.html.haml b/app/views/admin/licenses/new.html.haml
new file mode 100644
index 0000000000..da5c6a7cc4
--- /dev/null
+++ b/app/views/admin/licenses/new.html.haml
@@ -0,0 +1,21 @@
+- 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..
+
+%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,.txt"
+
+ .form-actions
+ = 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
new file mode 100644
index 0000000000..8c516fe25b
--- /dev/null
+++ b/app/views/admin/licenses/show.html.haml
@@ -0,0 +1,121 @@
+- page_title "License"
+%h3.page-title
+ Your License
+ = link_to 'Upload New 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
+ - if @license.expired?
+ Expired:
+ - else
+ 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 file.
+ %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"
+
+
+ .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: "Are you sure you want to remove the license?" }, method: :delete, class: "btn btn-remove"
+
+- if @previous_licenses.any?
+ %h4 License History
+
+ .panel.panel-default#license_history
+ %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..e01663e059 100644
--- a/app/views/layouts/_broadcast.html.haml
+++ b/app/views/layouts/_broadcast.html.haml
@@ -1,4 +1,9 @@
- if broadcast_message.present?
.broadcast-message{ style: broadcast_styling(broadcast_message) }
- %i.fa.fa-bullhorn
+ = icon('bullhorn')
= broadcast_message.message
+
+- if license_message.present?
+ .broadcast-message
+ = icon('bullhorn')
+ = license_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..4f4199ae56
--- /dev/null
+++ b/config/initializers/license.rb
@@ -0,0 +1,15 @@
+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?
+ warn "WARNING: #{message}"
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index c65139aed7..0ee8cb3b6e 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 e0734e445f..71b5522a2e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -211,6 +211,12 @@ ActiveRecord::Schema.define(version: 20150502064022) 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
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/features/support/env.rb b/features/support/env.rb
index f34302721e..1e041e4483 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
@@ -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/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 831434577b..e111e5d42f 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -113,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)
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/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 0a342e132d..cddb0b3798 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 "git_hook_check" do
diff --git a/spec/models/license_spec.rb b/spec/models/license_spec.rb
new file mode 100644
index 0000000000..3be18793a1
--- /dev/null
+++ b/spec/models/license_spec.rb
@@ -0,0 +1,174 @@
+require "spec_helper"
+
+describe License do
+ let(:gl_license) { build(:gitlab_license) }
+ let(:license) { build(:license, 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
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
new file mode 100644
index 0000000000..361e219e14
--- /dev/null
+++ b/spec/support/license.rb
@@ -0,0 +1,7 @@
+class TestLicense
+ def self.init
+ Gitlab::License.encryption_key = OpenSSL::PKey::RSA.generate(2048)
+
+ FactoryGirl.create(:license)
+ end
+end