Enterprise Edition support tokens

pull/5154/head
Wieland Lindenthal 8 years ago committed by Oliver Günther
parent 0e1f4ed4eb
commit 49f12b3e21
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 24
      .openproject-token.pub
  2. 4
      Gemfile
  3. 9
      Gemfile.lock
  4. BIN
      app/assets/images/enterprise_edition.png
  5. 13
      app/assets/stylesheets/content/_enterprise.sass
  6. 4
      app/assets/stylesheets/content/_ng_dialog.sass
  7. 25
      app/assets/stylesheets/content/_widget_box.sass
  8. 1
      app/assets/stylesheets/default.css.sass
  9. 4
      app/assets/stylesheets/specific/onboarding.sass
  10. 67
      app/controllers/enterprises_controller.rb
  11. 107
      app/models/enterprise_token.rb
  12. 2
      app/models/queries/relations/relation_query.rb
  13. 64
      app/services/authorization/enterprise_service.rb
  14. 38
      app/views/enterprises/_current.html.erb
  15. 21
      app/views/enterprises/_form.html.erb
  16. 48
      app/views/enterprises/show.html.erb
  17. 30
      app/views/homescreen/blocks/_upsale.html.erb
  18. 13
      app/views/timelines/show.html.erb
  19. 7
      config/initializers/ee_token.rb
  20. 4
      config/initializers/homescreen.rb
  21. 6
      config/initializers/menus.rb
  22. 30
      config/locales/en.yml
  23. 5
      config/locales/js-en.yml
  24. 1
      config/routes.rb
  25. 9
      db/migrate/20161102160032_create_enterprise_token.rb
  26. 99
      frontend/app/components/modals/confirm-form-submit/confirm-form-submit.directive.ts
  27. 18
      frontend/app/components/modals/confirm-form-submit/confirm-form-submit.modal.html
  28. 4
      lib/open_project/configuration.rb
  29. 4
      lib/open_project/static/links.rb
  30. 7
      lib/redmine/menu_manager/top_menu/help_menu.rb
  31. 4
      lib/tasks/copyright.rake
  32. 154
      spec/controllers/enterprises_controller_spec.rb
  33. 118
      spec/features/admin/enterprise_spec.rb
  34. 92
      spec/models/enterprise_token_spec.rb
  35. 87
      spec/services/authorization/enterprise_service_spec.rb

@ -0,0 +1,24 @@
-----BEGIN PUBLIC KEY-----
MIIEFjANBgkqhkiG9w0BAQEFAAOCBAMAMIID/gKCA/UAquIZchoog2ffcr9J2KSl
mlum6sN3smTVNsp9JGd1q4fr/kUFGch6q1cFEX3x5BGDXx7wPPI4ppKzeQHaxWmx
wxqs3eevcTFUEF9A2MPX7p5Ia0TbH4d7e7D9YMWvDXoQLggrxMFdUHY3ppUnBPgB
+EJG1Pv0FlBAdxYX0em7kLwhcp9PBP/zXso/qkkKK/pncyKizOLC3zv3E0ixcQ7o
Nq0aolTJFMHcqEquKaQN1jicDdzU6ks+YKh7kByZvVChe/InlroVXKrUa34hAZDM
acEkURJma3meN0IyPFA7fHRe1AhiNYF2MatNKysPrbOffYLOjamlaqmHTeJAec6e
vMHd+LlIz4xXivR0lY2wDawqp0waSLJaW8lZetOf0iwbqQkzZhz4sWDZopyGiqAU
v9/zS4OjUBr7JQbVcV3LIkzGWwNysSvTMrlvzCesYVsCwpLjP6gFxdclYJuTwEeL
o+T+AgoNyuj6ixhwHTJxIVhuBpebX44/YTYyUGMgItekDCH2Dxvtv2DaCL7YIqNG
ibvCyzCyLak7Tz97CMvCUf1EIRVfolbGpphi2Zzpoeqhfheh+0LQ3gmMBJpuLnJU
VXEtrOPunTkOrdUqL5rD/+mfd8yufsJ16Uk5j7gNUcIJsCcGWZ3Nhfidi3tvmJPF
H8HgNZ6W6smj9k7+TdZbRsH1LZp1LL/stLch3ffFHHcJye7d2t75uKiOoz1/1JMY
fl/wfaEaKvTGBKr/NFKcVDSBXHgx6VMA3oWV2AyLnTaY98XVPY1zzbyNKlNQkSaD
p/VCl334+YMwR9/5aJYJg58lljw6aBu+cozNkydKjCmqEpZNdR1tFusAY7jd/M2G
yXPBTdUJD6GrLx0Hot/wImJ30gOMgpeoetNUfM9/FimySLy65DRzCtLm1hthAlWM
Is5vFwMmFFSb3ozTsnkj9W16jHk9HdodKJfezzcPqu3TW2EMMNbbtXa3OPaHkht2
huYJGnGu0hNhAj+x+KCxkpLveS0Ajw322qmtAqnwJLvfwShnz//cptNX5kXtrNGy
+o6I4jHibIATfaMKMt8gHmCZ381zAwrAOU7c0FQna+IkU8dgZDx7T+Xo1Q/GXuaO
1b2aT6geT60A3VgF+OBnoHe5Ext7vfNL9v0wcN5NLR5KgjexwEhcBcA2FauCTrVt
FUNgir+5XALd/wBlvxkvPKTJnQld/aK7xF0ui3c3/ryPX5cKzpfm9APK/hOFzkJ7
ieARHqrQGqOYdClIUJIH0b/92dFq49Eqn1cKpztVzsU9xzdI/4w5JUSw/kbguVf7
Yd0Rdc9KF/9WMwjzrWSti4meNBUO6/18cAognx0Pf5qsrSzOewIDAQAB
-----END PUBLIC KEY-----

