Merge branch 'dev' into implementation/42552-api-add-live-file-link-collections-to-storages

pull/10703/head
Eric Schubert 2 years ago
commit 5a7aa2afba
No known key found for this signature in database
GPG Key ID: 1D346C019BD4BAA2
  1. 5
      .rubocop.yml
  2. 14
      Gemfile
  3. 160
      Gemfile.lock
  4. 1
      app/cells/enumerations/table_cell.rb
  5. 4
      app/cells/placeholder_users/table_cell.rb
  6. 15
      app/contracts/placeholder_users/delete_contract.rb
  7. 2
      app/contracts/work_packages/create_contract.rb
  8. 8
      app/controllers/application_controller.rb
  9. 6
      app/controllers/concerns/user_invitation.rb
  10. 15
      app/controllers/work_packages/auto_completes_controller.rb
  11. 2
      app/helpers/avatar_helper.rb
  12. 3
      app/helpers/placeholder_users_helper.rb
  13. 2
      app/helpers/projects_helper.rb
  14. 2
      app/helpers/warning_bar_helper.rb
  15. 8
      app/models/concerns/tableless.rb
  16. 2
      app/models/custom_option.rb
  17. 71
      app/models/day.rb
  18. 4
      app/models/non_working_day.rb
  19. 10
      app/models/projects/activity.rb
  20. 34
      app/models/queries/days.rb
  21. 36
      app/models/queries/days/day_query.rb
  22. 62
      app/models/queries/days/filters/dates_interval_filter.rb
  23. 35
      app/models/queries/days/filters/day_filter.rb
  24. 35
      app/models/queries/days/filters/working_filter.rb
  25. 15
      app/models/queries/filters/base.rb
  26. 30
      app/models/queries/week_days/week_day_query.rb
  27. 2
      app/models/queries/work_packages/filter/relatable_filter.rb
  28. 71
      app/models/query/results.rb
  29. 36
      app/models/query/results/group_by.rb
  30. 1
      app/models/setting.rb
  31. 11
      app/models/setting/aliases.rb
  32. 80
      app/models/setting/mail_settings.rb
  33. 22
      app/models/user.rb
  34. 6
      app/models/week_day.rb
  35. 58
      app/seeders/basic_data/week_day_seeder.rb
  36. 1
      app/seeders/standard_seeder/basic_data_seeder.rb
  37. 41
      app/services/authorization/user_allowed_service.rb
  38. 38
      app/services/ldap/base_service.rb
  39. 1
      app/views/admin/settings/general_settings/show.html.erb
  40. 2
      app/views/homescreen/blocks/_projects.html.erb
  41. 2
      app/views/projects/index.html.erb
  42. 6
      app/workers/application_job.rb
  43. 24
      config/application.rb
  44. 0
      config/constants/open_project/null_db_fallback.rb
  45. 41
      config/constants/open_project/project_activity.rb
  46. 21
      config/constants/settings/definitions.rb
  47. 6
      config/initializers/00-load_plugins.rb
  48. 1
      config/initializers/05-null_db_fallback.rb
  49. 12
      config/initializers/10-load_patches.rb
  50. 28
      config/initializers/activity.rb
  51. 2
      config/initializers/bcrypt.rb
  52. 32
      config/initializers/cache_store.rb
  53. 11
      config/initializers/grape.rb
  54. 4
      config/initializers/menus.rb
  55. 10
      config/initializers/migrate_email_settings.rb
  56. 8
      config/initializers/module_handler.rb
  57. 622
      config/initializers/permissions.rb
  58. 40
      config/initializers/rack_timeout.rb
  59. 9
      config/initializers/register_mail_interceptors.rb
  60. 128
      config/initializers/secure_headers.rb
  61. 146
      config/initializers/sentry.rb
  62. 31
      config/initializers/session_store.rb
  63. 92
      config/initializers/subscribe_listeners.rb
  64. 18
      config/initializers/user_invitation.rb
  65. 44
      config/initializers/warden.rb
  66. 1
      config/initializers/zeitwerk.rb
  67. 98
      config/locales/crowdin/js-az.yml
  68. 21
      db/migrate/20220511124930_create_week_days.rb
  69. 11
      db/migrate/20220517113828_create_non_working_days.rb
  70. 6
      docs/api/apiv3/paths/days.yml
  71. 10
      docs/development/localhost-ssl/README.md
  72. 4
      docs/installation-and-operations/configuration/environment/README.md
  73. 2
      docs/installation-and-operations/configuration/ssl/README.md
  74. 22
      docs/system-admin-guide/authentication/ldap-authentication/README.md
  75. 55
      docs/system-admin-guide/authentication/saml/README.md
  76. 2
      frontend/src/app/spot/components/text-field/text-field.component.ts
  77. 2
      lib/api/decorators/single.rb
  78. 29
      lib/api/root_api.rb
  79. 32
      lib/api/v3/days/day_collection_representer.rb
  80. 18
      lib/api/v3/days/day_representer.rb
  81. 43
      lib/api/v3/days/days_api.rb
  82. 43
      lib/api/v3/days/non_working_day_representer.rb
  83. 45
      lib/api/v3/days/non_working_days_api.rb
  84. 47
      lib/api/v3/days/week_api.rb
  85. 32
      lib/api/v3/days/week_day_collection_representer.rb
  86. 43
      lib/api/v3/days/week_day_representer.rb
  87. 3
      lib/api/v3/root.rb
  88. 4
      lib/api/v3/root_representer.rb
  89. 10
      lib/api/v3/utilities/endpoints/index.rb
  90. 30
      lib/api/v3/utilities/path_helper.rb
  91. 2
      lib/open_project.rb
  92. 2
      lib/open_project/access_control.rb
  93. 237
      lib/open_project/configuration.rb
  94. 5
      lib/open_project/plugins/acts_as_op_engine.rb
  95. 9
      lib/redmine/plugin.rb
  96. 4
      lib/tasks/code.rake
  97. 2
      lib/tasks/copyright.rake
  98. 10
      lib/tasks/packager.rake
  99. 1
      lib/tasks/parallel_testing.rake
  100. 26
      lib_static/open_project/authentication.rb
  101. Some files were not shown because too many files have changed in this diff Show More

@ -3,7 +3,7 @@ require:
- rubocop-rspec
AllCops:
TargetRubyVersion: 3.0
TargetRubyVersion: 3.1
# Enable any new cops in new versions by default
NewCops: enable
Exclude:
@ -172,8 +172,7 @@ RSpec/MultipleExpectations:
- 'modules/*/spec/features/**/*.rb'
RSpec/MultipleMemoizedHelpers:
Max: 20
AllowSubject: true
Enabled: false
RSpec/NestedGroups:
Max: 4

@ -34,7 +34,7 @@ gem 'actionpack-xml_parser', '~> 2.0.0'
gem 'activemodel-serializers-xml', '~> 1.0.1'
gem 'activerecord-import', '~> 1.4.0'
gem 'activerecord-session_store', '~> 2.0.0'
gem 'rails', '~> 6.1.5', '>= 6.1.5.1'
gem 'rails', '~> 7.0.3'
gem 'responders', '~> 3.0'
gem 'ffi', '~> 1.15'
@ -65,12 +65,6 @@ gem 'typed_dag', '~> 2.0.2', require: false
gem 'addressable', '~> 2.8.0'
# Needed to make rails 6.x work with ruby 3.1, can be dropped
# after migrated to rails 7 (see https://stackoverflow.com/a/70500221)
gem 'net-smtp', '~> 0.3.1', require: false
gem 'net-pop', '~> 0.1.1', require: false
gem 'net-imap', '~> 0.2.3', require: false
# Remove whitespace from model input
gem "auto_strip_attributes", "~> 2.5"
@ -173,7 +167,9 @@ end
gem 'i18n-js', '~> 3.9.0'
gem 'rails-i18n', '~> 7.0.0'
gem 'sprockets', '~> 3.7.0'
gem 'sprockets', '~> 3.7.2' # lock sprockets below 4.0
gem 'sprockets-rails', '~> 3.4.2'
gem 'puma', '~> 5.6'
gem 'rack-timeout', '~> 0.6.0', require: "rack/timeout/base"
@ -219,7 +215,7 @@ group :test do
gem 'rack_session_access'
gem 'rspec', '~> 3.11.0'
# also add to development group, so "spec" rake task gets loaded
gem 'rspec-rails', '~> 5.1.0', group: :development
gem 'rspec-rails', '6.0.0.rc1', group: :development
# Retry failures within the same environment
gem 'retriable', '~> 3.1.1'

