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?