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?