@ -153,6 +153,8 @@ gem 'nokogiri', '~> 1.6.8'
gem 'fog-aws' gem 'fog-aws'
gem 'carrierwave', git: 'https://github.com/carrierwaveuploader/carrierwave', branch: 'master' gem 'carrierwave', git: 'https://github.com/carrierwaveuploader/carrierwave', branch: 'master'
gem 'openproject-token', '~> 1.0.0'
group :test do group :test do
gem 'rack-test', '~> 0.6.3' gem 'rack-test', '~> 0.6.3'
gem 'shoulda-context', '~> 1.2' gem 'shoulda-context', '~> 1.2'
@ -261,7 +263,7 @@ platforms :jruby do
end end
group :opf_plugins do group :opf_plugins do
gem 'openproject-translations', git: 'https://github.com/opf/openproject-translations.git', branch: 'release/6.1' gem 'openproject-translations', git: 'https://github.com/opf/openproject-translations.git', branch: 'dev'
end end
# TODO: Make this group :optional when bundler v10.x # TODO: Make this group :optional when bundler v10.x

@ -60,10 +60,10 @@ GIT
GIT GIT
remote: https://github.com/opf/openproject-translations.git remote: https://github.com/opf/openproject-translations.git
revision: 1e8c4e52119f0ea690bf7d015798ae65b1af0f7a revision: bbc216af7f0133fd191fbe7ea121a2c962a8491f
branch: release/6.1 branch: dev
specs: specs:
openproject-translations (6.1.4) openproject-translations (6.2.0)
crowdin-api (~> 0.4.1) crowdin-api (~> 0.4.1)
mixlib-shellout (~> 2.1.0) mixlib-shellout (~> 2.1.0)
rails (~> 5.0.0) rails (~> 5.0.0)
@ -361,6 +361,8 @@ GEM
nokogiri (1.6.8.1) nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
oj (2.17.4) oj (2.17.4)
openproject-token (1.0.0)
activemodel (~> 5.0)
parallel (1.9.0) parallel (1.9.0)
parallel_tests (2.4.1) parallel_tests (2.4.1)
parallel parallel
@ -648,6 +650,7 @@ DEPENDENCIES
nokogiri (~> 1.6.8) nokogiri (~> 1.6.8)
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth! omniauth!
openproject-token (~> 1.0.0)
openproject-translations! openproject-translations!
parallel_tests (~> 2.4.1) parallel_tests (~> 2.4.1)
passenger passenger

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

@ -0,0 +1,13 @@
.upsale-notification
max-width: 560px
margin-bottom: 20px
padding-bottom: 20px
.widget-box--teaser-image
width: 30%
float: right
margin-top: -20px
.token-form textarea
font-family: "Lucida Console", Monaco, monospace
max-width: 560px

@ -46,6 +46,10 @@
// Required for close icon // Required for close icon
position: relative position: relative
.ngdialog-body
margin-top: 2rem
min-height: 50px
.ngdialog-close .ngdialog-close
cursor: pointer cursor: pointer
position: absolute position: absolute

