mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
create backups via UI (#9136)
* create backups via UI * Fix import of modal service * introduced backup token and addressed remaining comments * allow disabling permissions * improvements - only make user wait to use backup token in if really necessary - notify admins of new backup token - disable 'include attachments' option in UI if unavailable - documentation - misc * spec fixes * fixed feature spec * allow setting capybara host in every case * removed unused style file * addressed review feedback, added further feature specs * polish (code climate) * Avoid empty attachments * Don't raise filesize validation for internal exports Co-authored-by: Oliver Günther <mail@oliverguenther.de>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
module Backups
|
||||
class CreateContract < ::ModelContract
|
||||
include SingleTableInheritanceModelContract
|
||||
|
||||
validate :user_allowed_to_create_backup
|
||||
validate :backup_token
|
||||
validate :backup_limit
|
||||
validate :no_pending_backups
|
||||
|
||||
private
|
||||
|
||||
def backup_token
|
||||
token = Token::Backup.find_by_plaintext_value options[:backup_token].to_s
|
||||
|
||||
if token.blank? || token.user_id != user.id
|
||||
errors.add :base, :invalid_token, message: I18n.t("backup.error.invalid_token")
|
||||
else
|
||||
check_waiting_period token
|
||||
end
|
||||
end
|
||||
|
||||
def check_waiting_period(token)
|
||||
if token.waiting?
|
||||
valid_at = token.created_at + OpenProject::Configuration.backup_initial_waiting_period
|
||||
hours = ((valid_at - Time.zone.now) / 60.0 / 60.0).round
|
||||
|
||||
errors.add :base, :token_cooldown, message: I18n.t("backup.error.token_cooldown", hours: hours)
|
||||
end
|
||||
end
|
||||
|
||||
def backup_limit
|
||||
limit = OpenProject::Configuration.backup_daily_limit
|
||||
if Backup.where("created_at >= ?", Time.zone.today).count > limit
|
||||
errors.add :base, :limit_reached, message: I18n.t("backup.error.limit_reached", limit: limit)
|
||||
end
|
||||
end
|
||||
|
||||
def no_pending_backups
|
||||
current_backup = Backup.last
|
||||
if pending_statuses.include? current_backup&.job_status&.status
|
||||
errors.add :base, :backup_pending, message: I18n.t("backup.error.backup_pending")
|
||||
end
|
||||
end
|
||||
|
||||
def user_allowed_to_create_backup
|
||||
errors.add :base, :error_unauthorized unless user_allowed_to_create_backup?
|
||||
end
|
||||
|
||||
def user_allowed_to_create_backup?
|
||||
user.allowed_to_globally? Backup.permission
|
||||
end
|
||||
|
||||
def pending_statuses
|
||||
::JobStatus::Status.statuses.slice(:in_queue, :in_process).values
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
module SingleTableInheritanceModelContract
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
attribute model.inheritance_column
|
||||
|
||||
validate do
|
||||
if model.type != model.class.sti_name
|
||||
errors.add :type, :error_readonly # as in users should not be passing this
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
class Admin::BackupsController < ApplicationController
|
||||
include ActionView::Helpers::TagHelper
|
||||
include BackupHelper
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :check_enabled
|
||||
before_action :require_admin
|
||||
|
||||
menu_item :backups
|
||||
|
||||
def show
|
||||
@backup_token = Token::Backup.find_by user: current_user
|
||||
last_backup = Backup.last
|
||||
|
||||
if last_backup
|
||||
@job_status_id = last_backup.job_status.job_id
|
||||
@last_backup_date = I18n.localize(last_backup.updated_at)
|
||||
@last_backup_attachment_id = last_backup.attachments.first&.id
|
||||
end
|
||||
|
||||
@may_include_attachments = may_include_attachments? ? "true" : "false"
|
||||
end
|
||||
|
||||
def reset_token
|
||||
@backup_token = Token::Backup.find_by user: current_user
|
||||
end
|
||||
|
||||
def perform_token_reset
|
||||
token = create_backup_token user: current_user
|
||||
|
||||
token_reset_successful! token
|
||||
rescue StandardError => e
|
||||
token_reset_failed! e
|
||||
ensure
|
||||
redirect_to action: 'show'
|
||||
end
|
||||
|
||||
def delete_token
|
||||
Token::Backup.where(user: current_user).destroy_all
|
||||
|
||||
flash[:info] = t("backup.text_token_deleted")
|
||||
|
||||
redirect_to action: 'show'
|
||||
end
|
||||
|
||||
def default_breadcrumb
|
||||
t(:label_backup)
|
||||
end
|
||||
|
||||
def show_local_breadcrumb
|
||||
true
|
||||
end
|
||||
|
||||
def check_enabled
|
||||
render_404 unless OpenProject::Configuration.backup_enabled?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def token_reset_successful!(token)
|
||||
notify_user_and_admins current_user, backup_token: token
|
||||
|
||||
flash[:warning] = token_reset_flash_message token
|
||||
end
|
||||
|
||||
def token_reset_flash_message(token)
|
||||
[
|
||||
t('my.access_token.notice_reset_token', type: 'Backup').html_safe,
|
||||
content_tag(:strong, token.plain_value),
|
||||
t('my.access_token.token_value_warning')
|
||||
]
|
||||
end
|
||||
|
||||
def token_reset_failed!(e)
|
||||
Rails.logger.error "Failed to reset user ##{current_user.id}'s Backup key: #{e}"
|
||||
|
||||
flash[:error] = t('my.access_token.failed_to_reset_token', error: e.message)
|
||||
end
|
||||
|
||||
def may_include_attachments?
|
||||
Backup.include_attachments? && Backup.attachments_size_in_bounds?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
module BackupHelper
|
||||
##
|
||||
# The idea here is to only allow users, who can confirm their password, to backup
|
||||
# OpenProject without delay. Users who can't (since they use Google etc.) have to wait
|
||||
# just to make sure no one else accessed the computer to trigger a backup.
|
||||
#
|
||||
# A better long-term solution might be to introduce a PIN for sensitive operations
|
||||
# in general. Think the PIN for Windows users or trading passwords in online trade platforms.
|
||||
#
|
||||
# Also we make sure that in case there is a password that it wasn't just set by a would-be attacker.
|
||||
#
|
||||
# If OpenProject has just been installed we don't check any of this since there's likely nothing
|
||||
# sensitive to backup yet and it would prevent a new admin from trying this feature.
|
||||
def allow_instant_backup_for_user?(user, date: instant_backup_threshold_date)
|
||||
return true if just_installed_openproject? after: date
|
||||
|
||||
# user doesn't use OpenIDConnect (so can be asked to confirm their password)
|
||||
!user.uses_external_authentication? &&
|
||||
# user cannot change password in OP (LDAP) or hasn't changed it recently
|
||||
(user.passwords.empty? || user.passwords.first.updated_at < date)
|
||||
end
|
||||
|
||||
def instant_backup_threshold_date
|
||||
DateTime.now - OpenProject::Configuration.backup_initial_waiting_period
|
||||
end
|
||||
|
||||
def just_installed_openproject?(after: instant_backup_threshold_date)
|
||||
created_at = Project.order(created_at: :asc).limit(1).pick(:created_at)
|
||||
|
||||
created_at && created_at >= after
|
||||
end
|
||||
|
||||
def create_backup_token(user: current_user)
|
||||
token = Token::Backup.create! user: user
|
||||
|
||||
# activate token right away as user had to confirm password
|
||||
date = instant_backup_threshold_date
|
||||
if allow_instant_backup_for_user? user, date: date
|
||||
token.update_column :created_at, date
|
||||
end
|
||||
|
||||
token
|
||||
end
|
||||
|
||||
def notify_user_and_admins(user, backup_token:)
|
||||
waiting_period = backup_token.waiting? && OpenProject::Configuration.backup_initial_waiting_period
|
||||
users = ([user] + User.admin.active).uniq
|
||||
|
||||
users.each do |recipient|
|
||||
UserMailer.backup_token_reset(recipient, user: user, waiting_period: waiting_period).deliver_later
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -90,6 +90,28 @@ class UserMailer < BaseMailer
|
||||
end
|
||||
end
|
||||
|
||||
def backup_ready(user)
|
||||
User.execute_as user do
|
||||
@download_url = admin_backups_url
|
||||
|
||||
with_locale_for(user) do
|
||||
mail to: user.mail, subject: I18n.t("mail_subject_backup_ready")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def backup_token_reset(recipient, user:, waiting_period: OpenProject::Configuration.backup_initial_waiting_period)
|
||||
@admin_notification = recipient != user # notification for other admins rather than oneself
|
||||
@user_login = user.login
|
||||
@waiting_period = waiting_period
|
||||
|
||||
User.execute_as recipient do
|
||||
with_locale_for(recipient) do
|
||||
mail to: recipient.mail, subject: I18n.t("mail_subject_backup_token_reset")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def password_lost(token)
|
||||
return unless token.user # token's can have no user
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ class Attachment < ApplicationRecord
|
||||
validates_length_of :description, maximum: 255
|
||||
|
||||
validate :filesize_below_allowed_maximum,
|
||||
:container_changed_more_than_once
|
||||
if: -> { !internal_container? }
|
||||
validate :container_changed_more_than_once
|
||||
|
||||
acts_as_journalized
|
||||
acts_as_event title: -> { file.name },
|
||||
@@ -302,6 +303,10 @@ class Attachment < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def internal_container?
|
||||
container&.is_a?(Export)
|
||||
end
|
||||
|
||||
def container_changed_more_than_once
|
||||
if container_id_changed_more_than_once? || container_type_changed_more_than_once?
|
||||
errors.add(:container, :unchangeable)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
class Backup < Export
|
||||
class << self
|
||||
def permission
|
||||
:create_backup
|
||||
end
|
||||
|
||||
def include_attachments?
|
||||
val = OpenProject::Configuration.backup_include_attachments
|
||||
|
||||
val.nil? ? true : val.to_s.to_bool # default to true
|
||||
end
|
||||
|
||||
##
|
||||
# Don't include attachments in archive if they are larger than
|
||||
# this value combined.
|
||||
def attachment_size_max_sum_mb
|
||||
(OpenProject::Configuration.backup_attachment_size_max_sum_mb.presence || 1024).to_i
|
||||
end
|
||||
|
||||
def attachments_query
|
||||
Attachment
|
||||
.where.not(container_type: nil)
|
||||
.where.not(container_type: Export.name)
|
||||
end
|
||||
|
||||
def attachments_size_in_mb(attachments_query = self.attachments_query)
|
||||
attachments_query.pluck(:filesize).sum / 1024.0 / 1024.0
|
||||
end
|
||||
|
||||
def attachments_size_in_bounds?(attachments_query = self.attachments_query, max: self.attachment_size_max_sum_mb)
|
||||
attachments_size_in_mb(attachments_query) <= max
|
||||
end
|
||||
end
|
||||
|
||||
acts_as_attachable(
|
||||
view_permission: permission,
|
||||
add_permission: permission,
|
||||
delete_permission: permission,
|
||||
only_user_allowed: true
|
||||
)
|
||||
|
||||
def ready?
|
||||
attachments.any?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
class Export < ApplicationRecord
|
||||
has_one(
|
||||
:job_status,
|
||||
-> { where(reference_type: "Export") },
|
||||
class_name: "JobStatus::Status",
|
||||
foreign_key: :reference
|
||||
)
|
||||
|
||||
def ready?
|
||||
raise "subclass responsibility"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
module Token
|
||||
class Backup < HashedToken
|
||||
def ready?
|
||||
return false if created_at.nil?
|
||||
|
||||
created_at.since(OpenProject::Configuration.backup_initial_waiting_period).past?
|
||||
end
|
||||
|
||||
def waiting?
|
||||
!ready?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,4 @@
|
||||
class WorkPackages::Export < ApplicationRecord
|
||||
self.table_name = 'work_package_exports'
|
||||
|
||||
class WorkPackages::Export < Export
|
||||
acts_as_attachable view_permission: :export_work_packages,
|
||||
add_permission: :export_work_packages,
|
||||
delete_permission: :export_work_packages,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
module Backups
|
||||
class CreateService < ::BaseServices::Create
|
||||
def initialize(user:, backup_token:, include_attachments: true, contract_class: ::Backups::CreateContract)
|
||||
super user: user, contract_class: contract_class, contract_options: { backup_token: backup_token }
|
||||
|
||||
@include_attachments = include_attachments
|
||||
end
|
||||
|
||||
def include_attachments?
|
||||
@include_attachments
|
||||
end
|
||||
|
||||
def after_perform(call)
|
||||
if call.success?
|
||||
BackupJob.perform_later(
|
||||
backup: call.result,
|
||||
user: user,
|
||||
include_attachments: include_attachments?
|
||||
)
|
||||
end
|
||||
|
||||
call
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
module Backups
|
||||
class SetAttributesService < ::BaseServices::SetAttributes
|
||||
end
|
||||
end
|
||||
@@ -37,7 +37,7 @@ module Contracted
|
||||
|
||||
def contract_class=(cls)
|
||||
unless cls <= ::BaseContract
|
||||
raise ArgumentError "#{cls.name} is not an instance of BaseContract."
|
||||
raise ArgumentError, "#{cls.name} is not an instance of BaseContract."
|
||||
end
|
||||
|
||||
@contract_class = cls
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<% action = @backup_token.present? ? 'reset' : 'create' %>
|
||||
<% icon = @backup_token.present? ? 'delete' : 'add' %>
|
||||
|
||||
<% html_title(t(:label_administration), t("backup.reset_token.heading_#{action}")) -%>
|
||||
|
||||
<%= labelled_tabular_form_for(
|
||||
:user,
|
||||
url: { action: 'reset_token' },
|
||||
html: {
|
||||
method: :post, class: 'confirm_required request-for-confirmation form danger-zone',
|
||||
data: { "request-for-confirmation": true }
|
||||
}
|
||||
) do %>
|
||||
<div class='wiki'>
|
||||
<section class="form--section">
|
||||
<h3 class="form--section-title">
|
||||
<%= t("backup.reset_token.heading_#{action}") %>
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
<%= t("backup.reset_token.implications") %>
|
||||
</p>
|
||||
|
||||
<% if !allow_instant_backup_for_user? current_user %>
|
||||
<p class="danger-zone--warning">
|
||||
<span class="icon icon-error"></span>
|
||||
<span><%= t("backup.reset_token.warning") %></span>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<p>
|
||||
<%= t(
|
||||
"backup.reset_token.verification",
|
||||
word: "<em class=\"danger-zone--expected-value\">#{t("backup.reset_token.verification_word_#{action}")}</em>",
|
||||
action: action
|
||||
).html_safe %>
|
||||
</p>
|
||||
<div class="danger-zone--verification">
|
||||
<input type="text" name="login_verification"/>
|
||||
<%= styled_button_tag '', class: '-highlight', disabled: true do
|
||||
concat content_tag :i, '', class: "button--icon icon-#{icon}"
|
||||
concat content_tag :span, t("backup.reset_token.action_#{action}"), class: 'button--text'
|
||||
end %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,96 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<% html_title t(:label_administration), t(:label_backup) -%>
|
||||
|
||||
<%= toolbar title: t('label_backup') do %>
|
||||
<li class="toolbar-item">
|
||||
<% label_action = @backup_token.present? ? 'reset' : 'create' %>
|
||||
<% label = t("backup.label_#{label_action}_token") %>
|
||||
<%=
|
||||
link_to(
|
||||
{ action: 'reset_token' },
|
||||
method: :get,
|
||||
class: 'button -alt-highlight',
|
||||
aria: {label: label},
|
||||
title: label
|
||||
) do
|
||||
%>
|
||||
<%= op_icon("button--icon icon-#{@backup_token.present? ? 'reload' : 'add'}") %>
|
||||
<span class="button--text"><%= t('backup.label_backup_token') %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
<% if @backup_token.present? %>
|
||||
<li class="toolbar-item">
|
||||
<% label = t("backup.label_delete_token") %>
|
||||
<%=
|
||||
link_to(
|
||||
{ action: 'delete_token' },
|
||||
method: :post,
|
||||
class: 'button -alt-highlight',
|
||||
aria: {label: label},
|
||||
title: label
|
||||
) do
|
||||
%>
|
||||
<%= op_icon("button--icon icon-delete") %>
|
||||
<span class="button--text"><%= t('backup.label_backup_token') %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<p>
|
||||
<%= t("backup.reset_token.info") %>
|
||||
</p>
|
||||
|
||||
<% if Token::Backup.count > 0 %>
|
||||
<p>
|
||||
<span><%= I18n.t("backup.label_token_users") %></span>:
|
||||
|
||||
<div class="wiki">
|
||||
<ul>
|
||||
<% Token::Backup.all.includes(:user).each do |token| %>
|
||||
<li>
|
||||
<%= link_to token.user.name, edit_user_path(token.user) %>
|
||||
<%= token.user == current_user ? "(#{I18n.t(:you)})" : '' %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% if @backup_token.present? %>
|
||||
<%= tag :backup, data: {
|
||||
'job-status-id': @job_status_id,
|
||||
'last-backup-date': @last_backup_date,
|
||||
'last-backup-attachment-id': @last_backup_attachment_id,
|
||||
'may-include-attachments': @may_include_attachments
|
||||
} %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,33 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<p>
|
||||
<%= t(:mail_body_backup_ready) %><br />
|
||||
<%= link_to(@download_url, @download_url) %>
|
||||
</p>
|
||||
@@ -0,0 +1,31 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%= t(:mail_body_backup_ready) %>
|
||||
<%= @download_url %>
|
||||
@@ -0,0 +1,45 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<p>
|
||||
<%= @admin_notification ?
|
||||
t(:mail_body_backup_token_reset_admin_info, user: @user_login) :
|
||||
t(:mail_body_backup_token_reset_user_info)
|
||||
%>
|
||||
<% if @waiting_period %>
|
||||
|
||||
<%= t(:mail_body_backup_waiting_period hours: @waiting_period.in_hours.to_i) %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<% if !@admin_notification %>
|
||||
<p>
|
||||
<%= t(:mail_body_backup_token_warning) %>
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -0,0 +1,35 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%= @admin_notification ?
|
||||
t(:mail_body_backup_token_reset_admin_info, user: @user_login) :
|
||||
t(:mail_body_backup_token_reset_user_info)
|
||||
%><% if @waiting_period %> <%= t(:mail_body_backup_waiting_period, hours: @waiting_period.in_hours.to_i) %><% end %>
|
||||
|
||||
<% if !@admin_notification %><%= t(:mail_body_backup_token_warning) %><% end %>
|
||||
@@ -0,0 +1,239 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
require 'tempfile'
|
||||
require 'zip'
|
||||
|
||||
class BackupJob < ::ApplicationJob
|
||||
queue_with_priority :low
|
||||
|
||||
attr_reader :backup, :user
|
||||
|
||||
def perform(
|
||||
backup:,
|
||||
user:,
|
||||
include_attachments: Backup.include_attachments?,
|
||||
attachment_size_max_sum_mb: Backup.attachment_size_max_sum_mb
|
||||
)
|
||||
@backup = backup
|
||||
@user = user
|
||||
@include_attachments = include_attachments
|
||||
@attachment_size_max_sum_mb = attachment_size_max_sum_mb
|
||||
|
||||
run_backup!
|
||||
rescue StandardError => e
|
||||
failure! error: e.message
|
||||
|
||||
raise e
|
||||
ensure
|
||||
remove_files! db_dump_file_name, archive_file_name
|
||||
|
||||
attachments.each(&:destroy) unless success?
|
||||
|
||||
Rails.logger.info(
|
||||
"BackupJob(include_attachments: #{include_attachments}) finished " \
|
||||
"with status #{job_status.status} " \
|
||||
"(dumped: #{dumped?}, archived: #{archived?})"
|
||||
)
|
||||
end
|
||||
|
||||
def run_backup!
|
||||
@dumped = dump_database! db_dump_file_name # sets error on failure
|
||||
|
||||
return unless dumped?
|
||||
|
||||
file_name = create_backup_archive!(
|
||||
file_name: archive_file_name,
|
||||
db_dump_file_name: db_dump_file_name
|
||||
)
|
||||
|
||||
store_backup file_name, backup: backup, user: user
|
||||
cleanup_previous_backups!
|
||||
|
||||
UserMailer.backup_ready(user).deliver_later
|
||||
end
|
||||
|
||||
def dumped?
|
||||
@dumped
|
||||
end
|
||||
|
||||
def archived?
|
||||
@archived
|
||||
end
|
||||
|
||||
def db_dump_file_name
|
||||
@db_dump_file_name ||= tmp_file_name "openproject", ".sql"
|
||||
end
|
||||
|
||||
def archive_file_name
|
||||
@archive_file_name ||= tmp_file_name "openproject-backup", ".zip"
|
||||
end
|
||||
|
||||
def status_reference
|
||||
arguments.first[:backup]
|
||||
end
|
||||
|
||||
def updates_own_status?
|
||||
true
|
||||
end
|
||||
|
||||
def cleanup_previous_backups!
|
||||
Backup.where.not(id: backup.id).destroy_all
|
||||
end
|
||||
|
||||
def success?
|
||||
job_status.status == JobStatus::Status.statuses[:success]
|
||||
end
|
||||
|
||||
def remove_files!(*files)
|
||||
Array(files).each do |file|
|
||||
FileUtils.rm file if File.exists? file
|
||||
end
|
||||
end
|
||||
|
||||
def store_backup(file_name, backup:, user:)
|
||||
File.open(file_name) do |file|
|
||||
attachment = Attachments::CreateService
|
||||
.new(backup, author: user)
|
||||
.call(uploaded_file: file, description: 'OpenProject backup')
|
||||
|
||||
download_url = ::API::V3::Utilities::PathHelper::ApiV3Path.attachment_content(attachment.id)
|
||||
|
||||
upsert_status(
|
||||
status: :success,
|
||||
message: I18n.t('export.succeeded'),
|
||||
payload: download_payload(download_url)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def create_backup_archive!(file_name:, db_dump_file_name:, attachments: attachments_to_include)
|
||||
Zip::File.open(file_name, Zip::File::CREATE) do |zipfile|
|
||||
attachments.each do |attachment|
|
||||
# If an attachment is destroyed on disk, skip i
|
||||
diskfile = attachment.diskfile
|
||||
next unless diskfile
|
||||
|
||||
path = diskfile.path
|
||||
|
||||
zipfile.add "attachment/file/#{attachment.id}/#{attachment[:file]}", path
|
||||
end
|
||||
|
||||
zipfile.get_output_stream("openproject.sql") { |f| f.write File.read(db_dump_file_name) }
|
||||
end
|
||||
|
||||
@archived = true
|
||||
|
||||
file_name
|
||||
end
|
||||
|
||||
def attachments_to_include
|
||||
return Attachment.none if skip_attachments?
|
||||
|
||||
Backup.attachments_query
|
||||
end
|
||||
|
||||
def skip_attachments?
|
||||
!(include_attachments? && Backup.attachments_size_in_bounds?(max: attachment_size_max_sum_mb))
|
||||
end
|
||||
|
||||
def date_tag
|
||||
Time.zone.today.iso8601
|
||||
end
|
||||
|
||||
def tmp_file_name(name, ext)
|
||||
file = Tempfile.new [name, ext]
|
||||
|
||||
file.path
|
||||
ensure
|
||||
file.close
|
||||
file.unlink
|
||||
end
|
||||
|
||||
def include_attachments?
|
||||
@include_attachments
|
||||
end
|
||||
|
||||
def attachment_size_max_sum_mb
|
||||
@attachment_size_max_sum_mb
|
||||
end
|
||||
|
||||
def dump_database!(path)
|
||||
_out, err, st = Open3.capture3 pg_env, "pg_dump -x -O -f '#{path}'"
|
||||
|
||||
failure! error: err unless st.success?
|
||||
|
||||
st.success?
|
||||
end
|
||||
|
||||
def success!
|
||||
payload = download_payload(url_helpers.backups_path(target_project))
|
||||
|
||||
if errors.any?
|
||||
payload[:errors] = errors
|
||||
end
|
||||
|
||||
upsert_status status: :success,
|
||||
message: I18n.t('copy_project.succeeded', target_project_name: target_project.name),
|
||||
payload: payload
|
||||
end
|
||||
|
||||
def failure!(error: nil)
|
||||
msg = I18n.t 'backup.failed'
|
||||
|
||||
upsert_status(
|
||||
status: :failure,
|
||||
message: error.present? ? "#{msg}: #{error}" : msg
|
||||
)
|
||||
end
|
||||
|
||||
def pg_env
|
||||
config = ActiveRecord::Base.connection_db_config.configuration_hash
|
||||
entries = pg_env_to_connection_config.map do |key, config_key|
|
||||
value = config[config_key].to_s
|
||||
|
||||
[key.to_s, value] if value.present?
|
||||
end
|
||||
|
||||
entries.compact.to_h
|
||||
end
|
||||
|
||||
##
|
||||
# Maps the PG env variable name to the key in the AR connection config.
|
||||
def pg_env_to_connection_config
|
||||
{
|
||||
PGHOST: :host,
|
||||
PGPORT: :port,
|
||||
PGUSER: :username,
|
||||
PGPASSWORD: :password,
|
||||
PGDATABASE: :database
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -334,6 +334,13 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
last: true,
|
||||
icon: 'icon2 icon-plugins'
|
||||
|
||||
menu.push :backups,
|
||||
{ controller: '/admin/backups', action: 'show' },
|
||||
if: Proc.new { OpenProject::Configuration.backup_enabled? && User.current.admin? },
|
||||
caption: :label_backup,
|
||||
last: true,
|
||||
icon: 'icon2 icon-save'
|
||||
|
||||
menu.push :info,
|
||||
{ controller: '/admin', action: 'info' },
|
||||
if: Proc.new { User.current.admin? },
|
||||
|
||||
@@ -50,6 +50,12 @@ OpenProject::AccessControl.map do |map|
|
||||
global: true,
|
||||
contract_actions: { projects: %i[create] }
|
||||
|
||||
map.permission Backup.permission,
|
||||
{ backups: %i[index] },
|
||||
require: :loggedin,
|
||||
global: true,
|
||||
enabled: -> { OpenProject::Configuration.backup_enabled? }
|
||||
|
||||
map.permission :manage_user,
|
||||
{
|
||||
users: %i[index show new create edit update resend_invitation],
|
||||
|
||||
@@ -874,6 +874,40 @@ en:
|
||||
user: "User"
|
||||
version: "Version"
|
||||
work_package: "Work package"
|
||||
|
||||
backup:
|
||||
label_backup_token: "Backup token"
|
||||
label_create_token: "Create backup token"
|
||||
label_delete_token: "Delete backup token"
|
||||
label_reset_token: "Reset backup token"
|
||||
label_token_users: "The following users have active backup tokens"
|
||||
reset_token:
|
||||
action_create: Create
|
||||
action_reset: Reset
|
||||
heading_reset: "Reset backup token"
|
||||
heading_create: "Create backup token"
|
||||
implications: >
|
||||
Enabling backups will allow any user with the required permissions and this backup token
|
||||
to download a backup containing all data of this OpenProject installation.
|
||||
This includes the data of all other users.
|
||||
info: >
|
||||
You will need to generate a backup token to be able to create a backup.
|
||||
Each time you want to request a backup you will have to provide this token.
|
||||
You can delete the backup token to disable backups for this user.
|
||||
verification: >
|
||||
Enter %{word} to confirm you want to %{action} the backup token.
|
||||
verification_word_reset: reset
|
||||
verification_word_create: create
|
||||
warning: >
|
||||
When you create a new token you will only be allowed to request a backup after
|
||||
24 hours. This is a safety measure. After that you can request a backup any time using that token.
|
||||
text_token_deleted:
|
||||
Backup token deleted. Backups are now disabled.
|
||||
error:
|
||||
invalid_token: Invalid or missing backup token
|
||||
token_cooldown: The backup token will be valid in %{hours} hours.
|
||||
backup_pending: There is already a backup pending.
|
||||
limit_reached: You can only do %{limit} backups per day.
|
||||
|
||||
button_add: "Add"
|
||||
button_add_comment: "Add comment"
|
||||
@@ -1338,6 +1372,7 @@ en:
|
||||
label_available_project_versions: 'Available versions'
|
||||
label_available_project_repositories: 'Available repositories'
|
||||
label_api_documentation: "API documentation"
|
||||
label_backup: "Backup"
|
||||
label_between: "between"
|
||||
label_blocked_by: "blocked by"
|
||||
label_blocks: "blocks"
|
||||
@@ -1867,6 +1902,12 @@ en:
|
||||
mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
|
||||
mail_body_account_information: "Your account information"
|
||||
mail_body_account_information_external: "You can use your %{value} account to log in."
|
||||
mail_body_backup_ready: "Your requested backup is ready. You can download it here:"
|
||||
mail_body_backup_token_reset_admin_info: The backup token for user '%{user}' has been reset.
|
||||
mail_body_backup_token_reset_user_info: Your backup token has been reset.
|
||||
mail_body_backup_token_info: The previous token is no longer valid.
|
||||
mail_body_backup_waiting_period: The new token will be enabled in %{hours} hours.
|
||||
mail_body_backup_token_warning: If this wasn't you, login to OpenProject immediately and reset it again.
|
||||
mail_body_lost_password: "To change your password, click on the following link:"
|
||||
mail_body_register: "Welcome to OpenProject. Please activate your account by clicking on this link:"
|
||||
mail_body_register_header_title: "Project member invitation email"
|
||||
@@ -1883,6 +1924,8 @@ en:
|
||||
mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
|
||||
mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
|
||||
mail_subject_account_activation_request: "%{value} account activation request"
|
||||
mail_subject_backup_ready: "Your backup is ready"
|
||||
mail_subject_backup_token_reset: "Your backup token has been reset"
|
||||
mail_subject_lost_password: "Your %{value} password"
|
||||
mail_subject_register: "Your %{value} account activation"
|
||||
mail_subject_reminder: "%{count} work package(s) due in the next %{days} days"
|
||||
@@ -2843,6 +2886,7 @@ en:
|
||||
code_403: "You are not authorized to access this resource."
|
||||
code_404: "The requested resource could not be found."
|
||||
code_409: "Could not update the resource because of conflicting modifications."
|
||||
code_429: "Too many requests. Please try again later."
|
||||
code_500: "An internal error has occurred."
|
||||
expected:
|
||||
date: "YYYY-MM-DD (ISO 8601 date only)"
|
||||
@@ -2976,3 +3020,5 @@ en:
|
||||
authorization_error: "An authorization error has occurred."
|
||||
revoke_my_application_confirmation: "Do you really want to remove this application? This will revoke %{token_count} active for it."
|
||||
my_registered_applications: "Registered OAuth applications"
|
||||
|
||||
you: you
|
||||
|
||||
@@ -43,6 +43,25 @@ en:
|
||||
single: "Select \"%{name}\""
|
||||
remove: "Remove %{name}"
|
||||
active: "Active %{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).
|
||||
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
|
||||
|
||||
close_popup_title: "Close popup"
|
||||
close_filter_title: "Close filter"
|
||||
close_form_title: "Close form"
|
||||
|
||||
@@ -407,6 +407,15 @@ OpenProject::Application.routes.draw do
|
||||
get 'plugin/:id', action: :show_plugin
|
||||
post 'plugin/:id', action: :update_plugin
|
||||
end
|
||||
|
||||
resource :backups, controller: '/admin/backups', only: %i[show] do
|
||||
collection do
|
||||
get :reset_token
|
||||
post :reset_token, action: :perform_token_reset
|
||||
|
||||
post :delete_token
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resource :workflows, only: %i[edit update show] do
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class GeneralizeExports < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
rename_table :work_package_exports, :exports
|
||||
|
||||
change_table :exports do |t|
|
||||
t.string :type
|
||||
end
|
||||
|
||||
reversible do |dir|
|
||||
dir.up do
|
||||
execute "UPDATE exports SET type = 'WorkPackages::Export'"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -46,6 +46,7 @@ Configuring OpenProject through environment variables is detailed [in this separ
|
||||
* [`global_basic_auth`](#global-basic-auth)
|
||||
* [`apiv3_enable_basic_auth`](#apiv3_enable_basic_auth)
|
||||
* [`enterprise_limits`](#enterprise-limits)
|
||||
* [`backup_enabled`](#backup-enabled)
|
||||
|
||||
## Setting session options
|
||||
|
||||
@@ -387,7 +388,33 @@ Or through the environment like this:
|
||||
OPENPROJECT_ENTERPRISE_FAIL__FAST=true
|
||||
```
|
||||
|
||||
### Backup enabled
|
||||
|
||||
*default: true*
|
||||
|
||||
If enabled, admins (or users with the necessary permission) can download backups of the OpenProject installation
|
||||
via OpenProject's web interface or via the API.
|
||||
|
||||
There are further configurations you can use to adjust your backups.
|
||||
|
||||
```
|
||||
backup_enabled: true # enable/disable backups feature
|
||||
backup_daily_limit: 3 # number of times backups can be requested per day across all users
|
||||
backup_initial_waiting_period: 24.hours # time after which new backup token is usable
|
||||
backup_include_attachments: true # include/exclude attachments besides db dump
|
||||
backup_attachment_size_max_sum_mb: 1024 # if all attachments together are larger than this, they will not be included
|
||||
```
|
||||
|
||||
Per default the maximum overall size of all attachments must not exceed 1GB for them to be included
|
||||
in the backup. If they are larger only the database dump will be included.
|
||||
|
||||
As usual this can be override via the environment, for example like this:
|
||||
|
||||
```
|
||||
OPENPROJECT_BACKUP__ENABLED=true
|
||||
OPENPROJECT_BACKUP__INCLUDE__ATTACHMENTS=true
|
||||
OPENPROJECT_BACKUP__ATTACHMENT__SIZE__MAX__SUM__MB=1024
|
||||
```
|
||||
|
||||
| ----------- | :---------- |
|
||||
| [List of supported environment variables](./environment) | The full list of environment variables you can use to override the default configuration |
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
sidebar_navigation:
|
||||
title: Backup
|
||||
priority: 501
|
||||
description: Backing up OpenProject.
|
||||
robots: index, follow
|
||||
keywords: system backup
|
||||
---
|
||||
# Backup
|
||||
|
||||
Unless disabled via the [configuration](/installation-and-operations/configuration/#enable-user-initiated-backups)
|
||||
users can make backups of the OpenProject installation from within the administration area.
|
||||
They either need to be an administrator or have the global permission to do so.
|
||||
|
||||

|
||||
|
||||
## Backup token
|
||||
|
||||
To be able to create a backup, a so called _backup token_ has to be generated first.
|
||||
This is supposed to add another level of security since backing up the whole installation
|
||||
includes sensitive data.
|
||||
|
||||
You will be asked to confirm your password when you try to generate or reset a token.
|
||||
The _backup token_ will only be displayed once after it has been generated.
|
||||
Make sure you store it in a safe place.
|
||||
|
||||
Each time you request a backup this token has to be provided.
|
||||
This also applies when requesting a backup via the API where on top of the API token
|
||||
the _backup token_ will have to be provided as well.
|
||||
|
||||
## Delayed reset
|
||||
|
||||
If the user resetting (or creating) a backup token does not have a password, for instance because they
|
||||
authenticate using Google, the newly generated backup token will only be valid after an initial waiting period.
|
||||
This is to make sure that no unauthorised user can get their hands on a backup even when accessing
|
||||
a logged-in user's desktop.
|
||||
|
||||
As a system administrator you can skip this period by running the following rake task on the server's terminal:
|
||||
|
||||
```
|
||||
sudo openproject run rake backup:allow_now
|
||||
```
|
||||
|
||||
__In a docker setup you can open a terminal on any of the web or worker processes and run the rake task there.__
|
||||
|
||||
## Notifications
|
||||
|
||||
Each time a _backup token_ is created or reset an email notification will be sent to all administrators
|
||||
take make everyone aware that there is a new user with access to backups.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
@@ -78,6 +78,7 @@ import { OpenprojectInviteUserModalModule } from "core-app/modules/invite-user-m
|
||||
import { OpenprojectModalModule } from "core-app/modules/modal/modal.module";
|
||||
import { RevitAddInSettingsButtonService } from "core-app/modules/bim/revit_add_in/revit-add-in-settings-button.service";
|
||||
import { OpenprojectAutocompleterModule } from "core-app/modules/autocompleter/openproject-autocompleter.module";
|
||||
import { OpenProjectBackupService } from './components/api/op-backup/op-backup.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -153,6 +154,7 @@ import { OpenprojectAutocompleterModule } from "core-app/modules/autocompleter/o
|
||||
{ provide: States, useValue: new States() },
|
||||
{ provide: APP_INITIALIZER, useFactory: initializeServices, deps: [Injector], multi: true },
|
||||
PaginationService,
|
||||
OpenProjectBackupService,
|
||||
OpenProjectFileUploadService,
|
||||
OpenProjectDirectFileUploadService,
|
||||
// Split view
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<form class="form -bordered -compressed" method="get" [action]="getDownloadUrl()" [hidden]="!isDownloadReady()">
|
||||
<section class="form--section">
|
||||
<h3 class="form--section-title">
|
||||
{{ text.lastBackup }}
|
||||
</h3>
|
||||
<div class="form--field">
|
||||
<label class="form--label">{{ text.lastBackupFrom }}:</label>
|
||||
<div class="form--field-container">
|
||||
<div class="form--text-field-container">
|
||||
<em>{{ lastBackupDate }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button name="button" type="submit" class="button">
|
||||
<i class="button--icon icon-save"></i>
|
||||
<span class="button--text">{{ text.downloadBackup }}</span>
|
||||
</button>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<form class="form danger-zone" action="#">
|
||||
<div class='wiki'>
|
||||
<section class="form--section">
|
||||
<h3 class="form--section-title">
|
||||
{{ text.title }}
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
{{ text.info }}
|
||||
</p>
|
||||
|
||||
<p class="danger-zone--warning">
|
||||
<span class="icon icon-error"></span>
|
||||
<span>{{ text.note }}</span>
|
||||
</p>
|
||||
<div>
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend">
|
||||
{{ text.options }}
|
||||
</legend>
|
||||
<label class="form--label-with-check-box" [title]="includeAttachmentsTitle()">
|
||||
<div class="form--check-box-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form--check-box"
|
||||
[checked]="includeAttachments"
|
||||
(change)="includeAttachments = !includeAttachments"
|
||||
[disabled]="!mayIncludeAttachments"
|
||||
>
|
||||
</div>
|
||||
{{ text.includeAttachments }}
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="danger-zone--verification">
|
||||
<input
|
||||
type="password"
|
||||
name="backupToken"
|
||||
placeholder="Backup token"
|
||||
required="required"
|
||||
[value]="backupToken"
|
||||
(input)="backupToken = $event.target.value"
|
||||
#backupTokenInput
|
||||
/>
|
||||
<button name="button" type="submit" class="-highlight button" (click)="triggerBackup($event)" [disabled]="backupToken.length == 0">
|
||||
<i class="button--icon icon-export"></i>
|
||||
<span class="button--text">{{ text.requestBackup }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,125 @@
|
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { AfterViewInit, Component, ElementRef, Injector, ViewChild } from '@angular/core';
|
||||
import { InjectField } from 'core-app/helpers/angular/inject-field.decorator';
|
||||
import { I18nService } from "core-app/modules/common/i18n/i18n.service";
|
||||
import { NotificationsService } from 'core-app/modules/common/notifications/notifications.service';
|
||||
import { OpenProjectBackupService } from '../api/op-backup/op-backup.service';
|
||||
import { JobStatusModal } from "core-app/modules/job-status/job-status-modal/job-status.modal";
|
||||
import { PathHelperService } from 'core-app/modules/common/path-helper/path-helper.service';
|
||||
import { OpModalService } from "core-app/modules/modal/modal.service";
|
||||
|
||||
export const backupSelector = 'backup';
|
||||
|
||||
@Component({
|
||||
selector: backupSelector,
|
||||
templateUrl: './backup.component.html',
|
||||
})
|
||||
export class BackupComponent implements AfterViewInit {
|
||||
public text = {
|
||||
info: this.i18n.t('js.backup.info'),
|
||||
note: this.i18n.t('js.backup.note'),
|
||||
title: this.i18n.t('js.backup.title'),
|
||||
lastBackup: this.i18n.t('js.backup.last_backup'),
|
||||
lastBackupFrom: this.i18n.t('js.backup.last_backup_from'),
|
||||
includeAttachments: this.i18n.t('js.backup.include_attachments'),
|
||||
options: this.i18n.t('js.backup.options'),
|
||||
downloadBackup: this.i18n.t('js.backup.download_backup'),
|
||||
requestBackup: this.i18n.t('js.backup.request_backup'),
|
||||
attachmentsDisabled: this.i18n.t('js.backup.attachments_disabled'),
|
||||
};
|
||||
|
||||
public jobStatusId:string = this.elementRef.nativeElement.dataset['jobStatusId'];
|
||||
public lastBackupDate:string = this.elementRef.nativeElement.dataset['lastBackupDate'];
|
||||
public lastBackupAttachmentId:string = this.elementRef.nativeElement.dataset['lastBackupAttachmentId'];
|
||||
public mayIncludeAttachments:boolean = this.elementRef.nativeElement.dataset['mayIncludeAttachments'] != "false";
|
||||
|
||||
public isInProgress:boolean = false;
|
||||
public includeAttachments:boolean = true;
|
||||
public backupToken:string = "";
|
||||
|
||||
@InjectField() opBackup:OpenProjectBackupService;
|
||||
|
||||
@ViewChild("backupTokenInput") backupTokenInput: ElementRef;
|
||||
|
||||
constructor(
|
||||
readonly elementRef:ElementRef,
|
||||
public injector:Injector,
|
||||
protected i18n:I18nService,
|
||||
protected notificationsService:NotificationsService,
|
||||
protected opModalService:OpModalService,
|
||||
protected pathHelper:PathHelperService
|
||||
) {
|
||||
this.includeAttachments = this.mayIncludeAttachments;
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.backupTokenInput.nativeElement.focus();
|
||||
}
|
||||
|
||||
public isDownloadReady():boolean {
|
||||
return this.jobStatusId !== undefined && this.jobStatusId !== "" &&
|
||||
this.lastBackupAttachmentId !== undefined && this.lastBackupAttachmentId !== "";
|
||||
}
|
||||
|
||||
public getDownloadUrl():string {
|
||||
return this.pathHelper.attachmentDownloadPath(this.lastBackupAttachmentId, undefined);
|
||||
}
|
||||
|
||||
public includeAttachmentsDefault():boolean {
|
||||
return this.mayIncludeAttachments;
|
||||
}
|
||||
|
||||
public includeAttachmentsTitle():string {
|
||||
return this.mayIncludeAttachments ? '' : this.text.attachmentsDisabled;
|
||||
}
|
||||
|
||||
public triggerBackup(event?:JQuery.TriggeredEvent) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
var backupToken = this.backupToken;
|
||||
|
||||
this.backupToken = "";
|
||||
|
||||
this.opBackup
|
||||
.triggerBackup(backupToken, this.includeAttachments)
|
||||
.toPromise()
|
||||
.then((resp:any) => {
|
||||
this.jobStatusId = resp.jobStatusId;
|
||||
this.opModalService.show(JobStatusModal, 'global', { jobId: resp.jobStatusId });
|
||||
})
|
||||
.catch((error:HttpErrorResponse) => {
|
||||
this.notificationsService.addError(error.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {Injectable} from "@angular/core";
|
||||
import {HttpClient, HttpEvent, HttpEventType, HttpResponse} from "@angular/common/http";
|
||||
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
|
||||
import {Observable} from "rxjs";
|
||||
import {HalResourceService} from "core-app/modules/hal/services/hal-resource.service";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class OpenProjectBackupService {
|
||||
constructor(protected http:HttpClient,
|
||||
protected halResource:HalResourceService) {
|
||||
}
|
||||
|
||||
public triggerBackup(backupToken:string, includeAttachments:boolean=true):Observable<HalResource> {
|
||||
return this
|
||||
.http
|
||||
.request<HalResource>(
|
||||
"post",
|
||||
"/api/v3/backups",
|
||||
{
|
||||
body: { backupToken: backupToken, attachments: includeAttachments },
|
||||
withCredentials: true,
|
||||
responseType: "json" as any
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,7 @@ import {
|
||||
editableQueryPropsSelector
|
||||
} from "core-app/modules/admin/editable-query-props/editable-query-props.component";
|
||||
import { SlideToggleComponent, slideToggleSelector } from "core-app/modules/common/slide-toggle/slide-toggle.component";
|
||||
import { BackupComponent, backupSelector } from "./components/admin/backup.component";
|
||||
|
||||
export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
|
||||
{ selector: appBaseSelector, cls: ApplicationBaseComponent },
|
||||
@@ -201,7 +202,8 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
|
||||
{ selector: attributeLabelMacro, cls: AttributeLabelMacroComponent, embeddable: true },
|
||||
{ selector: quickInfoMacroSelector, cls: WorkPackageQuickinfoMacroComponent, embeddable: true },
|
||||
{ selector: editableQueryPropsSelector, cls: EditableQueryPropsComponent },
|
||||
{ selector: slideToggleSelector, cls: SlideToggleComponent }
|
||||
{ selector: slideToggleSelector, cls: SlideToggleComponent },
|
||||
{ selector: backupSelector, cls: BackupComponent }
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -34,8 +34,10 @@ module API
|
||||
identifier 'UpdateConflict'
|
||||
code 409
|
||||
|
||||
def initialize(*)
|
||||
super I18n.t('api_v3.errors.code_409')
|
||||
def initialize(*args)
|
||||
opts = args.last.is_a?(Hash) ? args.last : {}
|
||||
|
||||
super opts[:message] || I18n.t('api_v3.errors.code_409')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
module API
|
||||
module Errors
|
||||
class TooManyRequests < ErrorBase
|
||||
identifier 'TooManyRequests'
|
||||
code 429
|
||||
|
||||
def initialize(*args)
|
||||
opts = args.last.is_a?(Hash) ? args.last : {}
|
||||
|
||||
super opts[:message] || I18n.t('api_v3.errors.code_429')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -34,8 +34,10 @@ module API
|
||||
identifier 'MissingPermission'
|
||||
code 403
|
||||
|
||||
def initialize(*)
|
||||
super I18n.t('api_v3.errors.code_403')
|
||||
def initialize(*args)
|
||||
opts = args.last.is_a?(Hash) ? args.last : {}
|
||||
|
||||
super opts[:message] || I18n.t('api_v3.errors.code_403')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
module API
|
||||
module V3
|
||||
module Backups
|
||||
class BackupRepresenter < ::API::Decorators::Single
|
||||
include API::Decorators::LinkedResource
|
||||
include API::Caching::CachedRepresenter
|
||||
|
||||
property :job_status_id, getter: ->(*) { job_status.job_id }
|
||||
|
||||
link :job_status do
|
||||
{
|
||||
title: "Backup job status",
|
||||
href: api_v3_paths.job_status(represented.job_status.job_id)
|
||||
}
|
||||
end
|
||||
|
||||
def _type
|
||||
'Backup'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
module API
|
||||
module V3
|
||||
module Backups
|
||||
class BackupsAPI < ::API::OpenProjectAPI
|
||||
resources :backups do
|
||||
before do
|
||||
raise API::Errors::NotFound unless OpenProject::Configuration.backup_enabled?
|
||||
end
|
||||
|
||||
after_validation do
|
||||
authorize Backup.permission, global: true
|
||||
end
|
||||
|
||||
params do
|
||||
requires :backupToken, type: String
|
||||
|
||||
optional(
|
||||
:attachments,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
desc: 'Whether or not to include attachments (default: true)'
|
||||
)
|
||||
end
|
||||
post do
|
||||
service = ::Backups::CreateService.new(
|
||||
user: current_user,
|
||||
backup_token: params[:backupToken],
|
||||
include_attachments: params[:attachments]
|
||||
)
|
||||
call = service.call
|
||||
|
||||
if call.failure?
|
||||
errors = call.errors.errors
|
||||
|
||||
if err = errors.find { |e| e.type == :invalid_token || e.type == :token_cooldown }
|
||||
fail ::API::Errors::Unauthorized, message: err.full_message
|
||||
elsif err = errors.find { |e| e.type == :backup_pending }
|
||||
fail ::API::Errors::Conflict, message: err.full_message
|
||||
elsif err = errors.find { |e| e.type == :limit_reached }
|
||||
fail ::API::Errors::TooManyRequests, message: err.full_message
|
||||
end
|
||||
|
||||
fail ::API::Errors::ErrorBase.create_and_merge_errors(call.errors)
|
||||
end
|
||||
|
||||
status 202
|
||||
|
||||
BackupRepresenter.new call.result, current_user: current_user
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -46,6 +46,7 @@ module API
|
||||
mount ::API::V3::Activities::ActivitiesAPI
|
||||
mount ::API::V3::Attachments::AttachmentsAPI
|
||||
mount ::API::V3::Capabilities::CapabilitiesAPI
|
||||
mount ::API::V3::Backups::BackupsAPI
|
||||
mount ::API::V3::Categories::CategoriesAPI
|
||||
mount ::API::V3::Configuration::ConfigurationAPI
|
||||
mount ::API::V3::CustomActions::CustomActionsAPI
|
||||
|
||||
@@ -169,6 +169,8 @@ module API
|
||||
"#{capabilities}/contexts/global"
|
||||
end
|
||||
|
||||
index :backup
|
||||
|
||||
index :category
|
||||
show :category
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ module OpenProject
|
||||
end
|
||||
|
||||
def permissions
|
||||
@permissions
|
||||
@permissions.select(&:enabled?)
|
||||
end
|
||||
|
||||
def modules
|
||||
|
||||
@@ -43,6 +43,7 @@ module OpenProject
|
||||
@public = options[:public] || false
|
||||
@require = options[:require]
|
||||
@global = options[:global] || false
|
||||
@enabled = options.include?(:enabled) ? options[:enabled] : true
|
||||
@dependencies = Array(options[:dependencies]) || []
|
||||
@project_module = options[:project_module]
|
||||
@contract_actions = options[:contract_actions] || []
|
||||
@@ -71,6 +72,14 @@ module OpenProject
|
||||
def require_loggedin?
|
||||
@require && (@require == :member || @require == :loggedin)
|
||||
end
|
||||
|
||||
def enabled?
|
||||
if @enabled.respond_to?(:call)
|
||||
@enabled.call
|
||||
else
|
||||
@enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,6 +46,12 @@ module OpenProject
|
||||
'autologin_cookie_name' => 'autologin',
|
||||
'autologin_cookie_path' => '/',
|
||||
'autologin_cookie_secure' => false,
|
||||
# Allow users with the required permissions to create backups via the web interface or API.
|
||||
'backup_enabled' => true,
|
||||
'backup_daily_limit' => 3,
|
||||
'backup_initial_waiting_period' => 24.hours,
|
||||
'backup_include_attachments' => true,
|
||||
'backup_attachment_size_max_sum_mb' => 1024,
|
||||
'database_cipher_key' => nil,
|
||||
# only applicable in conjunction with fog (effectively S3) attachments
|
||||
# which will be uploaded directly to the cloud storage rather than via OpenProject's
|
||||
|
||||
@@ -126,4 +126,13 @@ namespace :backup do
|
||||
filename.gsub(/[^0-9A-Za-z.-]/, '_')
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Allows user-initiated backups right away, skipping the cooldown period after a new token was created.'
|
||||
task allow_now: :environment do
|
||||
date = DateTime.now - OpenProject::Configuration.backup_initial_waiting_period
|
||||
|
||||
Token::Backup.where("created_at > ?", date).each do |token|
|
||||
token.update_column :created_at, date
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,6 +68,7 @@ module JobStatus
|
||||
resource = ::JobStatus::Status.find_or_initialize_by(job_id: job_id)
|
||||
|
||||
if resource.new_record?
|
||||
resource.user = User.current # needed so `resource.user` works below
|
||||
resource.user_id = User.current.id
|
||||
resource.reference = status_reference
|
||||
end
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
require 'spec_helper'
|
||||
require 'contracts/shared/model_contract_shared_context'
|
||||
|
||||
describe Backups::CreateContract do
|
||||
let(:backup) { Backup.new }
|
||||
let(:contract) { described_class.new backup, current_user, options: { backup_token: backup_token.plain_value } }
|
||||
let(:backup_token) { FactoryBot.create :backup_token, user: current_user }
|
||||
|
||||
include_context 'ModelContract shared context'
|
||||
|
||||
it_behaves_like 'contract is valid for active admins and invalid for regular users'
|
||||
|
||||
context 'with regular user who has the :create_backup permission' do
|
||||
let(:current_user) { FactoryBot.create :user, global_permissions: [:create_backup] }
|
||||
|
||||
it_behaves_like 'contract is valid'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
FactoryBot.define do
|
||||
factory :backup, class: Backup do
|
||||
end
|
||||
end
|
||||
@@ -44,4 +44,22 @@ FactoryBot.define do
|
||||
factory :recovery_token, class: ::Token::Recovery do
|
||||
user
|
||||
end
|
||||
|
||||
factory :backup_token, class: ::Token::Backup do
|
||||
user
|
||||
|
||||
after(:build) do |token|
|
||||
token.created_at = DateTime.now - OpenProject::Configuration.backup_initial_waiting_period
|
||||
end
|
||||
|
||||
trait :with_waiting_period do
|
||||
transient do
|
||||
since { 0.seconds }
|
||||
end
|
||||
|
||||
after(:build) do |token, factory|
|
||||
token.created_at = DateTime.now - factory.since
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,14 +46,23 @@ FactoryBot.define do
|
||||
admin { false }
|
||||
first_login { false if User.table_exists? and User.columns.map(&:name).include? 'first_login' }
|
||||
|
||||
transient do
|
||||
global_permissions { [] }
|
||||
end
|
||||
|
||||
callback(:after_build) do |user, evaluator|
|
||||
evaluator.preferences.each do |key, val|
|
||||
user.pref[key] = val
|
||||
end
|
||||
end
|
||||
|
||||
callback(:after_create) do |user, evaluator|
|
||||
user.pref.save unless evaluator.preferences&.empty?
|
||||
callback(:after_create) do |user, factory|
|
||||
user.pref.save unless factory.preferences&.empty?
|
||||
|
||||
if factory.global_permissions.present?
|
||||
global_role = FactoryBot.create :global_role, permissions: factory.global_permissions
|
||||
FactoryBot.create :global_member, principal: user, roles: [global_role]
|
||||
end
|
||||
end
|
||||
|
||||
factory :admin do
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'backup', type: :feature, js: true do
|
||||
let(:current_user) { FactoryBot.create :admin, password: user_password, password_confirmation: user_password }
|
||||
let!(:backup_token) { FactoryBot.create :backup_token, user: current_user }
|
||||
let(:user_password) { "adminadmin!" }
|
||||
|
||||
before do
|
||||
@download_list = DownloadList.new
|
||||
|
||||
login_as current_user
|
||||
end
|
||||
|
||||
after do
|
||||
DownloadList.clear
|
||||
end
|
||||
|
||||
subject { @download_list.refresh_from(page).latest_download.to_s }
|
||||
|
||||
it "can be downloaded" do
|
||||
visit '/admin/backups'
|
||||
|
||||
fill_in 'backupToken', with: backup_token.plain_value
|
||||
click_on "Request backup"
|
||||
|
||||
expect(page).to have_content I18n.t('js.job_status.generic_messages.in_queue'), wait: 10
|
||||
|
||||
begin
|
||||
perform_enqueued_jobs
|
||||
rescue StandardError
|
||||
# nothing
|
||||
end
|
||||
|
||||
expect(page).to have_text "The export has completed successfully"
|
||||
expect(subject).to end_with ".zip"
|
||||
end
|
||||
|
||||
context "with an error" do
|
||||
it "shows the error" do
|
||||
visit "/admin/backups"
|
||||
|
||||
fill_in "backupToken", with: "foobar"
|
||||
click_on "Request backup"
|
||||
|
||||
expect(page).to have_content I18n.t("backup.error.invalid_token")
|
||||
end
|
||||
end
|
||||
|
||||
it "allows the backup token to be reset" do
|
||||
visit "/admin/backups"
|
||||
click_on I18n.t("backup.label_reset_token")
|
||||
|
||||
expect(page).to have_content /#{I18n.t('backup.reset_token.heading_reset')}/i
|
||||
|
||||
fill_in "login_verification", with: "reset"
|
||||
click_on "Reset"
|
||||
fill_in "request_for_confirmation_password", with: user_password
|
||||
click_on "Confirm"
|
||||
|
||||
new_token = Token::Backup.find_by(user: current_user)
|
||||
|
||||
expect(new_token.plain_value).not_to eq backup_token.plain_value
|
||||
expect(page).to have_content new_token.plain_value
|
||||
end
|
||||
|
||||
it "allows the backup token to be deleted" do
|
||||
visit "/admin/backups"
|
||||
|
||||
expect(page).to have_content /#{I18n.t('js.backup.title')}/i
|
||||
|
||||
click_on I18n.t("backup.label_delete_token")
|
||||
|
||||
expect(page).to have_content I18n.t("backup.text_token_deleted")
|
||||
|
||||
token = Token::Backup.find_by(user: current_user)
|
||||
|
||||
expect(token).to be_nil
|
||||
expect(page).not_to have_content /#{I18n.t('js.backup.title')}/i
|
||||
end
|
||||
end
|
||||
@@ -32,7 +32,7 @@ describe OpenProject::AccessControl do
|
||||
def stash_access_control_permissions
|
||||
@stashed_permissions = OpenProject::AccessControl.permissions.dup
|
||||
OpenProject::AccessControl.clear_caches
|
||||
OpenProject::AccessControl.permissions.clear
|
||||
OpenProject::AccessControl.instance_variable_get(:@permissions).clear
|
||||
end
|
||||
|
||||
def restore_access_control_permissions
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
require 'spec_helper'
|
||||
require 'rack/test'
|
||||
|
||||
describe API::V3::Backups::BackupsAPI, type: :request, with_config: { backup_enabled: true } do
|
||||
include API::V3::Utilities::PathHelper
|
||||
|
||||
let(:user) { FactoryBot.create :user, global_permissions: [:create_backup] }
|
||||
let(:params) { { backupToken: backup_token.plain_value } }
|
||||
|
||||
let(:backup_token) { FactoryBot.create :backup_token, user: user }
|
||||
|
||||
before do
|
||||
login_as user
|
||||
end
|
||||
|
||||
def create_backup
|
||||
post api_v3_paths.backups, params.to_json, "CONTENT_TYPE" => "application/json"
|
||||
end
|
||||
|
||||
describe "POST /api/v3/backups" do
|
||||
shared_context "request" do
|
||||
before do
|
||||
create_backup
|
||||
end
|
||||
end
|
||||
|
||||
context "with no pending backups" do
|
||||
context "with no params" do
|
||||
let(:params) { {} }
|
||||
|
||||
include_context "request"
|
||||
|
||||
it "results in a bad request error" do
|
||||
expect(last_response.status).to eq 400
|
||||
end
|
||||
end
|
||||
|
||||
context "with no options" do
|
||||
before do
|
||||
expect(Backups::CreateService)
|
||||
.to receive(:new)
|
||||
.with(user: user, backup_token: backup_token.plain_value, include_attachments: true)
|
||||
.and_call_original
|
||||
|
||||
create_backup
|
||||
end
|
||||
|
||||
it "enqueues the backup including attachments" do
|
||||
expect(last_response.status).to eq 202
|
||||
end
|
||||
end
|
||||
|
||||
context "with include_attachments: false" do
|
||||
let(:params) { { backupToken: backup_token.plain_value, attachments: false } }
|
||||
|
||||
before do
|
||||
expect(Backups::CreateService)
|
||||
.to receive(:new)
|
||||
.with(user: user, backup_token: backup_token.plain_value, include_attachments: false)
|
||||
.and_call_original
|
||||
|
||||
create_backup
|
||||
end
|
||||
|
||||
it "enqueues a backup not including attachments" do
|
||||
expect(last_response.status).to eq 202
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with pending backups" do
|
||||
let!(:backup) { FactoryBot.create :backup }
|
||||
let!(:status) { FactoryBot.create :delayed_job_status, user: user, reference: backup }
|
||||
|
||||
include_context "request"
|
||||
|
||||
it "results in a conflict" do
|
||||
expect(last_response.status).to eq 409
|
||||
end
|
||||
end
|
||||
|
||||
context "with missing permissions" do
|
||||
let(:user) { FactoryBot.create :user }
|
||||
|
||||
include_context "request"
|
||||
|
||||
it "is forbidden" do
|
||||
expect(last_response.status).to eq 403
|
||||
end
|
||||
end
|
||||
|
||||
context "with another user's token" do
|
||||
let(:other_user) { FactoryBot.create :user }
|
||||
let(:backup_token) { FactoryBot.create :backup_token, user: other_user }
|
||||
|
||||
include_context "request"
|
||||
|
||||
it "is forbidden" do
|
||||
expect(last_response.status).to eq 403
|
||||
end
|
||||
end
|
||||
|
||||
context "with daily backup limit reached", with_config: { backup_daily_limit: -1 } do
|
||||
include_context "request"
|
||||
|
||||
it "is rate limited" do
|
||||
expect(last_response.status).to eq 429
|
||||
end
|
||||
end
|
||||
|
||||
context "with backup token on cooldown", with_config: { backup_initial_waiting_period: 24.hours } do
|
||||
let(:backup_token) { FactoryBot.create :backup_token, :with_waiting_period, user: user, since: 5.hours }
|
||||
|
||||
include_context "request"
|
||||
|
||||
it "is forbidden" do
|
||||
expect(last_response.status).to eq 403
|
||||
end
|
||||
|
||||
it "shows the remaining hours until the token is valid" do
|
||||
expect(last_response.body).to include "19 hours"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
#-- encoding: UTF-8
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
require 'spec_helper'
|
||||
require 'services/base_services/behaves_like_create_service'
|
||||
|
||||
describe Backups::CreateService, type: :model do
|
||||
let(:user) { FactoryBot.create :admin }
|
||||
let(:service) { described_class.new user: user, backup_token: backup_token.plain_value }
|
||||
let(:backup_token) { FactoryBot.create :backup_token, user: user }
|
||||
|
||||
it_behaves_like 'BaseServices create service' do
|
||||
let(:instance) { service }
|
||||
let(:contract_options) { { backup_token: backup_token.plain_value } }
|
||||
end
|
||||
|
||||
context "with right permissions" do
|
||||
context "with no further options" do
|
||||
it "enqueues a BackupJob which includes attachments" do
|
||||
expect { service.call }.to have_enqueued_job(BackupJob).with do |args|
|
||||
expect(args["include_attachments"]).to eq true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with include_attachments: false" do
|
||||
let(:service) do
|
||||
described_class.new user: user, backup_token: backup_token.plain_value, include_attachments: false
|
||||
end
|
||||
|
||||
it "enqueues a BackupJob which does not include attachments" do
|
||||
expect(BackupJob)
|
||||
.to receive(:perform_later)
|
||||
.with(hash_including(include_attachments: false, user: user))
|
||||
|
||||
expect(service.call).to be_success
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with missing permission" do
|
||||
let(:user) { FactoryBot.create :user }
|
||||
|
||||
it "does not enqueue a BackupJob" do
|
||||
expect { expect(service.call).to be_failure }.not_to have_enqueued_job(BackupJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -35,6 +35,7 @@ shared_examples 'BaseServices create service' do
|
||||
let(:namespace) { service_class.to_s.deconstantize }
|
||||
let(:model_class) { namespace.singularize.constantize }
|
||||
let(:contract_class) { "#{namespace}::CreateContract".constantize }
|
||||
let(:contract_options) { {} }
|
||||
let(:factory) { namespace.singularize.underscore }
|
||||
|
||||
let(:set_attributes_class) { "#{namespace}::SetAttributesService".constantize }
|
||||
@@ -64,7 +65,7 @@ shared_examples 'BaseServices create service' do
|
||||
.with(user: user,
|
||||
model: model_instance,
|
||||
contract_class: contract_class,
|
||||
contract_options: {})
|
||||
contract_options: contract_options)
|
||||
.and_return(service)
|
||||
|
||||
allow(service)
|
||||
@@ -95,7 +96,7 @@ shared_examples 'BaseServices create service' do
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
context 'if contract validates and the user saves' do
|
||||
context 'if contract validates and the model saves' do
|
||||
it 'is successful' do
|
||||
expect(subject).to be_success
|
||||
end
|
||||
@@ -104,7 +105,7 @@ shared_examples 'BaseServices create service' do
|
||||
expect(subject.errors).to eq(set_attributes_errors)
|
||||
end
|
||||
|
||||
it 'returns the user as a result' do
|
||||
it 'returns the model as a result' do
|
||||
result = subject.result
|
||||
expect(result).to be_a model_class
|
||||
end
|
||||
@@ -126,7 +127,7 @@ shared_examples 'BaseServices create service' do
|
||||
expect(subject).to_not be_success
|
||||
end
|
||||
|
||||
it "returns the user's errors" do
|
||||
it "returns the model's errors" do
|
||||
allow(model_instance)
|
||||
.to(receive(:errors))
|
||||
.and_return errors
|
||||
|
||||
@@ -21,7 +21,7 @@ RSpec.configure do |_config|
|
||||
Capybara.server_host = ip_address
|
||||
Capybara.app_host = "http://#{hostname}"
|
||||
else
|
||||
Capybara.server_host = "0.0.0.0"
|
||||
Capybara.server_host = ENV.fetch('CAPYBARA_APP_HOSTNAME', '0.0.0.0')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
||||
#++
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe BackupJob, type: :model do
|
||||
shared_examples "it creates a backup" do |opts = {}|
|
||||
let(:job) { BackupJob.new }
|
||||
|
||||
let(:previous_backup) { FactoryBot.create :backup }
|
||||
let(:backup) { FactoryBot.create :backup }
|
||||
let(:status) { :in_queue }
|
||||
let(:job_id) { 42 }
|
||||
|
||||
let(:job_status) do
|
||||
FactoryBot.create(
|
||||
:delayed_job_status,
|
||||
user: user,
|
||||
reference: backup,
|
||||
status: JobStatus::Status.statuses[status],
|
||||
job_id: job_id
|
||||
)
|
||||
end
|
||||
|
||||
let(:db_dump_process_status) do
|
||||
success = db_dump_success
|
||||
|
||||
Object.new.tap do |o|
|
||||
o.define_singleton_method(:success?) { success }
|
||||
end
|
||||
end
|
||||
|
||||
let(:db_dump_success) { false }
|
||||
|
||||
let(:attachments) { [] }
|
||||
let(:arguments) { [{ backup: backup, user: user, **opts }] }
|
||||
|
||||
let(:user) { FactoryBot.create :user }
|
||||
|
||||
before do
|
||||
previous_backup; backup; status # create
|
||||
|
||||
allow(job).to receive(:job_status).and_return job_status
|
||||
allow(job).to receive(:attachments).and_return attachments
|
||||
allow(job).to receive(:arguments).and_return arguments
|
||||
allow(job).to receive(:job_id).and_return job_id
|
||||
|
||||
expect(Open3).to receive(:capture3).and_return [nil, "Dump failed", db_dump_process_status]
|
||||
|
||||
allow_any_instance_of(BackupJob)
|
||||
.to receive(:tmp_file_name).with("openproject", ".sql").and_return("/tmp/openproject.sql")
|
||||
|
||||
allow_any_instance_of(BackupJob)
|
||||
.to receive(:tmp_file_name).with("openproject-backup", ".zip").and_return("/tmp/openproject.zip")
|
||||
|
||||
allow(File).to receive(:read).and_call_original
|
||||
allow(File).to receive(:read).with("/tmp/openproject.sql").and_return "SOME SQL"
|
||||
end
|
||||
|
||||
def perform
|
||||
job.perform **arguments.first
|
||||
end
|
||||
|
||||
context "with a failed database dump" do
|
||||
let(:db_dump_success) { false }
|
||||
|
||||
before { perform }
|
||||
|
||||
it "retains previous backups" do
|
||||
expect(Backup.find_by(id: previous_backup.id)).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "with a successful database dump" do
|
||||
let(:db_dump_success) { true }
|
||||
|
||||
let!(:attachment) { FactoryBot.create :attachment }
|
||||
let(:stored_backup) { Attachment.where(container_type: "Export").last }
|
||||
let(:backup_files) { Zip::File.open(stored_backup.file.path) { |zip| zip.entries.map(&:name) } }
|
||||
let(:backed_up_attachment) { "attachment/file/#{attachment.id}/#{attachment.filename}" }
|
||||
|
||||
before { perform }
|
||||
|
||||
it "destroys any previous backups" do
|
||||
expect(Backup.find_by(id: previous_backup.id)).to be_nil
|
||||
end
|
||||
|
||||
it "stores a new backup as an attachment" do
|
||||
expect(stored_backup.filename).to eq "openproject.zip"
|
||||
end
|
||||
|
||||
it "includes the database dump in the backup" do
|
||||
expect(backup_files).to include "openproject.sql"
|
||||
end
|
||||
|
||||
if opts[:include_attachments] != false
|
||||
it "includes attachments in the backup" do
|
||||
expect(backup_files).to include backed_up_attachment
|
||||
end
|
||||
else
|
||||
it "does not include attachments in the backup" do
|
||||
expect(backup_files).not_to include backed_up_attachment
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "per default" do
|
||||
it_behaves_like "it creates a backup"
|
||||
end
|
||||
|
||||
context "with include_attachments: false" do
|
||||
it_behaves_like "it creates a backup", include_attachments: false
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user