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