@ -49,7 +49,8 @@ $widget-box--enumeration-width: 20px
flex-direction: column flex-direction: column
.widget-box--enumeration, .widget-box--enumeration,
.widget-box--arrow-links .widget-box--arrow-links,
.widget-box--feature-list
flex-grow: 2 flex-grow: 2
.icon-context:before .icon-context:before
@ -125,6 +126,28 @@ $widget-box--enumeration-width: 20px
//necessary for correct alignment even with long texts //necessary for correct alignment even with long texts
width: calc(100% - #{$widget-box--enumeration-width}) width: calc(100% - #{$widget-box--enumeration-width})
.widget-box--feature-list
list-style: none
margin: 0.5rem 0 1rem 0
&:last-child
margin-bottom: 0
li:before
@include icon-common
@extend .icon-notice
@extend .icon-notice:before
display: inline-block
font-size: 0.6rem
color: $alternative-color
width: $widget-box--enumeration-width
.widget-box--teaser-image
height: 100px
margin-left: auto
margin-right: auto
@include breakpoint(680px down) @include breakpoint(680px down)
.widget-boxes .widget-boxes
&.-flex &.-flex

@ -60,6 +60,7 @@
@import content/links @import content/links
@import content/action_menu_main @import content/action_menu_main
@import content/legacy_actions @import content/legacy_actions
@import content/enterprise
@import content/my_page @import content/my_page
@import content/project_overview @import content/project_overview
@import content/news @import content/news

@ -27,8 +27,10 @@
//++ //++
// Align ngDialog's close button // Align ngDialog's close button
.onboarding-modal .ngdialog-close .onboarding-modal .ngdialog-close,
.ngdialog-theme-openproject .ngdialog-close
line-height: $header-height line-height: $header-height
color: $header-item-font-color
.onboarding--top-menu .onboarding--top-menu
display: flex display: flex

@ -0,0 +1,67 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public token version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public token
# as published by the Free Software Foundation; either version 2
# of the token, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public token for more details.
#
# You should have received a copy of the GNU General Public token
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class EnterprisesController < ApplicationController
layout 'admin'
menu_item :enterprise
before_action :require_admin
def show
@current_token = EnterpriseToken.current
@token = @current_token || EnterpriseToken.new
end
def create
@token = EnterpriseToken.current || EnterpriseToken.new
@token.encoded_token = params[:enterprise_token][:encoded_token]
if @token.save
flash[:notice] = t(:notice_successful_update)
redirect_to action: :show
else
render action: :show
end
end
def destroy
token = EnterpriseToken.current
if token
token.destroy
flash[:notice] = t(:notice_successful_delete)
redirect_to action: :show
else
render_404
end
end
private
def default_breadcrumb
t(:label_enterprise)
end
end

@ -0,0 +1,107 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class EnterpriseToken < ActiveRecord::Base
class << self
def current
RequestStore.fetch(cache_key) do
set_current_token
end
end
def cache_key
RequestStore.fetch(:current_ee_token_updated_at) { EnterpriseToken.maximum(:updated_at) }
most_recent_update = (RequestStore[:current_ee_token_updated_at] || Time.now.utc).to_i
"/openproject/ee/#{most_recent_update}"
end
def clear_cache(key = cache_key)
Rails.cache.delete(key)
RequestStore.delete key
RequestStore.delete :current_ee_token_updated_at
end
def allows_to?(action)
Authorization::EnterpriseService.new(current).call(action).result
end
def show_banners
!current || current.expired?
end
def set_current_token
token = EnterpriseToken.order('created_at DESC').first
if token && token.token_object
token
end
end
end
validates_presence_of :encoded_token
validate :valid_token_object
before_save :unset_current_token
before_destroy :unset_current_token
delegate :will_expire?,
:expired?,
:subscriber,
:mail,
:issued_at,
:starts_at,
:expires_at,
:restrictions,
to: :token_object
def token_object
@token_object = load_token unless defined?(@token_object)
@token_object
end
def allows_to?(action)
Authorization::EnterpriseService.new(self).call(action).result
end
def unset_current_token
# Clear current cache
self.class.clear_cache
end
private
def load_token
OpenProject::Token.import(encoded_token)
rescue OpenProject::Token::ImportError => error
Rails.logger.error "Failed to load EE token: #{error}"
nil
end
def valid_token_object
errors.add(:encoded_token, :unreadable) unless token_object
end
end

@ -33,7 +33,7 @@ module Queries
Relation Relation
end end
def self.default_scope def default_scope
Relation.all Relation.all
end end
end end

@ -0,0 +1,64 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class Authorization::EnterpriseService
attr_accessor :token
def initialize(token)
self.token = token
end
# Return a true ServiceResult if the token contains this particular action.
def call(action)
allowed =
if token.nil? || token.expired?
false
else
process(action)
end
result(allowed)
end
private
def process(action)
case action
when :define_custom_style
true # Every non-expired token
else
false
end
end
def result(bool)
ServiceResult.new(success: bool, result: bool)
end
end

@ -0,0 +1,38 @@
<div class="enterprise--active-token">
<div class="attributes-group">
<div class="attributes-key-value">
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name(:subscriber) %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= @current_token.subscriber %></span>
</div>
</div>
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name(:mail) %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= @current_token.mail %></span>
</div>
</div>
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name(:starts_at) %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= format_date @current_token.starts_at %></span>
</div>
</div>
<% if @current_token.will_expire? %>
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name(:expires_at) %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= format_date @current_token.expires_at %></span>
</div>
</div>
<% end %>
</div>
</div>
</div>
<%= form_tag({ action: :destroy },
method: :delete,
class: 'confirm-form-submit') do %>
<%= styled_button_tag l(:button_delete), type: 'submit', class: '-with-icon icon-delete' %>
<% end %>

@ -0,0 +1,21 @@
<% if @current_token.present? %>
<collapsible-section section-title="<%=h t('admin.enterprise.replace_token') %>">
<% else %>
<collapsible-section initially-expanded="true"
section-title="<%=h t('admin.enterprise.add_token') %>">
<% end %>
<%= labelled_tabular_form_for @token,
url: { action: :create },
html: { class: 'token-form' },
method: :post do |f|%>
<div class="form--space">
<%= f.text_area :encoded_token, :cols => 60, :rows => 15, placeholder: t('admin.enterprise.paste'), autocomplete: "off", autocorrect: "off", autocapitalize: "off", spellcheck: false %>
</div>
<div class="form--space">
<%= styled_button_tag t(:button_create), id: 'token-submit-button', class: '-highlight -with-icon icon-checkmark' %>
<%= link_to t(:button_cancel), { action: :show }, class: 'button' %>
</div>
<% end %>
</collapsible-section>

@ -0,0 +1,48 @@
<% html_title t(:label_administration), t(:label_enterprise_edition) %>
<%= toolbar title: t(:label_enterprise_edition) do %>
<li class="toolbar-item">
<%= link_to( "#{OpenProject::Static::Links.links[:upsale][:href]}?utm_source=ce-token-admin",
{ class: 'button -alt-highlight',
title: t('admin.enterprise.order')}) do %>
<i class="button--icon icon-add"></i>
<span class="button--text"><%= t('admin.enterprise.order') %></span>
<% end %>
</li>
<% end %>
<%= error_messages_for 'token' %>
<% if @current_token.present? %>
<%= render partial: "current" %>
<% else %>
<div class="notification-box upsale-notification">
<div class="notification-box--content">
<h3><%= t('admin.enterprise.upgrade_to_ee') %></h3>
<%= image_tag "enterprise_edition.png", class: "widget-box--teaser-image" %>
<p><%= t('homescreen.blocks.upsale.description') %></p>
<ul class="">
<li>
<%= t('homescreen.blocks.upsale.additional_features') %>
</li>
<li>
<%= t('homescreen.blocks.upsale.professional_support') %>
</li>
</ul>
<p>
<b><%= t('homescreen.blocks.upsale.become_hero') %></b> <%= t('homescreen.blocks.upsale.you_contribute') %>
</p>
<%= link_to( "#{OpenProject::Static::Links.links[:upsale][:href]}?utm_source=ce-enterprise-admin",
{ class: 'button -alt-highlight',
aria: {label: t('admin.enterprise.order')},
title: t('admin.enterprise.order')}) do %>
<i class="button--icon icon-add"></i>
<span class="button--text"><%= t('admin.enterprise.order') %></span>
<% end %>
</div>
</div>
<% end %>
<%= render partial: "form" %>

@ -0,0 +1,30 @@
<h3 class="widget-box--header">
<span class="icon-context icon-cart"></span>
<span class="widget-box--header-title"><%= t('homescreen.blocks.upsale.title') %></span>
</h3>
<%= image_tag "enterprise_edition.png", class: "widget-box--teaser-image" %>
<p><%= t('homescreen.blocks.upsale.description') %></p>
<ul class="widget-box--feature-list">
<li>
<%= t('homescreen.blocks.upsale.additional_features') %>
</li>
<li>
<%= t('homescreen.blocks.upsale.professional_support') %>
</li>
</ul>
<p>
<b><%= t('homescreen.blocks.upsale.become_hero') %></b> <%= t('homescreen.blocks.upsale.you_contribute') %>
</p>
<div class="widget-box--blocks--buttons">
<%= link_to "#{OpenProject::Static::Links.links[:upsale][:href]}?utm_source=ce-homescreen",
{ class: 'button -alt-highlight',
aria: {label: t('homescreen.blocks.upsale.more_info')},
title: t('homescreen.blocks.upsale.more_info')} do %>
<i class="button--icon icon-info2"></i>
<span class="button--text"><%= t('homescreen.blocks.upsale.more_info') %></span>
<% end %>
</div>

@ -27,13 +27,14 @@ See doc/COPYRIGHT.rdoc for more details.
++#%> ++#%>
<% html_title "#{l('timelines.project_menu.timelines')}: #{h @timeline.name}" %> <% html_title "#{t('timelines.project_menu.timelines')}: #{h @timeline.name}" %>
<%= toolbar title: @timeline.name do %> <%= toolbar title: @timeline.name do %>
<% if timeline_action_authorized?(:new) %> <% if timeline_action_authorized?(:new) %>
<li class="toolbar-item"> <li class="toolbar-item">
<%= new_timeline_link @project do %> <%= new_timeline_link @project do %>
<i class="button--icon icon-add"></i> <i class="button--icon icon-add"></i>
<span class="button--text"><%= l('timelines.timeline') %></span> <span class="button--text"><%= t('timelines.timeline') %></span>
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
@ -42,7 +43,7 @@ See doc/COPYRIGHT.rdoc for more details.
<li class="toolbar-item"> <li class="toolbar-item">
<%= edit_timeline_link @project, @timeline do %> <%= edit_timeline_link @project, @timeline do %>
<i class="button--icon icon-edit"></i> <i class="button--icon icon-edit"></i>
<span class="button--text"><%= l(:button_edit) %></span> <span class="button--text"><%= t(:button_edit) %></span>
<span class='hidden-for-sighted'><%= h(@timeline.name) %></span> <span class='hidden-for-sighted'><%= h(@timeline.name) %></span>
<% end %> <% end %>
</li> </li>
@ -52,7 +53,7 @@ See doc/COPYRIGHT.rdoc for more details.
<li class="toolbar-item"> <li class="toolbar-item">
<%= destroy_timeline_link @project, @timeline do %> <%= destroy_timeline_link @project, @timeline do %>
<i class="button--icon icon-delete"></i> <i class="button--icon icon-delete"></i>
<span class="button--text"><%= l(:button_delete) %></span> <span class="button--text"><%= t(:button_delete) %></span>
<span class='hidden-for-sighted'><%= h(@timeline.name) %></span> <span class='hidden-for-sighted'><%= h(@timeline.name) %></span>
<% end %> <% end %>
</li> </li>
@ -61,7 +62,7 @@ See doc/COPYRIGHT.rdoc for more details.
<li class="toolbar-item"> <li class="toolbar-item">
<%= link_to url_for(controller: '/reportings', action: 'index', project_id: @project.id), class: 'button' do %> <%= link_to url_for(controller: '/reportings', action: 'index', project_id: @project.id), class: 'button' do %>
<i class="button--icon icon-status-reporting"></i> <i class="button--icon icon-status-reporting"></i>
<span class="button--text"><%= l('timelines.project_menu.reportings') %></span> <span class="button--text"><%= t('timelines.project_menu.reportings') %></span>
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
@ -69,7 +70,7 @@ See doc/COPYRIGHT.rdoc for more details.
<div id="timeline"> <div id="timeline">
<%= form_tag '', {id: "specialForm", 'ng-controller' => 'TimelineSelectionController'} do %> <%= form_tag '', {id: "specialForm", 'ng-controller' => 'TimelineSelectionController'} do %>
<div class="form--field"> <div class="form--field">
<%= label_tag 'timeline_select', l("timelines.timeline"), class: "form--label -transparent" %> <%= label_tag 'timeline_select', t("timelines.timeline"), class: "form--label -transparent" %>
<div class="form--field-container"> <div class="form--field-container">
<div class="form--select-container"> <div class="form--select-container">
<select <select

@ -0,0 +1,7 @@
begin
data = File.read(Rails.root.join(".openproject-token.pub"))
key = OpenSSL::PKey::RSA.new(data)
OpenProject::Token.key = key
rescue
warn "WARNING: Missing .openproject-token.pub key"
end

@ -43,7 +43,9 @@ OpenProject::Static::Homescreen.manage :blocks do |blocks|
if: Proc.new { !@news.empty? } }, if: Proc.new { !@news.empty? } },
{ partial: 'community' }, { partial: 'community' },
{ partial: 'administration', { partial: 'administration',
if: Proc.new { User.current.admin? } } if: Proc.new { User.current.admin? } },
{ partial: 'upsale',
if: Proc.new { EnterpriseToken.show_banners } }
) )
end end