@ -188,59 +188,66 @@ GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.1.0)
actioncable (6.1.6)
actionpack (= 6.1.6)
activesupport (= 6.1.6)
actioncable (7.0.3)
actionpack (= 7.0.3)
activesupport (= 7.0.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.6)
actionpack (= 6.1.6)
activejob (= 6.1.6)
activerecord (= 6.1.6)
activestorage (= 6.1.6)
activesupport (= 6.1.6)
actionmailbox (7.0.3)
actionpack (= 7.0.3)
activejob (= 7.0.3)
activerecord (= 7.0.3)
activestorage (= 7.0.3)
activesupport (= 7.0.3)
mail (>= 2.7.1)
actionmailer (6.1.6)
actionpack (= 6.1.6)
actionview (= 6.1.6)
activejob (= 6.1.6)
activesupport (= 6.1.6)
net-imap
net-pop
net-smtp
actionmailer (7.0.3)
actionpack (= 7.0.3)
actionview (= 7.0.3)
activejob (= 7.0.3)
activesupport (= 7.0.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (6.1.6)
actionview (= 6.1.6)
activesupport (= 6.1.6)
rack (~> 2.0, >= 2.0.9)
actionpack (7.0.3)
actionview (= 7.0.3)
activesupport (= 7.0.3)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actionpack-xml_parser (2.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
actiontext (6.1.6)
actionpack (= 6.1.6)
activerecord (= 6.1.6)
activestorage (= 6.1.6)
activesupport (= 6.1.6)
actiontext (7.0.3)
actionpack (= 7.0.3)
activerecord (= 7.0.3)
activestorage (= 7.0.3)
activesupport (= 7.0.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (6.1.6)
activesupport (= 6.1.6)
actionview (7.0.3)
activesupport (= 7.0.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.1.6)
activesupport (= 6.1.6)
activejob (7.0.3)
activesupport (= 7.0.3)
globalid (>= 0.3.6)
activemodel (6.1.6)
activesupport (= 6.1.6)
activemodel (7.0.3)
activesupport (= 7.0.3)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
activerecord (6.1.6)
activemodel (= 6.1.6)
activesupport (= 6.1.6)
activerecord (7.0.3)
activemodel (= 7.0.3)
activesupport (= 7.0.3)
activerecord-import (1.4.0)
activerecord (>= 4.2)
activerecord-nulldb-adapter (0.8.0)
@ -251,19 +258,18 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 2.0.8, < 3)
railties (>= 5.2.4.1)
activestorage (6.1.6)
actionpack (= 6.1.6)
activejob (= 6.1.6)
activerecord (= 6.1.6)
activesupport (= 6.1.6)
activestorage (7.0.3)
actionpack (= 7.0.3)
activejob (= 7.0.3)
activerecord (= 7.0.3)
activesupport (= 7.0.3)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (6.1.6)
activesupport (7.0.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
acts_as_list (1.0.4)
activerecord (>= 4.2)
acts_as_tree (2.9.1)
@ -272,7 +278,7 @@ GEM
public_suffix (>= 2.0.2, < 5.0)
aes_key_wrap (1.1.0)
afm (0.2.2)
airbrake (13.0.1)
airbrake (13.0.2)
airbrake-ruby (~> 6.0)
airbrake-ruby (6.1.0)
rbtree3 (~> 0.5)
@ -283,13 +289,13 @@ GEM
awesome_nested_set (3.5.0)
activerecord (>= 4.0.0, < 7.1)
aws-eventstream (1.2.0)
aws-partitions (1.587.0)
aws-sdk-core (3.130.2)
aws-partitions (1.589.0)
aws-sdk-core (3.131.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.56.0)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
@ -657,7 +663,7 @@ GEM
octokit (4.22.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
oj (3.13.11)
oj (3.13.13)
okcomputer (1.18.4)
omniauth-saml (1.10.3)
omniauth (~> 1.3, >= 1.3.2)
@ -676,7 +682,7 @@ GEM
openproject-token (2.2.0)
activemodel
parallel (1.22.1)
parallel_tests (3.8.1)
parallel_tests (3.10.0)
parallel
parser (3.1.2.0)
ast (~> 2.4.1)
@ -758,21 +764,20 @@ GEM
rack_session_access (0.2.0)
builder (>= 2.0.0)
rack (>= 1.0.0)
rails (6.1.6)
actioncable (= 6.1.6)
actionmailbox (= 6.1.6)
actionmailer (= 6.1.6)
actionpack (= 6.1.6)
actiontext (= 6.1.6)
actionview (= 6.1.6)
activejob (= 6.1.6)
activemodel (= 6.1.6)
activerecord (= 6.1.6)
activestorage (= 6.1.6)
activesupport (= 6.1.6)
rails (7.0.3)
actioncable (= 7.0.3)
actionmailbox (= 7.0.3)
actionmailer (= 7.0.3)
actionpack (= 7.0.3)
actiontext (= 7.0.3)
actionview (= 7.0.3)
activejob (= 7.0.3)
activemodel (= 7.0.3)
activerecord (= 7.0.3)
activestorage (= 7.0.3)
activesupport (= 7.0.3)
bundler (>= 1.15.0)
railties (= 6.1.6)
sprockets-rails (>= 2.0.0)
railties (= 7.0.3)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@ -785,12 +790,13 @@ GEM
rails-i18n (7.0.3)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
railties (6.1.6)
actionpack (= 6.1.6)
activesupport (= 6.1.6)
railties (7.0.3)
actionpack (= 7.0.3)
activesupport (= 7.0.3)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
rb-fsevent (0.11.1)
@ -837,14 +843,14 @@ GEM
rspec-mocks (3.11.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-rails (5.1.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
railties (>= 5.2)
rspec-core (~> 3.10)
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-rails (6.0.0.rc1)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.11)
rspec-expectations (~> 3.11)
rspec-mocks (~> 3.11)
rspec-support (~> 3.11)
rspec-retry (0.6.2)
rspec-core (> 3.3)
rspec-support (3.11.0)
@ -863,7 +869,7 @@ GEM
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
rubocop-rspec (2.10.0)
rubocop-rspec (2.11.1)
rubocop (~> 1.19)
ruby-duration (3.2.3)
activesupport (>= 3.0.0)
@ -1070,10 +1076,7 @@ DEPENDENCIES
mini_magick (~> 4.11.0)
multi_json (~> 1.15.0)
my_page!
net-imap (~> 0.2.3)
net-ldap (~> 0.17.0)
net-pop (~> 0.1.1)
net-smtp (~> 0.3.1)
nokogiri (~> 1.13.4)
oj (~> 3.13.0)
okcomputer (~> 1.18.1)
@ -1124,7 +1127,7 @@ DEPENDENCIES
rack-test (~> 1.1.0)
rack-timeout (~> 0.6.0)
rack_session_access
rails (~> 6.1.5, >= 6.1.5.1)
rails (~> 7.0.3)
rails-controller-testing (~> 1.0.2)
rails-i18n (~> 7.0.0)
rdoc (>= 2.4.2)
@ -1136,7 +1139,7 @@ DEPENDENCIES
roar (~> 1.1.0)
rouge (~> 3.28.0)
rspec (~> 3.11.0)
rspec-rails (~> 5.1.0)
rspec-rails (= 6.0.0.rc1)
rspec-retry (~> 0.6.1)
rubocop
rubocop-rails
@ -1157,7 +1160,8 @@ DEPENDENCIES
shoulda-matchers (~> 5.0)
spring
spring-commands-rspec
sprockets (~> 3.7.0)
sprockets (~> 3.7.2)
sprockets-rails (~> 3.4.2)
stackprof
stringex (~> 2.8.5)
structured_warnings (~> 0.4.0)

@ -24,6 +24,7 @@ module Enumerations
link_to new_enumeration_path(type: model.name),
aria: { label: t(:label_enumeration_new) },
class: 'wp-inline-create--add-link',
data: { 'qa-selector': "create-enumeration-#{model.name.underscore.dasherize}" },
title: t(:label_enumeration_new) do
op_icon('icon icon-add')
end

@ -52,9 +52,5 @@ module PlaceholderUsers
def desc_by_default
[:created_at]
end
def user_allowed_service
@user_allowed_service ||= Authorization::UserAllowedService.new(options[:current_user])
end
end
end

@ -36,19 +36,18 @@ module PlaceholderUsers
# Checks if a given placeholder user may be deleted by a user.
#
# @param actor [User] User who wants to delete the given placeholder user.
def self.deletion_allowed?(placeholder_user,
actor,
user_allowed_service = Authorization::UserAllowedService.new(actor))
def self.deletion_allowed?(placeholder_user, actor)
actor.allowed_to_globally?(:manage_placeholder_user) &&
affected_projects_managed_by_actor?(placeholder_user, user_allowed_service)
affected_projects_managed_by_actor?(placeholder_user, actor)
end
protected
def self.affected_projects_managed_by_actor?(placeholder_user, user_allowed_service)
def self.affected_projects_managed_by_actor?(placeholder_user, actor)
placeholder_user.projects.active.empty? ||
user_allowed_service.call(:manage_members, placeholder_user.projects.active).result
actor.allowed_to?(:manage_members, placeholder_user.projects.active)
end
private_class_method :affected_projects_managed_by_actor?
protected
def deletion_allowed?
self.class.deletion_allowed?(model, user)

@ -43,7 +43,7 @@ module WorkPackages
def user_allowed_to_add
if (model.project && !@user.allowed_to?(:add_work_packages, model.project)) ||
!@user.allowed_to?(:add_work_packages, nil, global: true)
!@user.allowed_to_globally?(:add_work_packages)
errors.add :base, :error_unauthorized
end

@ -139,7 +139,7 @@ class ApplicationController < ActionController::Base
:stop_if_feeds_disabled,
:set_cache_buster,
:action_hooks,
:reload_mailer_configuration!
:reload_mailer_settings!
include Redmine::Search::Controller
include Redmine::MenuManager::MenuController
@ -167,8 +167,8 @@ class ApplicationController < ActionController::Base
end
end
def reload_mailer_configuration!
OpenProject::Configuration.reload_mailer_configuration!
def reload_mailer_settings!
Setting.reload_mailer_settings!
end
# Checks if the session cookie is missing.
@ -228,7 +228,7 @@ class ApplicationController < ActionController::Base
# Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action], global = false)
context = @project || @projects
is_authorized = AuthorizationService.new({ controller: ctrl, action: action }, context: context, global: global).call
is_authorized = User.current.allowed_to?({ controller: ctrl, action: }, context, global:)
unless is_authorized
if @project&.archived?

@ -139,10 +139,10 @@ module UserInvitation
token = Token::Invitation.create! user: user
user.save!
return [user, token]
[user, token]
else
[user, nil]
end
end
[user, nil]
end
end

@ -81,19 +81,6 @@ class WorkPackages::AutoCompletesController < ::ApplicationController
end
def work_package_scope
scope = WorkPackage.all
# The filter on subject in combination with the ORDER BY on id
# seems to trip MySql's usage of indexes on the order statement
# I haven't seen similar problems on postgresql but there might be as the
# data at hand was not very large.
#
# For MySql we are therefore helping the DB optimizer to use the correct index
if ActiveRecord::Base.connection_config[:adapter] == 'mysql2'
scope = scope.from("#{WorkPackage.table_name} USE INDEX(PRIMARY)")
end
scope
WorkPackage.all
end
end

@ -38,3 +38,5 @@ module AvatarHelper
''.html_safe
end
end
ActiveSupport.run_load_hooks(:op_helpers_avatar, AvatarHelper)

@ -31,7 +31,6 @@ module PlaceholderUsersHelper
# Determine whether the given actor can delete the placeholder user
def can_delete_placeholder_user?(placeholder, actor = User.current)
PlaceholderUsers::DeleteContract.deletion_allowed? placeholder,
actor,
Authorization::UserAllowedService.new(actor)
actor
end
end

@ -57,7 +57,7 @@ module ProjectsHelper
end
def no_projects_result_box_params
if User.current.allowed_to?(:add_project, nil, global: true)
if User.current.allowed_to_globally?(:add_project)
{ action_url: new_project_path, display_action: true }
else
{}

@ -40,7 +40,7 @@ module WarningBarHelper
end
def setting_protocol_mismatched?
(request.ssl? && Setting.protocol == 'http') || (!request.ssl? && Setting.protocol == 'https')
request.ssl? != OpenProject::Configuration.secure_connection?
end
def setting_hostname_mismatched?

@ -42,15 +42,15 @@ module Tableless
@columns_hash ||= Hash.new
# From active_record/attributes.rb
attributes_to_define_after_schema_loads.each do |name, (type, options)|
attributes_to_define_after_schema_loads.each do |name, (type, default)|
if type.is_a?(Symbol)
type = ActiveRecord::Type.lookup(type, **options.except(:default))
type = ActiveRecord::Type.lookup(type, default)
end
define_attribute(name, type, **options.slice(:default))
define_attribute(name, type, default: default)
# Improve Model#inspect output
@columns_hash[name.to_s] = ActiveRecord::ConnectionAdapters::Column.new(name.to_s, options[:default])
@columns_hash[name.to_s] = ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default)
end
end
end

@ -49,7 +49,7 @@ class CustomOption < ApplicationRecord
def assure_at_least_one_option
return if CustomOption.where(custom_field_id: custom_field_id).where.not(id: id).count > 0
errors[:base] << I18n.t(:'activerecord.errors.models.custom_field.at_least_one_custom_option')
errors.add(:base, I18n.t(:'activerecord.errors.models.custom_field.at_least_one_custom_option'))
throw :abort
end

@ -0,0 +1,71 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
class Day < ApplicationRecord
include Tableless
belongs_to :week_day,
inverse_of: false,
class_name: 'WeekDay',
foreign_key: :day_of_week,
primary_key: :day
attribute :date, :date, default: nil
attribute :day_of_week, :integer, default: nil
attribute :working, :boolean, default: 't'
delegate :name, to: :week_day
def self.default
today = Time.zone.today
from = today.at_beginning_of_month
to = today.next_month.at_end_of_month
select('days.*')
.includes(:week_day)
.from(Arel.sql(from_sql(from:, to:)))
end
def self.from_sql(from:, to:)
<<~SQL.squish
(
SELECT
date_trunc('day', dd)::date date,
extract(isodow from dd) day_of_week,
week_days.working
FROM generate_series
( '#{from}'::timestamp,
'#{to}'::timestamp,
'1 day'::interval) dd
LEFT JOIN week_days
ON extract(isodow from dd) = week_days.day
ORDER BY date
) days
SQL
end
end

@ -0,0 +1,4 @@
class NonWorkingDay < ApplicationRecord
validates :name, :date, presence: true
validates :date, uniqueness: true
end

@ -26,23 +26,15 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require Rails.root.join('config/constants/project_activity')
module Projects::Activity
def self.included(base)
base.send :extend, ActivityScopes
end
module ActivityScopes
def register_latest_project_activity(on:, attribute:, chain: [])
Constants::ProjectActivity.register(on: on,
chain: chain,
attribute: attribute)
end
def latest_project_activity
@latest_project_activity ||=
Constants::ProjectActivity.registered.map do |params|
OpenProject::ProjectActivity.registered.map do |params|
build_latest_project_activity_for(on: params[:on].constantize,
chain: Array(params[:chain]).map(&:constantize),
attribute: params[:attribute])

@ -0,0 +1,34 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module Queries::Days
::Queries::Register.register(DayQuery) do
filter Filters::DatesIntervalFilter
filter Filters::WorkingFilter
end
end

@ -26,24 +26,28 @@
# See COPYRIGHT and LICENSE files for more details.
#++
# project, projects, global, user = nil
class Queries::Days::DayQuery < Queries::BaseQuery
def self.model
Day
end
def default_scope
Day.default
end
##
# This override is necessary, the dates interval filter needs to adjust the
# `from` clause of the query. To update the `from` clause, we reverse merge the filters,
# otherwise the `from` clause of the filter is ignored.
def apply_filters(scope)
filters.each do |filter|
scope = filter.scope.merge(scope)
end
class AuthorizationService
# @params
# ctrl - controller
# action - action
# @named params
# context - single project or array of projects - default nil
# global - global - default false
# user - user - default current user
def initialize(permission, context: nil, global: false, user: User.current)
@permission = permission
@context = context
@global = global
@user = user
scope
end
def call
@user.allowed_to?(@permission, @context, global: @global)
def results
super.reorder(date: :asc)
end
end

@ -0,0 +1,62 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
class Queries::Days::Filters::DatesIntervalFilter < Queries::Days::Filters::DayFilter
include Queries::Operators::DateRangeClauses
def type
:date
end
def self.key
:date
end
def from
from, to = values.map { |v| v.blank? ? nil : Date.parse(v) }
# Both from and to cannot be blank at this point
if from.nil?
from = to.at_beginning_of_month
end
if to.nil?
to = from.next_month.at_end_of_month
end
model.from_sql(from:, to:)
end
def type_strategy
@type_strategy ||= Queries::Filters::Strategies::DateInterval.new(self)
end
def connection
ActiveRecord::Base::connection
end
end

@ -0,0 +1,35 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
class Queries::Days::Filters::DayFilter < Queries::Filters::Base
self.model = Day
def human_name
model.human_attribute_name(name)
end
end

@ -0,0 +1,35 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
class Queries::Days::Filters::WorkingFilter < Queries::Days::Filters::DayFilter
include Queries::Filters::Shared::BooleanFilter
def self.key
:working
end
end

@ -73,7 +73,7 @@ class Queries::Filters::Base
def filter_instance_options
values = filter_params.map { |key| [key, send(key)] }
initial_options.merge(Hash[values])
initial_options.merge(values.to_h)
end
def human_name
@ -88,9 +88,7 @@ class Queries::Filters::Base
nil
end
def valid_values!
type_strategy.valid_values!
end
delegate :valid_values!, to: :type_strategy
def available?
true
@ -106,6 +104,7 @@ class Queries::Filters::Base
def scope
scope = model.where(where)
scope = scope.from(from) if from
scope = scope.joins(joins) if joins
scope = scope.left_outer_joins(left_outer_joins) if left_outer_joins
scope
@ -120,13 +119,17 @@ class Queries::Filters::Base
end
def self.all_for(context = nil)
create!(name: key, context: context)
create!(name: key, context:)
end
def where
operator_strategy.sql_for_field(values, self.class.model.table_name, self.class.key)
end
def from
nil
end
def joins
nil
end
@ -194,7 +197,7 @@ class Queries::Filters::Base
end
def validate_presence_of_values
if operator_strategy&.requires_value? && (values.nil? || values.reject(&:blank?).empty?)
if operator_strategy&.requires_value? && (values.nil? || values.compact_blank.empty?)
errors.add(:values, I18n.t('activerecord.errors.messages.blank'))
end
end

@ -0,0 +1,30 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
class Queries::WeekDays::WeekDayQuery < Queries::BaseQuery
end

@ -30,7 +30,7 @@ class Queries::WorkPackages::Filter::RelatableFilter < Queries::WorkPackages::Fi
include Queries::WorkPackages::Filter::FilterForWpMixin
def available?
User.current.allowed_to?(:manage_work_package_relations, nil, global: true)
User.current.allowed_to_globally?(:manage_work_package_relations)
end
def type

@ -123,25 +123,74 @@ class ::Query::Results
criteria = ::Query::SortCriteria.new query.sortable_columns
criteria.available_criteria = aliased_sorting_by_column_name
criteria.criteria = query.sort_criteria
criteria.map_each { |criteria| criteria.map { |raw| Arel.sql raw } }
criteria.map_each { |c| c.map { |raw| Arel.sql raw } }
end
def aliased_sorting_by_column_name
sorting_by_column_name = query.sortable_key_by_column_name
aliases = include_aliases
reflections = reflection_includes
sorting_by_column_name.each_with_object({}) do |(column_key, sortable), hash|
column_is_association = reflections.include?(column_key.to_sym)
columns_hash = columns_hash_for(column_is_association ? column_key : nil)
hash[column_key] = if column_is_association
alias_name = aliases[column_key.to_sym]
expand_association_columns(alias_name, sortable, columns_hash)
else
case_insensitive_condition(column_key, sortable, columns_hash)
end
end
end
reflection_includes.each do |inc|
sorting_by_column_name[inc.to_s] = Array(sorting_by_column_name[inc.to_s]).map do |column|
if column.respond_to?(:call)
column.call(aliases[inc])
else
"#{aliases[inc]}.#{column}"
end
end
##
# Returns the expanded association columns name
def expand_association_columns(alias_name, sortable, columns_hash)
Array(sortable).map do |column|
sort_condition = expand_association_column(column, alias_name)
case_insensitive_condition(column, sort_condition, columns_hash)
end
end
sorting_by_column_name
##
# Returns a single expanded association column name
def expand_association_column(column, alias_name)
if column.respond_to?(:call)
column.call(alias_name)
else
"#{alias_name}.#{column}"
end
end
##
# Return the columns hash for a given association
# If the association is nil, then return the WorkPackage.columns_hash
def columns_hash_for(association = nil)
if association
WorkPackage.reflections[association].klass.columns_hash
else
WorkPackage.columns_hash
end
end
##
# Return the case insensitive version for columns with a string type
def case_insensitive_condition(column_key, condition, columns_hash)
if columns_hash[column_key]&.type == :string
"LOWER(#{condition})"
elsif custom_field_type(column_key) == "string"
condition.map { |c| "LOWER(#{c})" }
else
condition
end
end
##
# Find the custom field type based on the column key
def custom_field_type(column_key)
(column = query.sortable_columns.detect { |c| c.name.to_s == column_key }) &&
column.respond_to?(:custom_field) &&
column.custom_field.field_format
end
# To avoid naming conflicts, joined tables are aliased if they are joined

@ -29,13 +29,11 @@
module ::Query::Results::GroupBy
# Returns the work package count by group or nil if query is not grouped
def work_package_count_by_group
@work_package_count_by_group ||= begin
if query.grouped?
r = group_counts_by_group
@work_package_count_by_group ||= if query.grouped?
r = group_counts_by_group
transform_group_keys(r)
end
end
transform_group_keys(r)
end
rescue ::ActiveRecord::StatementInvalid => e
raise ::Query::StatementInvalid.new(e.message)
end
@ -65,7 +63,7 @@ module ::Query::Results::GroupBy
def group_by_for_count
Array(query.group_by_statement).map { |statement| Arel.sql(statement) } +
[Arel.sql(group_by_sort(false))]
[Arel.sql(group_by_sort(order: false))]
end
def pluck_for_count
@ -137,33 +135,33 @@ module ::Query::Results::GroupBy
def transform_association_property_keys(association, groups)
ar_keys = association.class_name.constantize.find(groups.keys.compact)
groups.map do |key, value|
[ar_keys.detect { |ar_key| ar_key.id == key }, value]
end.to_h
groups.transform_keys do |key|
ar_keys.detect { |ar_key| ar_key.id == key }
end
end
# Returns the SQL sort order that should be prepended for grouping
def group_by_sort(order = true)
def group_by_sort(order: true)
if query.grouped? && (column = query.group_by_column)
aliases = include_aliases
alias_name = include_aliases[column.name]
columns_hash = columns_hash_for(alias_name ? column.association : nil)
Array(column.sortable).map do |s|
direction = order ? order_for_group_by(column) : nil
aliased_group_by_sort_order(aliases[column.name], s, direction)
aliased_group_by_sort_order(alias_name, s, columns_hash, direction)
end.join(', ')
end
end
def aliased_group_by_sort_order(alias_name, sortable, order = nil)
column = if alias_name && sortable.respond_to?(:call)
sortable.call(alias_name)
elsif alias_name
"#{alias_name}.#{sortable}"
def aliased_group_by_sort_order(alias_name, sortable, columns_hash, order = nil)
column = if alias_name
expand_association_column(sortable, alias_name)
else
sortable
end
column = case_insensitive_condition(sortable, column, columns_hash)
if order
column + " #{order} "
else

@ -29,6 +29,7 @@
class Setting < ApplicationRecord
extend CallbacksHelper
extend Aliases
extend MailSettings
ENCODINGS = %w(US-ASCII
windows-1250

@ -31,10 +31,13 @@ class Setting
# Shorthand to common setting aliases to avoid checking values
module Aliases
##
# Whether the application is configured to use or force SSL output
# for cookie storage et al.
def https?
Setting.protocol == 'https' || Rails.configuration.force_ssl
# Restore the previous Setting.protocol now replaced by https?
def protocol
if OpenProject::Configuration.secure_connection?
'https'
else
'http'
end
end
end
end

@ -0,0 +1,80 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
class Setting
module MailSettings
##
# Reload the currently configured mailer configuration
def reload_mailer_settings!
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.delivery_method = Setting.email_delivery_method if Setting.email_delivery_method
if Setting.email_delivery_method == :smtp
reload_smtp_settings!
end
rescue StandardError => e
Rails.logger.error "Unable to set ActionMailer settings (#{e.message}). " \
"Email sending will most likely NOT work."
end
private
# rubocop:disable Metrics/AbcSize
def reload_smtp_settings!
# Correct smtp settings when using authentication :none
authentication = Setting.smtp_authentication.try(:to_sym)
keys = %i[address port domain authentication user_name password]
if authentication == :none
# Rails Mailer will croak if passing :none as the authentication.
# Instead, it requires to be removed from its settings
ActionMailer::Base.smtp_settings.delete :user_name
ActionMailer::Base.smtp_settings.delete :password
ActionMailer::Base.smtp_settings.delete :authentication
keys = %i[address port domain]
end
keys.each do |setting|
value = Setting["smtp_#{setting}"]
if value.present?
ActionMailer::Base.smtp_settings[setting] = value
else
ActionMailer::Base.smtp_settings.delete setting
end
end
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = Setting.smtp_enable_starttls_auto?
ActionMailer::Base.smtp_settings[:ssl] = Setting.smtp_ssl?
Setting.smtp_openssl_verify_mode.tap do |mode|
ActionMailer::Base.smtp_settings[:openssl_verify_mode] = mode unless mode.nil?
end
end
# rubocop:enable Metrics/AbcSize
end
end

@ -164,8 +164,8 @@ class User < Principal
def reload(*args)
@name = nil
@projects_by_role = nil
@authorization_service = ::Authorization::UserAllowedService.new(self)
@project_role_cache = ::Users::ProjectRoleCache.new(self)
@user_allowed_service = nil
@project_role_cache = nil
super
end
@ -518,19 +518,19 @@ class User < Principal
Authorization.users(action, project).where.not(members: { id: nil })
end
def allowed_to?(action, context, options = {})
authorization_service.call(action, context, options).result
def allowed_to?(action, context, global: false)
user_allowed_service.call(action, context, global:).result
end
def allowed_to_in_project?(action, project, options = {})
authorization_service.call(action, project, options).result
def allowed_to_in_project?(action, project)
allowed_to?(action, project)
end
def allowed_to_globally?(action, options = {})
authorization_service.call(action, nil, options.merge(global: true)).result
def allowed_to_globally?(action)
allowed_to?(action, nil, global: true)
end
delegate :preload_projects_allowed_to, to: :authorization_service
delegate :preload_projects_allowed_to, to: :user_allowed_service
def reported_work_package_count
WorkPackage.on_active_project.with_author(self).visible.count
@ -648,8 +648,8 @@ class User < Principal
[skip_suffix_check, regexp]
end
def authorization_service
@authorization_service ||= ::Authorization::UserAllowedService.new(self, role_cache: project_role_cache)
def user_allowed_service
@user_allowed_service ||= ::Authorization::UserAllowedService.new(self, role_cache: project_role_cache)
end
def project_role_cache

@ -0,0 +1,6 @@
class WeekDay < ApplicationRecord
def name
day_names = I18n.t('date.day_names')
day_names[day % 7]
end
end

@ -0,0 +1,58 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module BasicData
class WeekDaySeeder < Seeder
def seed_data!
WeekDay.transaction do
days.each do |attributes|
WeekDay.create!(attributes)
end
end
end
def applicable?
WeekDay.none?
end
def not_applicable_message
'Skipping week days as there are already some configured'
end
def days
[
{ day: 1, working: true },
{ day: 2, working: true },
{ day: 3, working: true },
{ day: 4, working: true },
{ day: 5, working: true },
{ day: 6, working: false },
{ day: 7, working: false }
]
end
end
end

@ -31,6 +31,7 @@ module StandardSeeder
[
::BasicData::BuiltinRolesSeeder,
::BasicData::RoleSeeder,
::BasicData::WeekDaySeeder,
::StandardSeeder::BasicData::ActivitySeeder,
::BasicData::ColorSeeder,
::BasicData::ColorSchemeSeeder,

@ -41,12 +41,12 @@ class Authorization::UserAllowedService
# Context can be:
# * a project : returns true if user is allowed to do the specified action on this project
# * a group of projects : returns true if user is allowed on every project
# * nil with options[:global] set : check if user has at least one role allowed for this action,
# * nil with +global+ set to +true+ : check if user has at least one role allowed for this action,
# or falls back to Non Member / Anonymous permissions depending if the user is logged
def call(action, context, options = {})
if supported_context?(context, options)
def call(action, context, global: false)
if supported_context?(context, global:)
ServiceResult.new(success: true,
result: allowed_to?(action, context, options))
result: allowed_to?(action, context, global:))
else
ServiceResult.new(success: false,
result: false)
@ -61,25 +61,21 @@ class Authorization::UserAllowedService
attr_accessor :project_role_cache
def allowed_to?(action, context, options = {})
def allowed_to?(action, context, global: false)
action = normalize_action(action)
if context.nil? && options[:global]
allowed_to_globally?(action, options)
if context.nil? && global
allowed_to_globally?(action)
elsif context.is_a? Project
allowed_to_in_project?(action, context, options)
allowed_to_in_project?(action, context)
elsif context.respond_to?(:to_a)
context = context.to_a
# Authorize if user is authorized on every element of the array
context.present? && context.all? do |project|
allowed_to?(action, project, options)
end
allowed_to_in_all_projects?(action, context)
else
false
end
end
def allowed_to_in_project?(action, project, _options = {})
def allowed_to_in_project?(action, project)
if project_authorization_cache.cached?(action)
return project_authorization_cache.allowed?(action, project)
end
@ -97,9 +93,16 @@ class Authorization::UserAllowedService
has_authorized_role?(action, project)
end
# Authorize if user is authorized on every element of the array
def allowed_to_in_all_projects?(action, projects)
projects.present? && Array(projects).all? do |project|
allowed_to?(action, project)
end
end
# Is the user allowed to do the specified action on any project?
# See allowed_to? for the actions and valid options.
def allowed_to_globally?(action, _options = {})
# See allowed_to? for the action parameter description.
def allowed_to_globally?(action)
# Inactive users are never authorized
return false unless authorizable_user?
# Admin users are always authorized
@ -136,14 +139,14 @@ class Authorization::UserAllowedService
def normalize_action(action)
if action.is_a?(Hash) && action[:controller] && action[:controller].to_s.starts_with?('/')
action = action.dup
action[:controller] = action[:controller][1..-1]
action[:controller] = action[:controller][1..]
end
action
end
def supported_context?(context, options)
(context.nil? && options[:global]) ||
def supported_context?(context, global:)
(context.nil? && global) ||
context.is_a?(Project) ||
(!context.nil? && context.respond_to?(:to_a))
end

@ -18,15 +18,13 @@ module Ldap
raise NotImplementedError
end
# rubocop:disable Metrics/AbcSize
protected
def synchronize_user(user, ldap_con)
Rails.logger.debug { "[LDAP user sync] Synchronizing user #{user.login}." }
update_attributes = user_attributes(user.login, ldap_con)
if update_attributes.nil? && user.persisted?
Rails.logger.info { "Could not find user #{user.login} in #{ldap.name}. Locking the user." }
user.update_column(:status, Principal.statuses[:locked])
end
lock_user!(user) if update_attributes.nil? && user.persisted?
return unless update_attributes
if user.new_record?
@ -35,7 +33,6 @@ module Ldap
try_to_update(user, update_attributes)
end
end
# rubocop:enable Metrics/AbcSize
# Try to create the user from attributes
def try_to_update(user, attrs)
@ -44,8 +41,7 @@ module Ldap
.call(attrs)
if call.success?
# Ensure the user is activated
call.result.update_column(:status, Principal.statuses[:active])
activate_user!(user)
Rails.logger.info { "[LDAP user sync] User '#{call.result.login}' updated." }
else
Rails.logger.error { "[LDAP user sync] User '#{user.login}' could not be updated: #{call.message}" }
@ -64,6 +60,32 @@ module Ldap
end
end
##
# Locks the given user if this is what the sync service should do.
def lock_user!(user)
if OpenProject::Configuration.ldap_users_sync_status?
Rails.logger.info { "Could not find user #{user.login} in #{ldap.name}. Locking the user." }
user.update_column(:status, Principal.statuses[:locked])
else
Rails.logger.info do
"Could not find user #{user.login} in #{ldap.name}. Ignoring due to ldap_users_sync_status being unset"
end
end
end
##
# Activates the given user if this is what the sync service should do.
def activate_user!(user)
if OpenProject::Configuration.ldap_users_sync_status?
Rails.logger.info { "Activating #{user.login} due to it being synced from LDAP #{ldap.name}." }
user.update_column(:status, Principal.statuses[:active])
else
Rails.logger.info do
"Would activate #{user.login} through #{ldap.name} but ignoring due to ldap_users_sync_status being unset."
end
end
end
##
# Get the user attributes of a single matching LDAP entry.
#

@ -47,7 +47,6 @@ See COPYRIGHT and LICENSE files for more details.
<%= t(:label_example) %>: <%= @guessed_host %>
</span>
</div>
<div class="form--field"><%= setting_select :protocol, [['HTTP', 'http'], ['HTTPS', 'https']], container_class: '-xslim' %></div>
<div class="form--field"><%= setting_check_box :cache_formatted_text %></div>
<div class="form--field"><%= setting_check_box :feeds_enabled, size: 6 %></div>
<div class="form--field"><%= setting_text_field :feeds_limit, size: 6, container_class: '-xslim' %></div>

@ -17,7 +17,7 @@
<% end %>
<div class="widget-box--blocks--buttons">
<% if User.current.allowed_to?(:add_project, nil, global: true) %>
<% if User.current.allowed_to_globally?(:add_project) %>
<%= link_to new_project_path,
{ class: 'button -alt-highlight',
aria: {label: t(:label_project_new)},

@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details.
<% html_title(t(:label_project_plural)) -%>
<%= toolbar title: t(:label_project_plural), html: { class: '-with-dropdown' } do %>
<% if User.current.allowed_to?(:add_project, nil, global: true) %>
<% if User.current.allowed_to_globally?(:add_project) %>
<li class="toolbar-item">
<%= link_to new_project_path,
{ class: 'button -alt-highlight',

@ -88,15 +88,15 @@ class ApplicationJob < ::ActiveJob::Base
# Since the email configuration is now done in the web app, we need to
# make sure that any changes to the configuration is correctly picked up
# by the background jobs at runtime.
def reload_mailer_configuration!
OpenProject::Configuration.reload_mailer_configuration!
def reload_mailer_settings!
Setting.reload_mailer_settings!
end
private
def clean_context
with_clean_request_store do
reload_mailer_configuration!
reload_mailer_settings!
yield
end

@ -39,24 +39,12 @@ ActiveSupport::Deprecation.silenced =
(Rails.env.test? && ENV['CI'])
if defined?(Bundler)
# lib directory has to be added to the load path so that
# the open_project/plugins files can be found (places under lib).
# Now it would be possible to remove that and use require with
# lib included but some plugins already use
#
# require 'open_project/plugins'
#
# to ensure the code to be loaded. So we provide a compatibility
# layer here. One might remove this later.
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
require 'open_project/plugins'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups(:opf_plugins))
end
require_relative '../lib/open_project/configuration'
require_relative '../lib_static/open_project/configuration'
module OpenProject
class Application < Rails::Application
@ -97,12 +85,15 @@ module OpenProject
# http://stackoverflow.com/questions/4590229
config.middleware.use Rack::TempfileReaper
config.autoloader = :zeitwerk
# Custom directories with classes and modules you want to be autoloadable.
config.enable_dependency_loading = true
config.paths.add Rails.root.join('lib').to_s, eager_load: true
config.paths.add Rails.root.join('lib/constraints').to_s, eager_load: true
# Constants in lib_static should only be loaded once and never be unloaded.
# That directory contains configurations and patches to rails core functionality.
config.autoload_once_paths << Rails.root.join('lib_static').to_s
# Use our own error rendering for prettier error pages
config.exceptions_app = routes
@ -185,8 +176,6 @@ module OpenProject
# This allows for setting the root either via config file or via environment variable.
config.action_controller.relative_url_root = OpenProject::Configuration['rails_relative_url_root']
OpenProject::Configuration.configure_cache(config)
config.active_job.queue_adapter = :delayed_job
config.action_controller.asset_host = OpenProject::Configuration::AssetHost.value
@ -196,6 +185,9 @@ module OpenProject
config.log_level = OpenProject::Configuration['log_level'].to_sym
# Enable the Rails 7 cache format
config.active_support.cache_format_version = 7.0
def self.root_url
Setting.protocol + "://" + Setting.host_name
end

@ -0,0 +1,41 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
module OpenProject
module ProjectActivity
class << self
def register(on:, attribute:, chain: [])
@registered ||= Set.new
@registered << { on: on,
chain: chain,
attribute: attribute }
end
attr_reader :registered
end
end
end

@ -526,6 +526,12 @@ Settings::Definition.define do
default: false,
writable: false
# Update users' status through the synchronization job
add :ldap_users_sync_status,
format: :boolean,
default: true,
writable: false
add :ldap_tls_options,
default: {},
writable: false
@ -638,10 +644,6 @@ Settings::Definition.define do
add :plain_text_mail,
default: false
add :protocol,
default: "http",
allowed: %w[http https]
add :project_gantt_query,
default: nil,
format: :string
@ -662,8 +664,17 @@ Settings::Definition.define do
default: '',
writable: false
# Assume we're running in an TLS terminated connection.
# This does not affect HSTS, use +rails_force_ssl+ for that.
add :https,
format: :boolean,
default: Rails.env.production?,
writable: false
# Enable HTTPS and HSTS
add :rails_force_ssl,
default: false,
format: :boolean,
default: Rails.env.production?,
writable: false
add :registration_footer,

@ -26,8 +26,10 @@
# See COPYRIGHT and LICENSE files for more details.
#++
# Loads the core plugins located in lib/plugins
Dir.glob(File.join(Rails.root, 'lib/plugins/*')).sort.each do |directory|
# TODO: check if this can be postponed and if some plugins can make use of the ActiveSupport.on_load hooks
# Loads the core plugins located in lib_static/plugins
Dir.glob(Rails.root.join('lib_static/plugins/*')).each do |directory|
if File.directory?(directory)
lib = File.join(directory, 'lib')

@ -30,5 +30,6 @@
# As initializers and other parts of the boot sequence rely on calls accessing
# the DB, the null db gem is used to fake the existence of a database in cases where
# the db has not been created yet.
require Rails.root.join('config/constants/open_project/null_db_fallback')
OpenProject::NullDbFallback.fallback

@ -26,10 +26,12 @@
# See COPYRIGHT and LICENSE files for more details.
#++
# Do not place any patches within this file. Add a file to lib/open_project/patches
require 'open_project/patches'
Rails.application.reloader.to_prepare do
# Do not place any patches within this file. Add a file to lib/open_project/patches
require 'open_project/patches'
# Whatever ruby file is placed in lib/open_project/patches is required
Dir.glob(File.expand_path('../../lib/open_project/patches/*.rb', __dir__)).each do |path|
require path
# Whatever ruby file is placed in lib/open_project/patches is required
Dir.glob(File.expand_path('../../lib/open_project/patches/*.rb', __dir__)).each do |path|
require path
end
end

@ -26,6 +26,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require_relative '../constants/open_project/project_activity'
Rails.application.reloader.to_prepare do
OpenProject::Activity.map do |activity|
activity.register :work_packages, class_name: '::Activities::WorkPackageActivityProvider'
@ -38,21 +40,21 @@ Rails.application.reloader.to_prepare do
default: false
end
Project.register_latest_project_activity on: 'WorkPackage',
attribute: :updated_at
OpenProject::ProjectActivity.register on: 'WorkPackage',
attribute: :updated_at
Project.register_latest_project_activity on: 'News',
attribute: :updated_at
OpenProject::ProjectActivity.register on: 'News',
attribute: :updated_at
Project.register_latest_project_activity on: 'Changeset',
chain: 'Repository',
attribute: :committed_on
OpenProject::ProjectActivity.register on: 'Changeset',
chain: 'Repository',
attribute: :committed_on
Project.register_latest_project_activity on: 'WikiContent',
chain: %w(Wiki WikiPage),
attribute: :updated_at
OpenProject::ProjectActivity.register on: 'WikiContent',
chain: %w(Wiki WikiPage),
attribute: :updated_at
Project.register_latest_project_activity on: 'Message',
chain: 'Forum',
attribute: :updated_at
OpenProject::ProjectActivity.register on: 'Message',
chain: 'Forum',
attribute: :updated_at
end

@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
if OpenProject::Configuration.override_bcrypt_cost_factor?
if OpenProject::Configuration.override_bcrypt_cost_factor.present?
cost_factor = OpenProject::Configuration.override_bcrypt_cost_factor.to_i
current = BCrypt::Engine.cost

@ -0,0 +1,32 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
# Be sure to restart your server when you modify this file.
OpenProject::Application.configure do
OpenProject::Configuration.configure_cache(config)
end

@ -25,9 +25,12 @@
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Grape
class Endpoint
include ::API::V3::Utilities::PathHelper
Rails.application.reloader.to_prepare do
# rubocop:disable Lint/ConstantDefinitionInBlock
module Grape
class Endpoint
include ::API::V3::Utilities::PathHelper
end
end
# rubocop:enable Lint/ConstantDefinitionInBlock
end

@ -44,7 +44,7 @@ Redmine::MenuManager.map :top_menu do |menu|
caption: I18n.t('label_work_package_plural'),
if: Proc.new {
(User.current.logged? || !Setting.login_required?) &&
User.current.allowed_to?(:view_work_packages, nil, global: true)
User.current.allowed_to_globally?(:view_work_packages)
}
menu.push :news,
{ controller: '/news', project_id: nil, action: 'index' },
@ -52,7 +52,7 @@ Redmine::MenuManager.map :top_menu do |menu|
caption: I18n.t('label_news_plural'),
if: Proc.new {
(User.current.logged? || !Setting.login_required?) &&
User.current.allowed_to?(:view_news, nil, global: true)
User.current.allowed_to_globally?(:view_news)
}
menu.push :help,
OpenProject::Static::Links.help_link,

@ -1,10 +0,0 @@
OpenProject::Application.configure do
config.after_initialize do
# settings table may not exist when you run db:migrate at installation
# time, so just ignore this block when that happens.
if Setting.settings_table_exists_yet?
OpenProject::Configuration.migrate_mailer_configuration!
OpenProject::Configuration.reload_mailer_configuration!
end
end
end

@ -26,7 +26,9 @@
# See COPYRIGHT and LICENSE files for more details.
#++
if OpenProject::Configuration.disabled_modules.any?
to_disable = OpenProject::Configuration.disabled_modules
OpenProject::Plugins::ModuleHandler.disable_modules(to_disable)
Rails.application.config.after_initialize do
if OpenProject::Configuration.disabled_modules.any?
to_disable = OpenProject::Configuration.disabled_modules
OpenProject::Plugins::ModuleHandler.disable_modules(to_disable)
end
end

@ -26,333 +26,333 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require 'open_project/access_control'
OpenProject::AccessControl.map do |map|
map.project_module nil, order: 100 do
map.permission :add_project,
{ projects: %i[new] },
require: :loggedin,
global: true,
contract_actions: { projects: %i[create] }
map.permission :create_backup,
{
Rails.application.reloader.to_prepare do
OpenProject::AccessControl.map do |map|
map.project_module nil, order: 100 do
map.permission :add_project,
{ projects: %i[new] },
require: :loggedin,
global: true,
contract_actions: { projects: %i[create] }
map.permission :create_backup,
{
admin: %i[index],
'admin/backups': %i[delete_token perform_token_reset reset_token show]
},
require: :loggedin,
global: true,
enabled: -> { OpenProject::Configuration.backup_enabled? }
map.permission :manage_user,
{
users: %i[index show new create edit update resend_invitation],
'users/memberships': %i[create update destroy],
admin: %i[index]
},
require: :loggedin,
global: true,
contract_actions: { users: %i[create read update] }
map.permission :manage_placeholder_user,
{
placeholder_users: %i[index show new create edit update deletion_info destroy],
'placeholder_users/memberships': %i[create update destroy],
admin: %i[index]
},
require: :loggedin,
global: true,
contract_actions: { placeholder_users: %i[create read update] }
map.permission :view_project,
{ projects: [:show],
activities: [:index] },
public: true
map.permission :search_project,
{ search: :index },
public: true
map.permission :edit_project,
{
'projects/settings/general': %i[show],
'projects/settings/storage': %i[show],
'projects/templated': %i[create destroy],
'projects/identifier': %i[show update]
},
require: :member,
contract_actions: { projects: %i[update] }
map.permission :select_project_modules,
{
'projects/settings/modules': %i[show update]
},
require: :member
map.permission :manage_members,
{ members: %i[index new create update destroy autocomplete_for_member] },
require: :member,
dependencies: :view_members,
contract_actions: { members: %i[create update destroy] }
map.permission :view_members,
{ members: [:index] },
contract_actions: { members: %i[read] }
map.permission :manage_versions,
{
'projects/settings/versions': [:show],
versions: %i[new create edit update close_completed destroy]
},
require: :member
map.permission :manage_types,
{
'projects/settings/types': %i[show update]
},
require: :member
map.permission :select_custom_fields,
{
'projects/settings/custom_fields': %i[show update]
},
require: :member
map.permission :add_subprojects,
{ projects: %i[new] },
require: :member
map.permission :copy_projects,
{
projects: %i[copy]
},
require: :member,
contract_actions: { projects: %i[copy] }
end
map.project_module :work_package_tracking, order: 90 do |wpt|
wpt.permission :view_work_packages,
{
versions: %i[index show status_by],
journals: %i[index diff],
work_packages: %i[show index],
work_packages_api: [:get],
'work_packages/reports': %i[report report_details]
},
contract_actions: { work_packages: %i[read] }
wpt.permission :add_work_packages,
{}
wpt.permission :edit_work_packages,
{
'work_packages/bulk': %i[edit update]
},
require: :member,
dependencies: :view_work_packages
wpt.permission :move_work_packages,
{ 'work_packages/moves': %i[new create] },
require: :loggedin,
dependencies: :view_work_packages
wpt.permission :add_work_package_notes,
{
# FIXME: Although the endpoint is removed, the code checking whether a user
# is eligible to add work packages through the API still seems to rely on this.
journals: [:new]
},
dependencies: :view_work_packages
wpt.permission :edit_work_package_notes,
{},
require: :loggedin,
dependencies: :view_work_packages
wpt.permission :edit_own_work_package_notes,
{},
require: :loggedin,
dependencies: :view_work_packages
# WorkPackage categories
wpt.permission :manage_categories,
{
'projects/settings/categories': [:show],
categories: %i[new create edit update destroy]
},
require: :member
wpt.permission :export_work_packages,
{
work_packages: %i[index all]
},
dependencies: :view_work_packages
wpt.permission :delete_work_packages,
{
work_packages: :destroy,
'work_packages/bulk': :destroy
},
require: :member,
dependencies: :view_work_packages
wpt.permission :manage_work_package_relations,
{
work_package_relations: %i[create destroy]
},
dependencies: :view_work_packages
wpt.permission :manage_subtasks,
{},
dependencies: :view_work_packages
# Queries
wpt.permission :manage_public_queries,
{},
require: :member
wpt.permission :save_queries,
{},
require: :loggedin,
dependencies: :view_work_packages
# Watchers
wpt.permission :view_work_package_watchers,
{},
dependencies: :view_work_packages
wpt.permission :add_work_package_watchers,
{},
dependencies: :view_work_packages
wpt.permission :delete_work_package_watchers,
{},
dependencies: :view_work_packages
wpt.permission :assign_versions,
{},
dependencies: :view_work_packages
# A user having the following permission can become assignee and/or responsible of a work package.
# This is a passive permission in the sense that a user having the permission isn't eligible to perform
# actions but rather to have actions taken together with him/her.
wpt.permission :work_package_assigned,
{},
require: :member,
contract_actions: { work_packages: %i[assigned] },
grant_to_admin: false
end
map.project_module :news do |news|
news.permission :view_news,
{ news: %i[index show] },
public: true
news.permission :manage_news,
{
news: %i[new create edit update destroy preview],
'news/comments': [:destroy]
},
require: :member
news.permission :comment_news,
{ 'news/comments': :create }
end
map.project_module :wiki do |wiki|
wiki.permission :view_wiki_pages,
{ wiki: %i[index show special date_index] }
wiki.permission :list_attachments,
{ wiki: :list_attachments },
require: :member
wiki.permission :manage_wiki,
{ wikis: %i[edit destroy] },
require: :member
wiki.permission :manage_wiki_menu,
{ wiki_menu_items: %i[edit update select_main_menu_item replace_main_menu_item] },
require: :member
wiki.permission :rename_wiki_pages,
{ wiki: :rename },
require: :member
wiki.permission :change_wiki_parent_page,
{ wiki: %i[edit_parent_page update_parent_page] },
require: :member
wiki.permission :delete_wiki_pages,
{ wiki: :destroy },
require: :member
wiki.permission :export_wiki_pages,
{ wiki: [:export] }
wiki.permission :view_wiki_edits,
{ wiki: %i[history diff annotate] }
wiki.permission :edit_wiki_pages,
{ wiki: %i[edit update preview add_attachment new new_child create] }
wiki.permission :delete_wiki_pages_attachments,
{}
require: :loggedin,
global: true,
enabled: -> { OpenProject::Configuration.backup_enabled? }
map.permission :manage_user,
{
users: %i[index show new create edit update resend_invitation],
'users/memberships': %i[create update destroy],
admin: %i[index]
},
require: :loggedin,
global: true,
contract_actions: { users: %i[create read update] }
map.permission :manage_placeholder_user,
{
placeholder_users: %i[index show new create edit update deletion_info destroy],
'placeholder_users/memberships': %i[create update destroy],
admin: %i[index]
},
require: :loggedin,
global: true,
contract_actions: { placeholder_users: %i[create read update] }
map.permission :view_project,
{ projects: [:show],
activities: [:index] },
public: true
wiki.permission :protect_wiki_pages,
{ wiki: :protect },
require: :member
end
map.permission :search_project,
{ search: :index },
public: true
map.project_module :repository do |repo|
repo.permission :browse_repository,
{ repositories: %i[show browse entry annotate changes diff stats graph] }
map.permission :edit_project,
{
'projects/settings/general': %i[show],
'projects/settings/storage': %i[show],
'projects/templated': %i[create destroy],
'projects/identifier': %i[show update]
},
require: :member,
contract_actions: { projects: %i[update] }
map.permission :select_project_modules,
{
'projects/settings/modules': %i[show update]
},
require: :member
repo.permission :commit_access,
{}
map.permission :manage_members,
{ members: %i[index new create update destroy autocomplete_for_member] },
require: :member,
dependencies: :view_members,
contract_actions: { members: %i[create update destroy] }
map.permission :view_members,
{ members: [:index] },
contract_actions: { members: %i[read] }
map.permission :manage_versions,
{
'projects/settings/versions': [:show],
versions: %i[new create edit update close_completed destroy]
},
require: :member
repo.permission :manage_repository,
{
repositories: %i[edit create update committers destroy_info destroy],
'projects/settings/repository': :show
},
require: :member
map.permission :manage_types,
{
'projects/settings/types': %i[show update]
},
require: :member
repo.permission :view_changesets,
{ repositories: %i[show revisions revision] }
map.permission :select_custom_fields,
{
'projects/settings/custom_fields': %i[show update]
},
require: :member
repo.permission :view_commit_author_statistics,
{}
end
map.permission :add_subprojects,
{ projects: %i[new] },
require: :member
map.project_module :forums do |forum|
forum.permission :manage_forums,
{ forums: %i[new create edit update move destroy] },
map.permission :copy_projects,
{
projects: %i[copy]
},
require: :member,
contract_actions: { projects: %i[copy] }
end
map.project_module :work_package_tracking, order: 90 do |wpt|
wpt.permission :view_work_packages,
{
versions: %i[index show status_by],
journals: %i[index diff],
work_packages: %i[show index],
work_packages_api: [:get],
'work_packages/reports': %i[report report_details]
},
contract_actions: { work_packages: %i[read] }
wpt.permission :add_work_packages,
{}
wpt.permission :edit_work_packages,
{
'work_packages/bulk': %i[edit update]
},
require: :member,
dependencies: :view_work_packages
wpt.permission :move_work_packages,
{ 'work_packages/moves': %i[new create] },
require: :loggedin,
dependencies: :view_work_packages
wpt.permission :add_work_package_notes,
{
# FIXME: Although the endpoint is removed, the code checking whether a user
# is eligible to add work packages through the API still seems to rely on this.
journals: [:new]
},
dependencies: :view_work_packages
wpt.permission :edit_work_package_notes,
{},
require: :loggedin,
dependencies: :view_work_packages
wpt.permission :edit_own_work_package_notes,
{},
require: :loggedin,
dependencies: :view_work_packages
# WorkPackage categories
wpt.permission :manage_categories,
{
'projects/settings/categories': [:show],
categories: %i[new create edit update destroy]
},
require: :member
forum.permission :view_messages,
{ forums: %i[index show],
messages: [:show] },
public: true
wpt.permission :export_work_packages,
{
work_packages: %i[index all]
},
dependencies: :view_work_packages
wpt.permission :delete_work_packages,
{
work_packages: :destroy,
'work_packages/bulk': :destroy
},
require: :member,
dependencies: :view_work_packages
wpt.permission :manage_work_package_relations,
{
work_package_relations: %i[create destroy]
},
dependencies: :view_work_packages
wpt.permission :manage_subtasks,
{},
dependencies: :view_work_packages
# Queries
wpt.permission :manage_public_queries,
{},
require: :member
forum.permission :add_messages,
{ messages: %i[new create reply quote preview] }
wpt.permission :save_queries,
{},
require: :loggedin,
dependencies: :view_work_packages
# Watchers
wpt.permission :view_work_package_watchers,
{},
dependencies: :view_work_packages
wpt.permission :add_work_package_watchers,
{},
dependencies: :view_work_packages
wpt.permission :delete_work_package_watchers,
{},
dependencies: :view_work_packages
wpt.permission :assign_versions,
{},
dependencies: :view_work_packages
# A user having the following permission can become assignee and/or responsible of a work package.
# This is a passive permission in the sense that a user having the permission isn't eligible to perform
# actions but rather to have actions taken together with him/her.
wpt.permission :work_package_assigned,
{},
require: :member,
contract_actions: { work_packages: %i[assigned] },
grant_to_admin: false
end
map.project_module :news do |news|
news.permission :view_news,
{ news: %i[index show] },
public: true
news.permission :manage_news,
{
news: %i[new create edit update destroy preview],
'news/comments': [:destroy]
},
require: :member
news.permission :comment_news,
{ 'news/comments': :create }
end
map.project_module :wiki do |wiki|
wiki.permission :view_wiki_pages,
{ wiki: %i[index show special date_index] }
wiki.permission :list_attachments,
{ wiki: :list_attachments },
require: :member
wiki.permission :manage_wiki,
{ wikis: %i[edit destroy] },
require: :member
wiki.permission :manage_wiki_menu,
{ wiki_menu_items: %i[edit update select_main_menu_item replace_main_menu_item] },
require: :member
wiki.permission :rename_wiki_pages,
{ wiki: :rename },
require: :member
wiki.permission :change_wiki_parent_page,
{ wiki: %i[edit_parent_page update_parent_page] },
require: :member
wiki.permission :delete_wiki_pages,
{ wiki: :destroy },
require: :member
wiki.permission :export_wiki_pages,
{ wiki: [:export] }
wiki.permission :view_wiki_edits,
{ wiki: %i[history diff annotate] }
wiki.permission :edit_wiki_pages,
{ wiki: %i[edit update preview add_attachment new new_child create] }
wiki.permission :delete_wiki_pages_attachments,
{}
wiki.permission :protect_wiki_pages,
{ wiki: :protect },
require: :member
end
map.project_module :repository do |repo|
repo.permission :browse_repository,
{ repositories: %i[show browse entry annotate changes diff stats graph] }
repo.permission :commit_access,
{}
repo.permission :manage_repository,
{
repositories: %i[edit create update committers destroy_info destroy],
'projects/settings/repository': :show
},
require: :member
repo.permission :view_changesets,
{ repositories: %i[show revisions revision] }
repo.permission :view_commit_author_statistics,
{}
end
map.project_module :forums do |forum|
forum.permission :manage_forums,
{ forums: %i[new create edit update move destroy] },
require: :member
forum.permission :view_messages,
{ forums: %i[index show],
messages: [:show] },
public: true
forum.permission :add_messages,
{ messages: %i[new create reply quote preview] }
forum.permission :edit_messages,
{ messages: %i[edit update preview] },
require: :member
forum.permission :edit_messages,
{ messages: %i[edit update preview] },
require: :member
forum.permission :edit_own_messages,
{ messages: %i[edit update preview] },
require: :loggedin
forum.permission :edit_own_messages,
{ messages: %i[edit update preview] },
require: :loggedin
forum.permission :delete_messages,
{ messages: :destroy },
require: :member
forum.permission :delete_messages,
{ messages: :destroy },
require: :member
forum.permission :delete_own_messages,
{ messages: :destroy },
require: :loggedin
end
forum.permission :delete_own_messages,
{ messages: :destroy },
require: :loggedin
map.project_module :activity
end
map.project_module :activity
end

@ -14,30 +14,34 @@ if OpenProject::Configuration.web_workers >= 2
term_on_timeout: 1 # shut down worker (gracefully) right away on timeout to be restarted
)
# remove default logger (logging uninteresting extra info with each not timed out request)
Rack::Timeout.unregister_state_change_observer(:logger)
Rails.application.config.after_initialize do
# remove default logger (logging uninteresting extra info with each not timed out request)
Rack::Timeout.unregister_state_change_observer(:logger)
Rack::Timeout.register_state_change_observer(:wait_timeout_logger) do |env|
details = env[Rack::Timeout::ENV_INFO_KEY]
Rack::Timeout.register_state_change_observer(:wait_timeout_logger) do |env|
details = env[Rack::Timeout::ENV_INFO_KEY]
if details.state == :timed_out && details.wait.present?
::OpenProject.logger.error "Request timed out waiting to be served!"
if details.state == :timed_out && details.wait.present?
::OpenProject.logger.error "Request timed out waiting to be served!"
end
end
end
# The timeout itself is already reported so no need to
# report the generic internal server error too as it doesn't
# add any more information. Even worse, it's not immediately
# clear that the two reports are related.
module SuppressInternalErrorReportOnTimeout
def op_handle_error(message_or_exception, context = {})
return if request && request.env[Rack::Timeout::ENV_INFO_KEY].try(:state) == :timed_out
super
# The timeout itself is already reported so no need to
# report the generic internal server error too as it doesn't
# add any more information. Even worse, it's not immediately
# clear that the two reports are related.
# rubocop:disable Lint/ConstantDefinitionInBlock
module SuppressInternalErrorReportOnTimeout
def op_handle_error(message_or_exception, context = {})
return if request && request.env[Rack::Timeout::ENV_INFO_KEY].try(:state) == :timed_out
super
end
end
end
OpenProjectErrorHelper.prepend SuppressInternalErrorReportOnTimeout
OpenProjectErrorHelper.prepend SuppressInternalErrorReportOnTimeout
end
# rubocop:enable Lint/ConstantDefinitionInBlock
else
Rails.logger.debug { "Not enabling Rack::Timeout since we are not running in cluster mode with at least 2 workers" }
end

@ -28,7 +28,8 @@
# Register interceptors defined in app/mailers/user_mailer.rb
# Do this here, so they aren't registered multiple times due to reloading in development mode.
ApplicationMailer.register_interceptor Interceptors::DefaultHeaders
# following needs to be the last interceptor
ApplicationMailer.register_interceptor Interceptors::DoNotSendMailsWithoutRecipient
Rails.application.reloader.to_prepare do
ApplicationMailer.register_interceptor Interceptors::DefaultHeaders
# following needs to be the last interceptor
ApplicationMailer.register_interceptor Interceptors::DoNotSendMailsWithoutRecipient
end

@ -1,75 +1,79 @@
SecureHeaders::Configuration.default do |config|
config.cookies = {
secure: true,
httponly: true
}
# Add "; preload" and submit the site to hstspreload.org for best protection.
config.hsts = "max-age=#{20.years.to_i}; includeSubdomains"
config.x_frame_options = "SAMEORIGIN"
config.x_content_type_options = "nosniff"
config.x_xss_protection = "1; mode=block"
config.x_permitted_cross_domain_policies = "none"
config.referrer_policy = "origin-when-cross-origin"
# rubocop:disable Lint/PercentStringArray
Rails.application.config.after_initialize do
SecureHeaders::Configuration.default do |config|
config.cookies = {
secure: true,
httponly: true
}
# Add "; preload" and submit the site to hstspreload.org for best protection.
config.hsts = "max-age=#{20.years.to_i}; includeSubdomains"
config.x_frame_options = "SAMEORIGIN"
config.x_content_type_options = "nosniff"
config.x_xss_protection = "1; mode=block"
config.x_permitted_cross_domain_policies = "none"
config.referrer_policy = "origin-when-cross-origin"
# Valid for assets
assets_src = ["'self'"]
asset_host = OpenProject::Configuration.rails_asset_host
assets_src << asset_host if asset_host.present?
# Valid for assets
assets_src = ["'self'"]
asset_host = OpenProject::Configuration.rails_asset_host
assets_src << asset_host if asset_host.present?
# Valid for iframes
frame_src = %w['self' https://player.vimeo.com]
frame_src << OpenProject::Configuration[:security_badge_url]
# Valid for iframes
frame_src = %w['self' https://player.vimeo.com]
frame_src << OpenProject::Configuration[:security_badge_url]
# Default src
default_src = %w('self') + OpenProject::Configuration.remote_storage_hosts
# Default src
default_src = %w('self') + OpenProject::Configuration.remote_storage_hosts
# Allow requests to CLI in dev mode
connect_src = default_src + [OpenProject::Configuration.enterprise_trial_creation_host]
# Allow requests to CLI in dev mode
connect_src = default_src + [OpenProject::Configuration.enterprise_trial_creation_host]
if OpenProject::Configuration.sentry_frontend_dsn.present?
connect_src += [OpenProject::Configuration.sentry_host]
end
if OpenProject::Configuration.sentry_frontend_dsn.present?
connect_src += [OpenProject::Configuration.sentry_host]
end
# Add proxy configuration for Angular CLI to csp
if FrontendAssetHelper.assets_proxied?
proxied = ['ws://localhost:*', 'http://localhost:*', FrontendAssetHelper.cli_proxy]
connect_src += proxied
assets_src += proxied
end
# Add proxy configuration for Angular CLI to csp
if FrontendAssetHelper.assets_proxied?
proxied = ['ws://localhost:*', 'http://localhost:*', FrontendAssetHelper.cli_proxy]
connect_src += proxied
assets_src += proxied
end
# Allow to extend the script-src in specific situations
script_src = assets_src
# Allow to extend the script-src in specific situations
script_src = assets_src
# Allow unsafe-eval for rack-mini-profiler
if Rails.env.development? && ENV['OPENPROJECT_RACK_PROFILER_ENABLED']
script_src += %w('unsafe-eval')
end
# Allow unsafe-eval for rack-mini-profiler
if Rails.env.development? && ENV.fetch('OPENPROJECT_RACK_PROFILER_ENABLED', false)
script_src += %w('unsafe-eval')
end
config.csp = {
preserve_schemes: true,
config.csp = {
preserve_schemes: true,
# Fallback when no value is defined
default_src: default_src,
# Allowed uri in <base> tag
base_uri: %w('self'),
# Fallback when no value is defined
default_src: default_src,
# Allowed uri in <base> tag
base_uri: %w('self'),
# Allow fonts from self, asset host, or DATA uri
font_src: assets_src + %w(data:),
# Form targets can only be self
form_action: default_src,
# Allow iframe from vimeo (welcome video)
frame_src: frame_src + %w('self'),
frame_ancestors: %w('self'),
# Allow images from anywhere including data urls and blobs (used in resizing)
img_src: %w(* data: blob:),
# Allow scripts from self
script_src: script_src,
# Allow unsafe-inline styles
style_src: assets_src + %w('unsafe-inline'),
# Allow object-src from Release API
object_src: [OpenProject::Configuration[:security_badge_url]],
# Allow fonts from self, asset host, or DATA uri
font_src: assets_src + %w(data:),
# Form targets can only be self
form_action: default_src,
# Allow iframe from vimeo (welcome video)
frame_src: frame_src + %w('self'),
frame_ancestors: %w('self'),
# Allow images from anywhere including data urls and blobs (used in resizing)
img_src: %w(* data: blob:),
# Allow scripts from self
script_src: script_src,
# Allow unsafe-inline styles
style_src: assets_src + %w('unsafe-inline'),
# Allow object-src from Release API
object_src: [OpenProject::Configuration[:security_badge_url]],
# Connect sources for CLI in dev mode
connect_src: connect_src
}
# Connect sources for CLI in dev mode
connect_src: connect_src
}
end
end
# rubocop:enable Lint/PercentStringArray

@ -1,87 +1,89 @@
if OpenProject::Logging::SentryLogger.enabled?
Sentry.init do |config|
config.dsn = OpenProject::Logging::SentryLogger.sentry_dsn
config.breadcrumbs_logger = OpenProject::Configuration.sentry_breadcrumb_loggers.map(&:to_sym)
# Output debug info for sentry
config.before_send = lambda do |event, hint|
Rails.logger.debug do
payload_sizes = event.to_json_compatible.transform_values { |v| JSON.generate(v).bytesize }.inspect
"[Sentry] will send event #{hint}. Payload sizes are #{payload_sizes.inspect}"
Rails.application.config.after_initialize do
if OpenProject::Logging::SentryLogger.enabled?
Sentry.init do |config|
config.dsn = OpenProject::Logging::SentryLogger.sentry_dsn
config.breadcrumbs_logger = OpenProject::Configuration.sentry_breadcrumb_loggers.map(&:to_sym)
# Output debug info for sentry
config.before_send = lambda do |event, hint|
Rails.logger.debug do
payload_sizes = event.to_json_compatible.transform_values { |v| JSON.generate(v).bytesize }.inspect
"[Sentry] will send event #{hint}. Payload sizes are #{payload_sizes.inspect}"
end
event
end
event
end
# Add plugins with openproject/open_project in backtraces to internal
config.app_dirs_pattern = /(bin|exe|app|config|lib|open_?project)/
# Add plugins with openproject/open_project in backtraces to internal
config.app_dirs_pattern = /(bin|exe|app|config|lib|open_?project)/
# Don't send loaded modules
config.send_modules = false
# Don't send loaded modules
config.send_modules = false
# Disable sentry's internal client reports
config.send_client_reports = false
# Disable sentry's internal client reports
config.send_client_reports = false
# Cleanup backtrace
config.backtrace_cleanup_callback = lambda do |backtrace|
Rails.backtrace_cleaner.clean(backtrace)
end
# Cleanup backtrace
config.backtrace_cleanup_callback = lambda do |backtrace|
Rails.backtrace_cleaner.clean(backtrace)
end
# Sample rate for performance
# 0.0 = disabled
# 1.0 = all samples are traced
sample_factor = OpenProject::Configuration.sentry_trace_factor.to_f
# Define a tracing sample handler
trace_sampler = lambda do |sampling_context|
# if this is the continuation of a trace, just use that decision (rate controlled by the caller)
next sampling_context[:parent_sampled] unless sampling_context[:parent_sampled].nil?
# transaction_context is the transaction object in hash form
# keep in mind that sampling happens right after the transaction is initialized
# for example, at the beginning of the request
transaction_context = sampling_context[:transaction_context]
# transaction_context helps you sample transactions with more sophistication
# for example, you can provide different sample rates based on the operation or name
op = transaction_context[:op]
transaction_name = transaction_context[:name]
rate = case op
when /request/
case transaction_name
when /health_check/
0.0
# Sample rate for performance
# 0.0 = disabled
# 1.0 = all samples are traced
sample_factor = OpenProject::Configuration.sentry_trace_factor.to_f
# Define a tracing sample handler
trace_sampler = lambda do |sampling_context|
# if this is the continuation of a trace, just use that decision (rate controlled by the caller)
next sampling_context[:parent_sampled] unless sampling_context[:parent_sampled].nil?
# transaction_context is the transaction object in hash form
# keep in mind that sampling happens right after the transaction is initialized
# for example, at the beginning of the request
transaction_context = sampling_context[:transaction_context]
# transaction_context helps you sample transactions with more sophistication
# for example, you can provide different sample rates based on the operation or name
op = transaction_context[:op]
transaction_name = transaction_context[:name]
rate = case op
when /request/
case transaction_name
when /health_check/
0.0
else
[0.1 * sample_factor, 1.0].min
end
when /delayed_job/
[0.01 * sample_factor, 1.0].min
else
[0.1 * sample_factor, 1.0].min
0.0 # ignore all other transactions
end
when /delayed_job/
[0.01 * sample_factor, 1.0].min
else
0.0 # ignore all other transactions
end
Rails.logger.debug { "[SENTRY TRACE SAMPLER] Decided on sampling rate #{rate} for #{op}: #{transaction_name} " }
Rails.logger.debug { "[SENTRY TRACE SAMPLER] Decided on sampling rate #{rate} for #{op}: #{transaction_name} " }
rate
end
rate
end
# Assign the sampler conditionally to avoid running the lambda
# when we don't trace anyway
if sample_factor.zero?
Rails.logger.debug { "[SENTRY TRACE SAMPLER] Requested factor is zero, skipping performance tracing" }
config.traces_sample_rate = 0
config.traces_sampler = nil
else
Rails.logger.debug { "[SENTRY TRACE SAMPLER] Requested factor is #{sample_factor}, setting up performance tracing" }
config.traces_sampler = trace_sampler
end
# Assign the sampler conditionally to avoid running the lambda
# when we don't trace anyway
if sample_factor.zero?
Rails.logger.debug { "[SENTRY TRACE SAMPLER] Requested factor is zero, skipping performance tracing" }
config.traces_sample_rate = 0
config.traces_sampler = nil
else
Rails.logger.debug { "[SENTRY TRACE SAMPLER] Requested factor is #{sample_factor}, setting up performance tracing" }
config.traces_sampler = trace_sampler
# Set release info
config.release = OpenProject::VERSION.to_s
end
# Set release info
config.release = OpenProject::VERSION.to_s
# Extend the core log delegator
handler = ::OpenProject::Logging::SentryLogger.method(:log)
::OpenProject::Logging::LogDelegator.register(:sentry, handler)
end
# Extend the core log delegator
handler = ::OpenProject::Logging::SentryLogger.method(:log)
::OpenProject::Logging::LogDelegator.register(:sentry, handler)
end

@ -54,7 +54,7 @@ module OpenProject
rescue StandardError => e
Rails.logger.error(
"Failed to determine new `after_expire` value. " +
"Falling back to original value. (#{e.message} at #{caller.first})"
"Falling back to original value. (#{e.message} at #{caller.first})"
)
options[:expire_after]
@ -73,13 +73,13 @@ if Rails.env.test?
config['session_store'] = :active_record_store
end
session_store = config['session_store'].to_sym
session_store = config['session_store'].to_sym
relative_url_root = config['rails_relative_url_root'].presence
session_options = {
key: config['session_cookie_name'],
httponly: true,
secure: Setting.https?,
secure: config.secure_connection?,
path: relative_url_root
}
@ -97,25 +97,26 @@ if session_store == :cache_store
expire_after # anonymous user
end
end
end
OpenProject::Application.config.session_store session_store, **session_options
Rails.application.reloader.to_prepare do
method = ActionDispatch::Session::CacheStore.instance_method(:write_session)
unless method.to_s.include?("write_session(env, sid, session, options)")
raise(
"The signature for `ActionDispatch::Session::CacheStore.write_session` " +
"seems to have changed. Please update the " +
"`ExpireStoreAfterOption` module (and this check) in #{__FILE__}"
"seems to have changed. Please update the " +
"`ExpireStoreAfterOption` module (and this check) in #{__FILE__}"
)
end
ActionDispatch::Session::CacheStore.prepend OpenProject::ExpireStoreAfterOption
end
OpenProject::Application.config.session_store session_store, **session_options
##
# We use our own decorated session model to note the user_id
# for each session.
ActionDispatch::Session::ActiveRecordStore.session_class = ::Sessions::SqlBypass
# Continue to use marshal serialization to retain symbols and whatnot
ActiveRecord::SessionStore::Session.serializer = :marshal
##
# We use our own decorated session model to note the user_id
# for each session.
ActionDispatch::Session::ActiveRecordStore.session_class = ::Sessions::SqlBypass
# Continue to use marshal serialization to retain symbols and whatnot
ActiveRecord::SessionStore::Session.serializer = :marshal
end

@ -26,59 +26,61 @@
# See COPYRIGHT and LICENSE files for more details.
#++
OpenProject::Notifications.subscribe(OpenProject::Events::JOURNAL_CREATED) do |payload|
# A job is scheduled that creates notifications (in app if supported) right away and schedules
# jobs to be run for mail and digest mails.
Notifications::WorkflowJob
.perform_later(:create_notifications,
payload[:journal],
payload[:send_notification])
Rails.application.config.after_initialize do
OpenProject::Notifications.subscribe(OpenProject::Events::JOURNAL_CREATED) do |payload|
# A job is scheduled that creates notifications (in app if supported) right away and schedules
# jobs to be run for mail and digest mails.
Notifications::WorkflowJob
.perform_later(:create_notifications,
payload[:journal],
payload[:send_notification])
# A job is scheduled for the end of the journal aggregation time. If the journal does still exist
# at the end (it might be replaced because another journal was created within that timeframe)
# that job generates a OpenProject::Events::AGGREGATED_..._JOURNAL_READY event.
Journals::CompletedJob.schedule(payload[:journal], payload[:send_notification])
end
# A job is scheduled for the end of the journal aggregation time. If the journal does still exist
# at the end (it might be replaced because another journal was created within that timeframe)
# that job generates a OpenProject::Events::AGGREGATED_..._JOURNAL_READY event.
Journals::CompletedJob.schedule(payload[:journal], payload[:send_notification])
end
OpenProject::Notifications.subscribe(OpenProject::Events::JOURNAL_AGGREGATE_BEFORE_DESTROY) do |payload|
Notifications::AggregatedJournalService.relocate_immediate(**payload.slice(:journal, :predecessor))
end
OpenProject::Notifications.subscribe(OpenProject::Events::JOURNAL_AGGREGATE_BEFORE_DESTROY) do |payload|
Notifications::AggregatedJournalService.relocate_immediate(**payload.slice(:journal, :predecessor))
end
OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_ADDED) do |payload|
next unless payload[:send_notifications]
OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_ADDED) do |payload|
next unless payload[:send_notifications]
Mails::WatcherAddedJob
.perform_later(payload[:watcher],
payload[:watcher_setter])
end
Mails::WatcherAddedJob
.perform_later(payload[:watcher],
payload[:watcher_setter])
end
OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_REMOVED) do |payload|
Mails::WatcherRemovedJob
.perform_later(payload[:watcher].attributes,
payload[:watcher_remover])
end
OpenProject::Notifications.subscribe(OpenProject::Events::WATCHER_REMOVED) do |payload|
Mails::WatcherRemovedJob
.perform_later(payload[:watcher].attributes,
payload[:watcher_remover])
end
OpenProject::Notifications.subscribe(OpenProject::Events::MEMBER_CREATED) do |payload|
next unless payload[:send_notifications]
OpenProject::Notifications.subscribe(OpenProject::Events::MEMBER_CREATED) do |payload|
next unless payload[:send_notifications]
Mails::MemberCreatedJob
.perform_later(current_user: User.current,
member: payload[:member],
message: payload[:message])
end
Mails::MemberCreatedJob
.perform_later(current_user: User.current,
member: payload[:member],
message: payload[:message])
end
OpenProject::Notifications.subscribe(OpenProject::Events::MEMBER_UPDATED) do |payload|
next unless payload[:send_notifications]
OpenProject::Notifications.subscribe(OpenProject::Events::MEMBER_UPDATED) do |payload|
next unless payload[:send_notifications]
Mails::MemberUpdatedJob
.perform_later(current_user: User.current,
member: payload[:member],
message: payload[:message])
end
Mails::MemberUpdatedJob
.perform_later(current_user: User.current,
member: payload[:member],
message: payload[:message])
end
OpenProject::Notifications.subscribe(OpenProject::Events::NEWS_COMMENT_CREATED) do |payload|
Notifications::WorkflowJob
.perform_later(:create_notifications,
payload[:comment],
payload[:send_notification])
OpenProject::Notifications.subscribe(OpenProject::Events::NEWS_COMMENT_CREATED) do |payload|
Notifications::WorkflowJob
.perform_later(:create_notifications,
payload[:comment],
payload[:send_notification])
end
end

@ -1,10 +1,12 @@
##
# The default behaviour is to send the user a sign-up mail
# when they were invited.
OpenProject::Notifications.subscribe UserInvitation::Events.user_invited do |token|
Mails::InvitationJob.perform_later(token)
end
Rails.application.config.after_initialize do
##
# The default behaviour is to send the user a sign-up mail
# when they were invited.
OpenProject::Notifications.subscribe UserInvitation::Events.user_invited do |token|
Mails::InvitationJob.perform_later(token)
end
OpenProject::Notifications.subscribe UserInvitation::Events.user_reinvited do |token|
Mails::InvitationJob.perform_later(token)
OpenProject::Notifications.subscribe UserInvitation::Events.user_reinvited do |token|
Mails::InvitationJob.perform_later(token)
end
end

@ -1,32 +1,20 @@
require 'open_project/authentication'
Rails.application.config.after_initialize do
namespace = OpenProject::Authentication::Strategies::Warden
# Strategies provided by OpenProject:
require 'open_project/authentication/strategies/warden/basic_auth_failure'
require 'open_project/authentication/strategies/warden/global_basic_auth'
require 'open_project/authentication/strategies/warden/user_basic_auth'
require 'open_project/authentication/strategies/warden/doorkeeper_oauth'
require 'open_project/authentication/strategies/warden/session'
strategies = [
[:basic_auth_failure, namespace::BasicAuthFailure, 'Basic'],
[:global_basic_auth, namespace::GlobalBasicAuth, 'Basic'],
[:user_basic_auth, namespace::UserBasicAuth, 'Basic'],
[:oauth, namespace::DoorkeeperOAuth, 'OAuth'],
[:anonymous_fallback, namespace::AnonymousFallback, 'Basic'],
[:session, namespace::Session, 'Session']
]
WS = OpenProject::Authentication::Strategies::Warden
strategies.each do |name, clazz, auth_scheme|
OpenProject::Authentication.add_strategy name, clazz, auth_scheme
end
strategies = [
[:basic_auth_failure, WS::BasicAuthFailure, 'Basic'],
[:global_basic_auth, WS::GlobalBasicAuth, 'Basic'],
[:user_basic_auth, WS::UserBasicAuth, 'Basic'],
[:oauth, WS::DoorkeeperOAuth, 'OAuth'],
[:anonymous_fallback, WS::AnonymousFallback, 'Basic'],
[:session, WS::Session, 'Session']
]
strategies.each do |name, clazz, auth_scheme|
OpenProject::Authentication.add_strategy name, clazz, auth_scheme
end
include OpenProject::Authentication::Scope
api_v3_options = {
store: false
}
OpenProject::Authentication.update_strategies(API_V3, api_v3_options) do |_strategies|
%i[global_basic_auth user_basic_auth basic_auth_failure oauth session anonymous_fallback]
OpenProject::Authentication.update_strategies(OpenProject::Authentication::Scope::API_V3, { store: false }) do |_|
%i[global_basic_auth user_basic_auth basic_auth_failure oauth session anonymous_fallback]
end
end

@ -68,7 +68,6 @@ Rails.autoloaders.each do |autoloader|
autoloader.inflector = OpenProject::Inflector.new(__FILE__)
end
Rails.autoloaders.main.ignore(Rails.root.join('lib/plugins'))
Rails.autoloaders.main.ignore(Rails.root.join('lib/open_project/patches'))
Rails.autoloaders.main.ignore(Rails.root.join('lib/generators'))

@ -22,68 +22,68 @@
az:
js:
ajax:
hide: "Hide"
loading: "Loading…"
updating: "Updating…"
hide: "Gizlət"
loading: "Yüklənir…"
updating: "Yenilənir…"
attachments:
draggable_hint: |
Drag on editor field to inline image or reference attachment. Closed editor fields will be opened while you keep dragging.
autocomplete_select:
placeholder:
multi: "Add \"%{name}\""
single: "Select \"%{name}\""
remove: "Remove %{name}"
active: "Active %{label} %{name}"
multi: "\"%{name}\" əlavə et"
single: "\"%{name}\" seç"
remove: "%{name} çıxart"
active: "Aktiv %{label} %{name}"
backup:
attachments_disabled: Attachments may not be included since they exceed the maximum overall size allowed. You can change this via the configuration (requires a server restart).
attachments_disabled: İcazə verilən maksimal cəm sayını aşdığı üçün qoşmalar daxil edilməyə bilər. Bunu konfiqurasiya vasitəsilə dəyişdirə bilərsiniz (serverin yenidən başladılmasını tələb edir).
info: >
You can trigger a backup here. The process can take some time depending on the amount of data (especially attachments) you have. You will receive an email once it's ready.
note: >
A new backup will override any previous one. Only a limited number of backups per day can be requested.
last_backup: Last backup
last_backup_from: Last backup from
title: Backup OpenProject
options: Options
include_attachments: Include attachments
download_backup: Download backup
request_backup: Request backup
Yeni bir nüsxələmə, əvvəlkini etibarsız edəcək. Gündəlik yalnız limitli sayda nüsxələmə tələb oluna bilər.
last_backup: Son nüsxələmə
last_backup_from: Son nüsxələmə
title: OpenProject nüsxələməsi
options: Seçimlər
include_attachments: Qoşmaları daxil et
download_backup: Nüsxəni endir
request_backup: Nüsxə tələb et
close_popup_title: "Açılan pəncərəni bağla"
close_filter_title: "Close filter"
close_form_title: "Close form"
button_add_watcher: "Add watcher"
button_add: "Add"
button_back: "Back"
button_back_to_list_view: "Back to list view"
button_cancel: "Cancel"
button_close: "Close"
button_change_project: "Change project"
button_check_all: "Check all"
button_configure-form: "Configure form"
button_confirm: "Confirm"
button_continue: "Continue"
button_copy: "Copy"
button_copy_to_other_project: "Copy to other project"
button_custom-fields: "Custom fields"
button_delete: "Delete"
close_filter_title: "Filtr seç"
close_form_title: "Formu bağla"
button_add_watcher: "Nəzarətçi əlavə et"
button_add: "Əlavə et"
button_back: "Geri"
button_back_to_list_view: "Siyahı görünüşünə qayıt"
button_cancel: "İmtina"
button_close: "Bağla"
button_change_project: "Layihəni dəyişdir"
button_check_all: "Hamısını seç"
button_configure-form: "Formu konfiqurasiya et"
button_confirm: "Təsdiqlə"
button_continue: "Davam"
button_copy: "Kopyala"
button_copy_to_other_project: "Digər layihəyə kopyala"
button_custom-fields: "Özəl sahələr"
button_delete: "Sil"
button_delete_watcher: "İzləyicini sil"
button_details_view: "Təfsilat baxışı"
button_duplicate: "Duplicate"
button_edit: "Edit"
button_filter: "Filter"
button_collapse_all: "Collapse all"
button_expand_all: "Expand all"
button_advanced_filter: "Advanced filter"
button_duplicate: "Çoxalt"
button_edit: "Düzəliş et"
button_filter: "Filtr"
button_collapse_all: "Hamısını yığcamlaşdır"
button_expand_all: "Hamısını genişləndir"
button_advanced_filter: "Qabaqcıl filtr"
button_list_view: "Siyahı baxışı"
button_show_view: "Fullscreen view"
button_log_time: "Log time"
button_more: "More"
button_show_view: "Tam ekran görünüşü"
button_log_time: "Jurnal vaxtı"
button_more: "Daha çox"
button_open_details: "Təfsilat baxışını aç"
button_close_details: "Close details view"
button_open_fullscreen: "Open fullscreen view"
button_show_cards: "Show card view"
button_show_list: "Show list view"
button_quote: "Quote"
button_save: "Save"
button_close_details: "Təfsilatlar görünüşünü bağla"
button_open_fullscreen: "Tam ekran görünüşünü aç"
button_show_cards: "Kart görünüşünü göstər"
button_show_list: "Siyahı görünüşünü göstər"
button_quote: "Sitat"
button_save: "Saxla"
button_settings: "Settings"
button_uncheck_all: "Uncheck all"
button_update: "Yeniləmə"
@ -147,7 +147,7 @@ az:
code_block:
button: 'Insert code snippet'
title: 'Insert / edit Code snippet'
language: 'Formatting language'
language: 'Formatlama dili'
language_hint: 'Enter the formatting language that will be used for highlighting (if supported).'
dropdown:
macros: 'Macros'

@ -0,0 +1,21 @@
class CreateWeekDays < ActiveRecord::Migration[6.1]
def up
create_table :week_days do |t|
t.integer :day, null: false
t.boolean :working, null: false, default: true
t.timestamps
end
execute <<-SQL.squish
ALTER TABLE week_days
ADD CONSTRAINT unique_day_number UNIQUE (day);
ALTER TABLE week_days
ADD CHECK (day >= 1 AND day <=7);
SQL
end
def down
drop_table :week_days
end
end

@ -0,0 +1,11 @@
class CreateNonWorkingDays < ActiveRecord::Migration[6.1]
def change
create_table :non_working_days do |t|
t.string :name, null: false
t.date :date, null: false
t.timestamps
end
add_index :non_working_days, :date, unique: true
end
end

@ -19,11 +19,11 @@ get:
Accepts the same format as returned by the [queries](https://www.openproject.org/docs/api/endpoints/queries/)
endpoint. Currently supported filters are:
+ interval: the inclusive date interval to scope days to look up. When
+ date: the inclusive date interval to scope days to look up. When
unspecified, default is from the beginning of current month to the end
of following month.
Example: `{ "interval": { "operator": "<>d", "values": ["2022-05-02","2022-05-26"] } }`
Example: `{ "date": { "operator": "<>d", "values": ["2022-05-02","2022-05-26"] } }`
would return days between May 5 and May 26 2022, inclusive.
+ working: when `true`, returns only the working days. When `false`,
@ -32,7 +32,7 @@ get:
Example: `{ "working": { "operator": "=", "values": ["t"] } }`
would exclude non-working days from the response.
example: '[{ "interval": { "operator": "<>d", "values": ["2022-05-02","2022-05-26"] } }, { "working": { "operator": "=", "values": ["f"] } }]'
example: '[{ "date": { "operator": "<>d", "values": ["2022-05-02","2022-05-26"] } }, { "working": { "operator": "=", "values": ["f"] } }]'
required: false
schema:
type: string

@ -102,17 +102,13 @@ Rails.application.config.hosts << 'openproject.example.com'
Then, you will start a REPL console for OpenProject with: `RAILS_ENV=development ./bin/rails console`
Update the settings for host name and protocol:
Then, you will to set the following settings
```ruby
Setting.protocol = 'https'
Setting.host_name = 'openproject.example.com'
export OPENPROJECT_HTTPS = true
export OPENPROJECT_HOST__NAME = 'openproject.example.com'
```
Finally, start your OpenProject development server and Frontend server and access `https://openproject.example.com` in your browser.

@ -135,5 +135,7 @@ OPENPROJECT_SECURITY__BADGE__URL (default="https://releases.openproject.com/v1/c
OPENPROJECT_MIGRATION__CHECK__ON__EXCEPTIONS (default=true)
OPENPROJECT_SHOW__PENDING__MIGRATIONS__WARNING (default=true)
OPENPROJECT_SHOW__WARNING__BARS (default=true)
OPENPROJECT_SHOW__STORAGE__INFORMATION (default=true)
OPENPROJECT_SHOW__STORAGE__INFORMATION (default=true)
OPENPROJECT_LDAP__USERS__SYNC__STATUS (default=true)
OPENPROJECT_LDAP__USERS__DISABLE__SYNC__JOB(default=false)
```

@ -79,7 +79,7 @@ If you're terminating SSL on the outer server, you need to set the `X-Forwarded-
Finally, to let OpenProject know that it should create links with 'https' when no request is available (for example, when sending emails), you need to set the Protocol setting of OpenProject to `https`. You will find this setting on your system settings or via the rails console with `Setting.protocol = 'https'`
Finally, to let OpenProject know that it should create links with 'https' when no request is available (for example, when sending emails), you need to set the Protocol setting of OpenProject to `https`. You can set this configuration by setting the ENV `OPENPROJECT_HTTPS=true`.
_<sup>1</sup> In the packaged installation this means you selected "no" when asked for SSL in the configuration wizard but at the same time take care of SSL termination elsewhere. This can be a manual Apache setup on the same server (not recommended) or an external server, for instance._

@ -137,3 +137,25 @@ With the [OpenProject Enterprise Edition](https://www.openproject.org/enterprise
OpenProject supports multiple LDAP connections to source users from. The user's authentication source is remembered the first time it is created (but can be switched in the administration backend). This ensures that the correct connection / LDAP source will be used for the user.
Duplicates in the unique attributes (login, email) are not allowed and a second user with the same attributes will not be able to login. Please ensure that amongst all LDAP connections, a unique attribute is used that does not result in conflicting logins.
## LDAP user synchronization
By default, OpenProject will synchronize user account details (name, e-mail, login) and their account status from the LDAP through a background worker job every 24 hours.
The user will be ensured to be active if it can be found in LDAP. Likewise, if the user cannot be found in the LDAP, its associated OpenProject account will be locked.
### **Disabling status synchronization**
If you wish to synchronize account data from the LDAP, but not synchronize the status to the associated OpenProject account, you can do so with the following configuration variable:
- `ldap_users_sync_status: false`
- (or the ENV variable `OPENPROJECT_LDAP__USERS__SYNC__STATUS=false`)
### Disabling the synchronization job
If for any reason, you do not wish to perform the synchronization at all, you can also remove the synchronization job from being run at all with the following variable:
- `ldap_users_disable_sync_job: true`
- (or the ENV variable `OPENPROJECT_LDAP__USERS__DISABLE__SYNC__JOB=true`)

@ -13,7 +13,16 @@ keywords: SAML, SSO, single sign-on, authentication
</div>
You can integrate your active directory or other SAML compliant identity provider in your OpenProject Enterprise Edition.
### Prerequisites
In order to use integrate OpenProject as a service provider (SP) using SAML, your identity providers (idP):
- needs to be able to handle SAML 2.0 redirect Single-Sign On (SSO) flows, in some implementations also referred to as WebSSO
- has a known or configurable set of attributes that map to the following required OpenProject attributes. The way these attribute mappings will be defined is described later in this document.
- **login**: A stable attribute used to uniquely identify the user. This willl most commonly map to an account ID, samAccountName or email (but please note that emails are often interchangeable, and this might result in logins changing in OpenProject).
- **email**: The email attribute of the user being authenticated
- **first name** and **last name** of the user.
- provides the public certificate or certificate fingerprint (SHA1) in use for communicating with the idP.
### 1: Configuring the SAML integration
@ -48,13 +57,16 @@ The following is an exemplary file with a set of common settings:
```yaml
saml:
# Name of the provider, leave this at saml unless you use multiple providers
name: "saml"
# The name that will be display in the login button
display_name: "My SSO"
# Use the default SAML icon
icon: "auth_provider-saml.png"
# omniauth-saml config
# The callback within OpenProject that your idP should redirect to
assertion_consumer_service_url: "https://<YOUR OPENPROJECT HOSTNAME>/auth/saml/callback"
# The SAML issuer string that OpenProject will call your idP with
issuer: "https://<YOUR OPENPROJECT HOSTNAME>"
# IF your SSL certificate on your SSO is not trusted on this machine, you need to add it here in ONE line
@ -65,10 +77,10 @@ saml:
# Either `idp_cert` or `idp_cert_fingerprint` must be present!
idp_cert_fingerprint: "E7:91:B2:E1:..."
# Replace with your single sign on URL
# Replace with your SAML 2.0 redirect flow single sign on URL
# For example: "https://sso.example.com/saml/singleSignOn"
idp_sso_target_url: "<YOUR SSO URL>"
# Replace with your single sign out URL
# Replace with your redirect flow single sign out URL
# or comment out
# For example: "https://sso.example.com/saml/proxySingleLogout"
idp_slo_target_url: "<YOUR SSO logout URL>"
@ -94,10 +106,39 @@ As with [all the rest of the OpenProject configuration settings](../../../instal
E.g.
```bash
OPENPROJECT_SAML_MY__SAML_NAME="your-provider-name"
OPENPROJECT_SAML_MY__SAML_DISPLAY__NAME="My SAML provider"
...
OPENPROJECT_SAML_MY__SAML_ATTRIBUTE__STATEMENTS_ADMIN="['openproject-isadmin']"
# Name of the provider, leave this at saml unless you use multiple providers
OPENPROJECT_SAML_SAML_NAME="saml"
# The name that will be display in the login button
OPENPROJECT_SAML_SAML_DISPLAY__NAME=">Name of the login button>"
# The callback within OpenProject that your idP should redirect to
OPENPROJECT_SAML_SAML_ASSERTION__CONSUMER__SERVICE__URL="https://<openproject.host>/auth/saml/callback"
# The SAML issuer string that OpenProject will call your idP with
OPENPROJECT_SAML_SAML_ISSUER="https://<openproject.host>"
# IF your SSL certificate on your SSO is not trusted on this machine, you need to add it here in ONE line
### one liner to generate certificate in ONE line
### awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' <yourcert.pem>
#idp_cert: "-----BEGIN CERTIFICATE-----\n ..... SSL CERTIFICATE HERE ...-----END CERTIFICATE-----\n"
# Otherwise, the certificate fingerprint must be added
# Either `OPENPROJECT_SAML_SAML_IDP__CERT` or `OPENPROJECT_SAML_SAML_IDP__CERT__FINGERPRINT` must be present!
OPENPROJECT_SAML_SAML_IDP__CERT="-----BEGIN CERTIFICATE-----<cert one liner>-----END CERTIFICATE-----"
OPENPROJECT_SAML_SAML_IDP__CERT__FINGERPRINT="da:39:a3:ee:5e:6b:4b:0d:32:55:bf:ef:95:60:18:90:af:d8:07:09"
# Replace with your single sign on URL
OPENPROJECT_SAML_SAML_IDP__SSO__TARGET__URL="https://<auth.host>/application/saml/vjdyzjls/sso/binding/post/"
# (Optinal) Replace with your redirect flow single sign out URL that we should redirect to
OPENPROJECT_SAML_SAML_IDP__SLO__TARGET__URL=""
#
# Which SAMLAttribute we should look for for the corresponding attributes of OpenProject
# can be a string or URI/URN depending on our idP format
OPENPROJECT_SAML_SAML_ATTRIBUTE__STATEMENTS_EMAIL="mail"
OPENPROJECT_SAML_SAML_ATTRIBUTE__STATEMENTS_LOGIN="mail"
OPENPROJECT_SAML_SAML_ATTRIBUTE__STATEMENTS_FIRST__NAME="givenName"
OPENPROJECT_SAML_SAML_ATTRIBUTE__STATEMENTS_LAST__NAME="sn"
```
Please note that every underscore (`_`) in the original configuration key has to be replaced by a duplicate underscore

@ -50,7 +50,7 @@ export class SpotTextFieldComponent implements ControlValueAccessor {
}
writeValue(value:string) {
this.value = value;
this.value = value || '';
}
onChange = (_:string):void => {};

@ -30,8 +30,6 @@ require 'roar/decorator'
require 'roar/hypermedia'
require 'roar/json/hal'
require 'api/v3/utilities/path_helper'
module API
module Decorators
class Single < ::Roar::Decorator

@ -46,7 +46,7 @@ module API
use OpenProject::Authentication::Manager
helpers API::Caching::Helpers
helpers do
module Helpers
def current_user
User.current
end
@ -116,12 +116,27 @@ module API
current_user && (current_user.admin? || !current_user.anonymous?)
end
# Checks that the current user has the given permission or raise
# {API::Errors::Unauthorized}.
#
# @param permission [String] the permission name
#
# @param context [Project, Array<Project>, nil] can be:
# * a project : returns true if user is allowed to do the specified
# action on this project
# * a group of projects : returns true if user is allowed on every
# project
# * +nil+ with +options[:global]+ set: check if user has at least one
# role allowed for this action, or falls back to Non Member /
# Anonymous permissions depending if the user is logged
#
# @param global [Boolean] when +true+ and with +context+ set to +nil+:
# checks that the current user is allowed to do the specified action on
# any project
#
# @raise [API::Errors::Unauthorized] when permission is not met
def authorize(permission, context: nil, global: false, user: current_user, &block)
auth_service = AuthorizationService.new(permission,
context: context,
global: global,
user: user)
auth_service = -> { user.allowed_to?(permission, context, global:) }
authorize_by_with_raise auth_service, &block
end
@ -184,6 +199,8 @@ module API
end
end
helpers Helpers
def self.auth_headers
lambda do
header = OpenProject::Authentication::WWWAuthenticate

@ -0,0 +1,32 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module API::V3::Days
class DayCollectionRepresenter < ::API::Decorators::UnpaginatedCollection
end
end

@ -26,18 +26,16 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module Constants
module ProjectActivity
class << self
def register(on:, attribute:, chain: [])
@registered ||= Set.new
module API::V3::Days
class DayRepresenter < ::API::Decorators::Single
property :date
property :name
property :working
@registered << { on: on,
chain: chain,
attribute: attribute }
end
self_link path: :day, id_attribute: :date
attr_reader :registered
def _type
'Day'
end
end
end

@ -0,0 +1,43 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module API::V3::Days
class DaysAPI < ::API::OpenProjectAPI
helpers ::API::Utilities::UrlPropsParsingHelper
resources :days do
mount NonWorkingDaysAPI
mount WeekAPI
get &::API::V3::Utilities::Endpoints::Index.new(
model: Day,
self_path: -> { api_v3_paths.days }
).mount
end
end
end

@ -0,0 +1,43 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module API::V3::Days
class NonWorkingDayRepresenter < ::API::Decorators::Single
include ::API::Decorators::DateProperty
include ::API::Caching::CachedRepresenter
property :name
date_property :date
self_link path: :days_non_working_day, id_attribute: :date
def _type
'NonWorkingDay'
end
end
end

@ -0,0 +1,45 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module API::V3::Days
class NonWorkingDaysAPI < ::API::OpenProjectAPI
helpers ::API::Utilities::UrlPropsParsingHelper
resources :non_working do
route_param :date, type: Date, desc: 'NonWorkingDay DATE' do
after_validation do
@non_working_day = NonWorkingDay.find_by!(date: declared_params[:date])
end
get &::API::V3::Utilities::Endpoints::Show.new(model: NonWorkingDay,
render_representer: NonWorkingDayRepresenter)
.mount
end
end
end
end

@ -0,0 +1,47 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module API::V3::Days
class WeekAPI < ::API::OpenProjectAPI
helpers ::API::Utilities::UrlPropsParsingHelper
resources :week do
get &::API::V3::Utilities::Endpoints::Index.new(model: WeekDay,
render_representer: WeekDayCollectionRepresenter,
self_path: -> { api_v3_paths.days_week })
.mount
route_param :day, type: Integer, desc: 'WeekDay ID' do
after_validation do
@week_day = WeekDay.find_by!(day: declared_params[:day])
end
get &::API::V3::Utilities::Endpoints::Show.new(model: WeekDay, render_representer: WeekDayRepresenter).mount
end
end
end
end

@ -0,0 +1,32 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module API::V3::Days
class WeekDayCollectionRepresenter < ::API::Decorators::UnpaginatedCollection
end
end

@ -0,0 +1,43 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module API::V3::Days
class WeekDayRepresenter < ::API::Decorators::Single
include ::API::Caching::CachedRepresenter
property :day
property :name
property :working
self_link path: :days_week_day, id_attribute: :day
def _type
'WeekDay'
end
end
end

@ -52,6 +52,7 @@ module API
mount ::API::V3::Configuration::ConfigurationAPI
mount ::API::V3::CustomActions::CustomActionsAPI
mount ::API::V3::CustomOptions::CustomOptionsAPI
mount ::API::V3::Days::DaysAPI
mount ::API::V3::Notifications::NotificationsAPI
mount ::API::V3::HelpTexts::HelpTextsAPI
mount ::API::V3::Memberships::MembershipsAPI
@ -80,7 +81,7 @@ module API
mount ::API::V3::Grids::GridsAPI
get '/' do
RootRepresenter.new({}, current_user: current_user)
RootRepresenter.new({}, current_user:)
end
get '/spec.json' do

@ -44,8 +44,8 @@ module API
end
link :memberships do
next unless current_user.allowed_to?(:view_members, nil, global: true) ||
current_user.allowed_to?(:manage_members, nil, global: true)
next unless current_user.allowed_to_globally?(:view_members) ||
current_user.allowed_to_globally?(:manage_members)
{
href: api_v3_paths.memberships

@ -77,7 +77,7 @@ module API
private
def render_success(query, params, self_path, base_scope)
results = merge_scopes(base_scope, query.results)
results = apply_scope_constraint(base_scope, query.results)
if paginated_representer?
render_paginated_success(results, query, params, self_path)
@ -159,11 +159,11 @@ module API
end
end
def merge_scopes(scope_a, scope_b)
if scope_a.is_a? Class
scope_b
def apply_scope_constraint(constraint, result_scope)
if constraint.is_a?(Class)
result_scope
else
scope_a.merge(scope_b)
result_scope.where id: constraint.select(:id)
end
end
end

@ -192,6 +192,30 @@ module API
"#{root}/custom_options/#{id}"
end
def self.day(date)
"#{days}/#{date}"
end
def self.days
"#{root}/days"
end
def self.days_week
"#{days}/week"
end
def self.days_week_day(day)
"#{days_week}/#{day}"
end
def self.days_non_working
"#{root}/days/non_working"
end
def self.days_non_working_day(date)
"#{days_non_working}/#{date}"
end
index :help_text
show :help_text
@ -473,7 +497,7 @@ module API
"#{project_id}-#{type_id}"
end
filter = [{ id: { operator: '=', values: values } }]
filter = [{ id: { operator: '=', values: } }]
path + "?filters=#{CGI.escape(filter.to_s)}"
end
@ -497,8 +521,8 @@ module API
sortBy: sort_by&.to_json,
groupBy: group_by,
pageSize: page_size,
offset: offset,
select: select
offset:,
select:
}.compact_blank
if query_params.any?

@ -33,8 +33,6 @@ require 'open_project/logging'
require 'open_project/patches'
require 'open_project/mime_type'
require 'open_project/custom_styles/design'
require 'open_project/hook'
require 'open_project/hooks'
require 'redmine/plugin'
require 'csv'

@ -40,6 +40,8 @@ module OpenProject
@modules += mapper.mapped_modules
@project_modules_without_permissions ||= []
@project_modules_without_permissions += mapper.project_modules_without_permissions
clear_caches
end
# Get a sorted array of module names

@ -1,237 +0,0 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
require_relative 'configuration/helpers'
require_relative 'configuration/asset_host'
module OpenProject
module Configuration
extend Helpers
class << self
# Returns a configuration setting
def [](name)
Settings::Definition[name]&.value
end
# Sets configuration setting
def []=(name, value)
Settings::Definition[name].value = value
end
def configure_cache(application_config)
return unless override_cache_config? application_config
# rails defaults to :file_store, use :mem_cache_store when :memcache is configured in configuration.yml
# Also use :mem_cache_store for when :dalli_store is configured
cache_store = self['rails_cache_store'].try(:to_sym)
case cache_store
when :memcache, :dalli_store
cache_config = [:mem_cache_store]
cache_config << self['cache_memcache_server'] if self['cache_memcache_server']
# default to :file_store
when NilClass, :file_store
cache_config = [:file_store, Rails.root.join('tmp/cache')]
else
cache_config = [cache_store]
end
parameters = cache_parameters
cache_config << parameters if parameters.size > 0
application_config.cache_store = cache_config
end
def override_cache_config?(application_config)
# override if cache store is not set
# or cache store is :file_store
# or there is something to overwrite it
application_config.cache_store.nil? ||
application_config.cache_store == :file_store ||
self['rails_cache_store'].present?
end
def migrate_mailer_configuration!
# do not migrate if forced to legacy configuration (using settings or ENV)
return true if self['email_delivery_configuration'] == 'legacy'
# do not migrate if no legacy configuration
return true if self['email_delivery_method'].blank?
# do not migrate if the setting already exists and is not blank
return true if Setting.email_delivery_method.present?
Rails.logger.info 'Migrating existing email configuration to the settings table...'
Setting.email_delivery_method = self['email_delivery_method'].to_sym
['smtp_', 'sendmail_'].each do |config_type|
mail_delivery_configs = Settings::Definition.all_of_prefix(config_type)
next if mail_delivery_configs.empty?
mail_delivery_configs.each do |config|
Setting["#{config_type}#{config.name}"] = case config.value
when TrueClass
1
when FalseClass
0
else
v
end
end
end
true
end
def reload_mailer_configuration!
if self['email_delivery_configuration'] == 'legacy'
configure_legacy_action_mailer
else
case Setting.email_delivery_method
when :smtp
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.delivery_method = Setting.email_delivery_method
reload_smtp_settings!
when :sendmail
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.delivery_method = Setting.email_delivery_method
end
end
rescue StandardError => e
Rails.logger.warn "Unable to set ActionMailer settings (#{e.message}). " \
'Email sending will most likely NOT work.'
end
# This is used to configure email sending from users who prefer to
# continue using environment variables of configuration.yml settings. Our
# hosted SaaS version requires this.
def configure_legacy_action_mailer
return true if self['email_delivery_method'].blank?
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.delivery_method = self['email_delivery_method'].to_sym
%w[smtp_ sendmail_].each do |config_type|
config = settings_of_prefix(config_type)
next if config.empty?
ActionMailer::Base.send("#{config_type}settings=", config)
end
end
private
def reload_smtp_settings!
# Correct smtp settings when using authentication :none
authentication = Setting.smtp_authentication.try(:to_sym)
keys = %i[address port domain authentication user_name password]
if authentication == :none
# Rails Mailer will croak if passing :none as the authentication.
# Instead, it requires to be removed from its settings
ActionMailer::Base.smtp_settings.delete :user_name
ActionMailer::Base.smtp_settings.delete :password
ActionMailer::Base.smtp_settings.delete :authentication
keys = %i[address port domain]
end
keys.each do |setting|
value = Setting["smtp_#{setting}"]
if value.present?
ActionMailer::Base.smtp_settings[setting] = value
else
ActionMailer::Base.smtp_settings.delete setting
end
end
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = Setting.smtp_enable_starttls_auto?
ActionMailer::Base.smtp_settings[:ssl] = Setting.smtp_ssl?
Setting.smtp_openssl_verify_mode.tap do |mode|
ActionMailer::Base.smtp_settings[:openssl_verify_mode] = mode unless mode.nil?
end
end
def cache_parameters
mapping = {
'cache_expires_in_seconds' => %i[expires_in to_i],
'cache_namespace' => %i[namespace to_s]
}
parameters = {}
mapping.each_pair do |from, to|
if self[from]
to_key, method = to
parameters[to_key] = self[from].method(method).call
end
end
parameters
end
def method_missing(name, *args, &block)
setting_name = name.to_s.sub(/\?$/, '')
definition = Settings::Definition[setting_name]
if definition
define_config_methods(definition)
send(name, *args, &block)
else
super
end
end
def respond_to_missing?(name, include_private = false)
Settings::Definition.exists?(name.to_s.sub(/\?$/, '')) || super
end
def define_config_methods(definition)
define_singleton_method definition.name do
self[definition.name]
end
define_singleton_method "#{definition.name}?" do
if definition.format != :boolean
ActiveSupport::Deprecation.warn "Calling #{self}.#{definition.name}? is deprecated since it is not a boolean", caller
end
['true', true, '1'].include? self[definition.name]
end
end
# Filters a hash with String keys by a key prefix and removes the prefix from the keys
def settings_of_prefix(prefix)
Settings::Definition
.all_of_prefix(prefix)
.to_h { |setting| [setting.name.delete_prefix(prefix), setting.value] }
.symbolize_keys!
end
end
end
end

@ -62,8 +62,9 @@ module OpenProject::Plugins
app.config.i18n.load_path += Dir[config.root.join('config', 'locales', 'crowdin', '*.{rb,yml}').to_s]
end
initializer "#{engine_name}.register_cell_view_paths" do |_app|
pathname = config.root.join("app/cells/views")
current_engine = self
config.to_prepare do
pathname = current_engine.root.join("app/cells/views")
::RailsCell.view_paths << pathname.to_path if pathname.exist?
end

@ -331,10 +331,13 @@ module Redmine #:nodoc:
# permission :destroy_contacts, { contacts: :destroy }
# end
def project_module(name, options = {}, &block)
@project_scope = [name, options]
instance_eval(&block)
plugin = self
Rails.application.reloader.to_prepare do
plugin.instance_eval { @project_scope = [name, options] }
plugin.instance_eval(&block)
end
ensure
@project_scope = nil
plugin.instance_eval { @project_scope = nil }
end
# Registers an activity provider.

@ -32,8 +32,8 @@ namespace :code do
Dir.chdir(File.join(File.dirname(__FILE__), '../..')) do
files = Dir['**/**{.rb,.html.erb,.rhtml,.rjs,.plain.erb,.rxml,.yml,.rake,.eml}']
files.reject! do |f|
f.include?('lib/plugins') ||
f.include?('lib/diff')
f.include?('lib_static/plugins') ||
f.include?('lib_static/diff')
end
# handle files in chunks of 50 to avoid too long command lines

@ -143,7 +143,7 @@ namespace :copyright do
desc 'Update the copyright on .rb source files'
task :update_rb, :arg1 do |_task, args|
excluded = (%w(acts_as_tree rfpdf verification).map { |dir| "lib/plugins/#{dir}" })
excluded = (%w(acts_as_tree rfpdf verification).map { |dir| "lib_static/plugins/#{dir}" })
rewrite_copyright('rb', excluded, :rb, args[:arg1])
end

@ -66,17 +66,17 @@ namespace :packager do
Setting.host_name = ENV.fetch('SERVER_HOSTNAME', Setting.host_name)
if ENV['SERVER_PROTOCOL_HTTPS_NO_HSTS']
# Allow setting only the Setting.protocol without enabling FORCE__SSL
# due to external proxy configuration
Setting.protocol = 'https'
# Allow setting only HTTPS setting without enabling FORCE__SSL
# due to external proxy configuration. This avoids activation of HSTS headers.
shell_setup(['config:set', "OPENPROJECT_HTTPS=true"])
shell_setup(['config:unset', "OPENPROJECT_RAILS__FORCE__SSL"])
elsif ENV['SERVER_PROTOCOL_FORCE_HTTPS'] || ENV.fetch('SERVER_PROTOCOL', Setting.protocol) == 'https'
# Allow overriding the protocol setting from ENV
# to allow instances where SSL is terminated earlier to respect that setting
Setting.protocol = 'https'
shell_setup(['config:set', "OPENPROJECT_HTTPS=true"])
shell_setup(['config:set', "OPENPROJECT_RAILS__FORCE__SSL=true"])
else
Setting.protocol = 'http'
shell_setup(['config:set', "OPENPROJECT_HTTPS=false"])
shell_setup(['config:unset', "OPENPROJECT_RAILS__FORCE__SSL"])
end

@ -27,7 +27,6 @@
#++
require 'optparse'
require 'plugins/load_path_helper'
begin
Bundler.gem('parallel_tests')

@ -1,3 +1,29 @@
# OpenProject is an open source project management software.
# Copyright (C) 2022 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
require 'open_project/authentication/manager'
module OpenProject

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save