@ -198,6 +198,12 @@ Redmine::MenuManager.map :admin_menu do |menu|
{ controller: '/project_types', action: 'index' }, { controller: '/project_types', action: 'index' },
caption: :'timelines.admin_menu.project_types', caption: :'timelines.admin_menu.project_types',
html: { class: 'icon2 icon-project-types' } html: { class: 'icon2 icon-project-types' }
menu.push :enterprise,
{ controller: '/enterprises', action: 'show' },
caption: :label_enterprise_edition,
html: { class: 'enterprise icon2 icon-key' },
if: proc { OpenProject::Configuration.ee_manager_visible? }
end end
Redmine::MenuManager.map :project_menu do |menu| Redmine::MenuManager.map :project_menu do |menu|

@ -39,6 +39,12 @@ en:
no_results_content_text: Create a new project no_results_content_text: Create a new project
plugins: plugins:
no_results_title_text: There are currently no plugins available. no_results_title_text: There are currently no plugins available.
enterprise:
upgrade_to_ee: "Upgrade to Enterprise Edition"
add_token: "Upload an Enterprise Edition support token"
replace_token: "Replace your current support token"
order: "Order Enterprise Edition"
paste: "Paste your Enterprise Edition support token"
announcements: announcements:
show_until: Show until show_until: Show until
@ -266,6 +272,11 @@ en:
visible: "Visible" visible: "Visible"
custom_value: custom_value:
value: "Value" value: "Value"
enterprise_token:
starts_at: "Valid since"
expires_at: "Expires at"
subscriber: "Subscriber"
encoded_token: "Enterprise support token"
relation: relation:
delay: "Delay" delay: "Delay"
from: "Work Package" from: "Work Package"
@ -404,6 +415,8 @@ en:
before_or_equal_to: "must be before or equal to %{date}." before_or_equal_to: "must be before or equal to %{date}."
could_not_be_copied: "could not be (fully) copied." could_not_be_copied: "could not be (fully) copied."
models: models:
enterprise_token:
unreadable: "can't be read. Are you sure it is a support token?"
project: project:
attributes: attributes:
types: types:
@ -790,6 +803,7 @@ en:
error_can_not_reopen_work_package_on_closed_version: "A work package assigned to a closed version cannot be reopened" error_can_not_reopen_work_package_on_closed_version: "A work package assigned to a closed version cannot be reopened"
error_check_user_and_role: "Please choose a user and a role." error_check_user_and_role: "Please choose a user and a role."
error_cookie_missing: 'The OpenProject cookie is missing. Please ensure that cookies are enabled, as this application will not properly function without.' error_cookie_missing: 'The OpenProject cookie is missing. Please ensure that cookies are enabled, as this application will not properly function without.'
error_failed_to_delete_entry: 'Failed to delete this entry.'
error_pdf_export_too_many_columns: "Too many columns selected for the PDF export. Please reduce the number of columns." error_pdf_export_too_many_columns: "Too many columns selected for the PDF export. Please reduce the number of columns."
error_token_authenticity: 'Unable to verify Cross-Site Request Forgery token.' error_token_authenticity: 'Unable to verify Cross-Site Request Forgery token.'
error_work_package_done_ratios_not_updated: "Work package done ratios not updated." error_work_package_done_ratios_not_updated: "Work package done ratios not updated."
@ -847,7 +861,16 @@ en:
users: "Newest registered users in this instance." users: "Newest registered users in this instance."
blocks: blocks:
community: "OpenProject community" community: "OpenProject community"
upsale:
become_hero: "Become a hero!"
title: "Upgrade to Enterprise Edition"
description: "Boost your productivity with the Enterprise Edition"
more_info: "More information"
additional_features: "Additional powerful premium features"
professional_support: "Professional support from the OpenProject experts"
you_contribute: "Developers need to pay their bills, too. With Enterprise Edition you substantially contribute to this Open-Source community effort."
links: links:
upgrade_enterprise_edition: "Upgrade to Enterprise Edition"
user_guides: "User guides" user_guides: "User guides"
faq: "FAQ" faq: "FAQ"
glossary: "Glossary" glossary: "Glossary"
@ -856,6 +879,7 @@ en:
blog: "OpenProject blog" blog: "OpenProject blog"
boards: "Community forum" boards: "Community forum"
instructions_after_registration: "You can sign in as soon as your account has been activated by clicking %{signin}." instructions_after_registration: "You can sign in as soon as your account has been activated by clicking %{signin}."
instructions_after_logout: "You can sign in again by clicking %{signin}." instructions_after_logout: "You can sign in again by clicking %{signin}."
instructions_after_error: "You can try to sign in again by clicking %{signin}. If the error persists, ask your admin for help." instructions_after_error: "You can try to sign in again by clicking %{signin}. If the error persists, ask your admin for help."
@ -871,6 +895,7 @@ en:
expiration: "Expires" expiration: "Expires"
indefinite_expiration: "Never" indefinite_expiration: "Never"
label_accessibility: "Accessibility"
label_account: "Account" label_account: "Account"
label_active: "Active" label_active: "Active"
label_activity: "Activity" label_activity: "Activity"
@ -973,6 +998,7 @@ en:
label_custom_field_plural: "Custom fields" label_custom_field_plural: "Custom fields"
label_custom_field_default_type: "Empty type" label_custom_field_default_type: "Empty type"
label_date: "Date" label_date: "Date"
label_date_and_time: "Date and time"
label_date_from: "From" label_date_from: "From"
label_date_from_to: "From %{start} to %{end}" label_date_from_to: "From %{start} to %{end}"
label_date_range: "Date range" label_date_range: "Date range"
@ -1008,6 +1034,8 @@ en:
label_enumeration_new: "New enumeration value" label_enumeration_new: "New enumeration value"
label_enumeration_value: "Enumeration value" label_enumeration_value: "Enumeration value"
label_enumerations: "Enumerations" label_enumerations: "Enumerations"
label_enterprise: "Enterprise"
label_enterprise_edition: "Enterprise Edition"
label_environment: "Environment" label_environment: "Environment"
label_equals: "is" label_equals: "is"
label_example: "Example" label_example: "Example"
@ -1054,6 +1082,7 @@ en:
label_show_hide: "Show/hide" label_show_hide: "Show/hide"
label_journal: "Journal" label_journal: "Journal"
label_journal_diff: "Description Comparison" label_journal_diff: "Description Comparison"
label_language: "Language"
label_jump_to_a_project: "Jump to a project..." label_jump_to_a_project: "Jump to a project..."
label_language_based: "Based on user's language" label_language_based: "Based on user's language"
label_last_activity: "Last activity" label_last_activity: "Last activity"
@ -1272,6 +1301,7 @@ en:
label_updated_time_at: "%{author} %{age}" label_updated_time_at: "%{author} %{age}"
label_updated_time_by: "Updated by %{author} %{age} ago" label_updated_time_by: "Updated by %{author} %{age} ago"
label_used_by: "Used by" label_used_by: "Used by"
label_user: "User"
label_user_activity: "%{value}'s activity" label_user_activity: "%{value}'s activity"
label_user_anonymous: "Anonymous" label_user_anonymous: "Anonymous"
label_user_mail_option_all: "For any event on all my projects" label_user_mail_option_all: "For any event on all my projects"

@ -43,6 +43,7 @@ en:
button_cancel: "Cancel" button_cancel: "Cancel"
button_check_all: "Check all" button_check_all: "Check all"
button_confirm: "Confirm" button_confirm: "Confirm"
button_continue: "Continue"
button_copy: "Copy" button_copy: "Copy"
button_delete: "Delete" button_delete: "Delete"
button_delete_watcher: "Delete watcher" button_delete_watcher: "Delete watcher"
@ -525,6 +526,10 @@ en:
button_save: "Save" button_save: "Save"
button_submit: "Submit" button_submit: "Submit"
button_cancel: "Cancel" button_cancel: "Cancel"
form_submit:
title: 'Confirm to continue'
text: 'Are you sure you want to perform this action?'
notice_successful_create: "Successful creation." notice_successful_create: "Successful creation."
notice_successful_delete: "Successful deletion." notice_successful_delete: "Successful deletion."
notice_successful_update: "Successful update." notice_successful_update: "Successful update."

@ -373,6 +373,7 @@ OpenProject::Application.routes.draw do
scope 'admin' do scope 'admin' do
resource :announcements, only: [:edit, :update] resource :announcements, only: [:edit, :update]
resource :enterprise, only: [:show, :create, :destroy]
resources :enumerations resources :enumerations
resources :groups do resources :groups do

@ -0,0 +1,9 @@
class CreateEnterpriseToken < ActiveRecord::Migration[5.0]
def change
create_table :enterprise_tokens do |t|
t.text :encoded_token
t.timestamps
end
end
end

@ -0,0 +1,99 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import IAugmentedJQuery = angular.IAugmentedJQuery;
import { IDialogOpenResult, IDialogService } from 'ng-dialog';
import {IDialogScope} from 'ng-dialog';
export class ConfirmFormSubmitController {
// Allow original form submission after dialog was closed
private confirmed = false;
private dialog: IDialogOpenResult;
constructor(protected $element:IAugmentedJQuery,
protected $scope:angular.IScope,
protected $http:angular.IHttpService,
protected $q:angular.IQService,
protected ngDialog:IDialogService,
protected I18n:op.I18n) {
this.$scope['text'] = {
title: I18n.t('js.modals.form_submit.title'),
text: I18n.t('js.modals.form_submit.text'),
button_continue: I18n.t('js.button_continue'),
button_cancel: I18n.t('js.button_cancel')
};
this.$scope['confirmAndClose'] = () => {
this.confirmed = true;
this.dialog.close();
};
$element.on('submit', (evt) => {
if (!this.confirmed) {
evt.preventDefault();
this.openConfirmationDialog();
return false;
}
return true;
});
}
public openConfirmationDialog() {
this.dialog = this.ngDialog.open({
closeByEscape: true,
showClose: true,
closeByDocument: true,
scope: <IDialogScope> this.$scope,
template: '/components/modals/confirm-form-submit/confirm-form-submit.modal.html',
className: 'ngdialog-theme-openproject',
preCloseCallback: () => {
if (this.confirmed) {
this.$element.submit();
}
return true;
}
});
}
}
function confirmFormSubmit() {
return {
restrict: 'AC',
scope: {},
bindToController: true,
controller: ConfirmFormSubmitController,
controllerAs: '$ctrl',
};
}
angular
.module('openproject.uiComponents')
.directive('confirmFormSubmit', confirmFormSubmit);

@ -0,0 +1,18 @@
<div class="confirm-form-submit--modal">
<div class="onboarding--start-modal">
<div class="onboarding--top-menu">
<span class="icon-context icon-attention"></span>
<h2 ng-bind="::text.title"></h2>
</div>
<div class="ngdialog-body onboarding--main">
<p ng-bind="::text.text"></p>
</div>
<div class="onboarding--footer">
<button class="confirm-form-submit--continue button -highlight"
ng-click="confirmAndClose()"
ng-bind="::text.button_continue"
ng-attr-title="::text.button_continue">
</button>
</div>
</div>
</div>

@ -93,7 +93,9 @@ module OpenProject
'apiv2_enable_basic_auth' => true, 'apiv2_enable_basic_auth' => true,
'onboarding_video_url' => 'https://player.vimeo.com/video/163426858?autoplay=1', 'onboarding_video_url' => 'https://player.vimeo.com/video/163426858?autoplay=1',
'onboarding_enabled' => true 'onboarding_enabled' => true,
'ee_manager_visible' => true
} }
@config = nil @config = nil

@ -42,6 +42,10 @@ module OpenProject
def links def links
{ {
upsale: {
href: 'https://www.openproject.org/enterprise',
label: 'homescreen.links.upgrade_enterprise_edition'
},
user_guides: { user_guides: {
href: 'https://www.openproject.org/help/user-guides', href: 'https://www.openproject.org/help/user-guides',
label: 'homescreen.links.user_guides' label: 'homescreen.links.user_guides'

@ -83,6 +83,9 @@ module Redmine::MenuManager::TopMenu::HelpMenu
class: 'drop-down--help-headline', class: 'drop-down--help-headline',
title: l('top_menu.help_and_support') title: l('top_menu.help_and_support')
} }
if EnterpriseToken.show_banners
result << static_link_item(:upsale, href_suffix: "?utm_source=ce-helpmenu")
end
result << static_link_item(:user_guides) result << static_link_item(:user_guides)
result << static_link_item(:faq) result << static_link_item(:faq)
result << content_tag(:li) { result << content_tag(:li) {
@ -111,11 +114,11 @@ module Redmine::MenuManager::TopMenu::HelpMenu
result << static_link_item(:api_docs) result << static_link_item(:api_docs)
end end
def static_link_item(key) def static_link_item(key, options = {})
link = OpenProject::Static::Links.links[key] link = OpenProject::Static::Links.links[key]
label = I18n.t(link[:label]) label = I18n.t(link[:label])
content_tag(:li) do content_tag(:li) do
link_to label, link[:href], title: label link_to label, "#{link[:href]}#{options[:href_suffix]}", title: label
end end
end end
end end

@ -1,13 +1,13 @@
#-- encoding: UTF-8 #-- encoding: UTF-8
#-- copyright #-- copyright
# OpenProject is a project management system. # OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) # Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3. # modify it under the terms of the GNU General Public License version 3.
# #
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang # Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team # Copyright (C) 2010-2013 the ChiliProject Team
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or

@ -0,0 +1,154 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public EnterpriseToken version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public EnterpriseToken
# as published by the Free Software Foundation; either version 2
# of the EnterpriseToken, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public EnterpriseToken for more details.
#
# You should have received a copy of the GNU General Public EnterpriseToken
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe EnterprisesController, type: :controller do
let(:a_token) { EnterpriseToken.new }
let(:token_object) do
token = OpenProject::Token.new
token.subscriber = 'Foobar'
token.mail = 'foo@example.org'
token.starts_at = Date.today
token.expires_at = nil
token
end
before do
login_as user
allow(a_token).to receive(:token_object).and_return(token_object)
end
context 'with admin' do
let(:user) { FactoryGirl.build(:admin) }
describe '#show' do
render_views
context 'when token exists' do
before do
allow(EnterpriseToken).to receive(:current).and_return(a_token)
get :show
end
it 'renders the overview' do
expect(response).to be_success
expect(response).to render_template 'show'
expect(response).to render_template partial: 'enterprises/_current'
expect(response).to render_template partial: 'enterprises/_form'
end
end
context 'when no token exists' do
before do
allow(EnterpriseToken).to receive(:current).and_return(nil)
get :show
end
it 'still renders #show with form' do
expect(response).not_to render_template partial: 'enterprises/_current'
expect(response.body).to have_selector '.upsale-notification'
end
end
end
describe '#create' do
let(:params) do
{
enterprise_token: { encoded_token: 'foo' }
}
end
before do
allow(EnterpriseToken).to receive(:new).and_return(a_token)
expect(a_token).to receive(:encoded_token=).with('foo')
expect(a_token).to receive(:save).and_return(valid)
post :create, params: params
end
context 'valid token input' do
let(:valid) { true }
it 'redirects to index' do
expect(controller).to set_flash[:notice].to I18n.t(:notice_successful_update)
expect(response).to redirect_to action: :show
end
end
context 'invalid token input' do
let(:valid) { false }
it 'renders with error' do
expect(response).not_to be_redirect
expect(response).to render_template 'enterprises/show'
end
end
end
describe '#destroy' do
context 'when a token exists' do
before do
expect(EnterpriseToken).to receive(:current).and_return(a_token)
expect(a_token).to receive(:destroy)
delete :destroy
end
it 'redirects to show' do
expect(controller).to set_flash[:notice].to I18n.t(:notice_successful_delete)
expect(response).to redirect_to action: :show
end
end
context 'when no token exists' do
before do
expect(EnterpriseToken).to receive(:current).and_return(nil)
delete :destroy
end
it 'renders 404' do
expect(response.status).to eq(404)
end
end
end
end
context 'regular user' do
let(:user) { FactoryGirl.build(:user) }
before do
get :show
end
it 'is forbidden' do
expect(response.status).to eq 403
end
end
end

@ -0,0 +1,118 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public EnterpriseToken version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public EnterpriseToken
# as published by the Free Software Foundation; either version 2
# of the EnterpriseToken, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public EnterpriseToken for more details.
#
# You should have received a copy of the GNU General Public EnterpriseToken
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe 'Enterprise token', type: :feature do
include Redmine::I18n
let(:admin) { FactoryGirl.create(:admin) }
let(:token_object) do
token = OpenProject::Token.new
token.subscriber = 'Foobar'
token.mail = 'foo@example.org'
token.starts_at = Date.today
token.expires_at = nil
token
end
let(:textarea) { find '#enterprise_token_encoded_token' }
let(:submit_button) { find '#token-submit-button' }
describe 'EnterpriseToken management' do
before do
login_as(admin)
visit enterprise_path
end
it 'shows a teaser and token form without a token' do
expect(page).to have_selector('.upsale-notification a', text: 'Order Enterprise Edition')
expect(textarea.value).to be_empty
textarea.set 'foobar'
submit_button.click
# Error output
expect(page).to have_selector('.errorExplanation',
text: "Enterprise support token can't be read. Are you sure it is a support token?")
expect(page).to have_selector('span.errorSpan #enterprise_token_encoded_token')
# Keeps value
expect(textarea.value).to eq('foobar')
end
context 'assuming valid input' do
before do
allow(OpenProject::Token).to receive(:import).and_return(token_object)
end
it 'allows token import flow', js: true do
textarea.set 'foobar'
submit_button.click
expect(page).to have_selector('.flash.notice', text: I18n.t(:notice_successful_update))
expect(page).to have_selector('.enterprise--active-token')
expect(page.all('.attributes-key-value--key').map(&:text))
.to eq ['Subscriber', 'Email', 'Valid since']
expect(page.all('.attributes-key-value--value').map(&:text))
.to eq ['Foobar', 'foo@example.org', format_date(Date.today)]
expect(page).to have_selector('.button.icon-delete', text: I18n.t(:button_delete))
# Expect section to be collapsed
expect(page).to have_no_selector('#token_encoded_token', visible: true)
expect(EnterpriseToken.current.encoded_token).to eq('foobar')
# Replace token
find('.collapsible-section--toggle-link').click
textarea.set 'blabla'
submit_button.click
expect(page).to have_selector('.flash.notice', text: I18n.t(:notice_successful_update))
# Assume next request
RequestStore.clear!
expect(EnterpriseToken.current.encoded_token).to eq('blabla')
# Remove token
find('.button.icon-delete', text: I18n.t(:button_delete)).click
# Expect modal
find('.confirm-form-submit--continue').click
expect(textarea.value).to be_empty
expect(page).to have_selector('.flash.notice', text: I18n.t(:notice_successful_delete))
# Assume next request
RequestStore.clear!
expect(EnterpriseToken.current).to be_nil
end
end
end
end

@ -0,0 +1,92 @@
Grequire 'spec_helper'
RSpec.describe EnterpriseToken, type: :model do
let(:object) { OpenProject::Token.new }
subject { EnterpriseToken.new(encoded_token: 'foo') }
before do
EnterpriseToken.clear_cache
end
describe 'existing token' do
before do
allow_any_instance_of(EnterpriseToken).to receive(:token_object).and_return(object)
subject.save!(validate: false)
end
context 'when inner token is active' do
it 'has an active token' do
expect(object).to receive(:expired?).and_return(false)
expect(EnterpriseToken.count).to eq(1)
expect(EnterpriseToken.current).to eq(subject)
expect(EnterpriseToken.current.encoded_token).to eq('foo')
expect(EnterpriseToken.show_banners).to eq(false)
# Deleting it updates the current token
EnterpriseToken.current.destroy!
expect(EnterpriseToken.count).to eq(0)
expect(EnterpriseToken.current).to be_nil
end
it 'delegates to the token object' do
allow(object).to receive_messages(
subscriber: 'foo',
mail: 'bar',
starts_at: Date.today,
issued_at: Date.today,
expires_at: 'never',
restrictions: { foo: :bar }
)
expect(subject.subscriber).to eq('foo')
expect(subject.mail).to eq('bar')
expect(subject.starts_at).to eq(Date.today)
expect(subject.issued_at).to eq(Date.today)
expect(subject.expires_at).to eq('never')
expect(subject.restrictions).to eq(foo: :bar)
end
describe '#allows_to?' do
let(:service_double) { ::Authorization::EnterpriseService.new(subject) }
before do
expect(::Authorization::EnterpriseService).to receive(:new).twice.with(subject).and_return(service_double)
end
it 'forwards to EnterpriseTokenService for checks' do
expect(service_double).to receive(:call).with(:forbidden_action).and_return double('ServiceResult', result: false)
expect(service_double).to receive(:call).with(:allowed_action).and_return double('ServiceResult', result: true)
expect(EnterpriseToken.allows_to?(:forbidden_action)).to eq false
expect(EnterpriseToken.allows_to?(:allowed_action)).to eq true
end
end
end
context 'when inner token is expired' do
before do
expect(object).to receive(:expired?).and_return(true)
end
it 'has an expired token' do
expect(EnterpriseToken.current).to eq(subject)
expect(EnterpriseToken.show_banners).to eq(true)
end
end
end
describe 'no token' do
it do
expect(EnterpriseToken.current).to be_nil
expect(EnterpriseToken.show_banners).to eq(true)
end
end
describe 'invalid token' do
it 'appears as if no token is shown' do
expect(EnterpriseToken.current).to be_nil
expect(EnterpriseToken.show_banners).to eq(true)
end
end
end

@ -0,0 +1,87 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe Authorization::EnterpriseService do
let(:token_object) do
token = OpenProject::Token.new
token.subscriber = 'Foobar'
token.mail = 'foo@example.org'
token.starts_at = Date.today
token.expires_at = nil
token
end
let(:token) { mock_model(EnterpriseToken, token_object: token_object) }
let(:instance) { described_class.new(token) }
let(:result) { instance.call(action) }
let(:action) { :an_action }
describe '#initialize' do
it 'has the token' do
expect(instance.token).to eql token
end
end
describe 'expiry' do
before do
allow(token).to receive(:expired?).and_return(expired)
end
context 'when expired' do
let(:expired) { true }
it 'returns a false result' do
expect(result).to be_kind_of ServiceResult
expect(result.result).to be_falsey
expect(result.success?).to be_falsey
end
end
context 'when active' do
let(:expired) { false }
context 'invalid action' do
it 'returns false' do
expect(result.result).to be_falsey
end
end
context 'valid action requires active token' do
let(:action) { :define_custom_style }
it 'returns a true result' do
expect(result).to be_kind_of ServiceResult
expect(result.result).to be_truthy
expect(result.success?).to be_truthy
end
end
end
end
end
Loading…
Cancel
Save