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>pull/9216/head
parent
0bec3cb8c2
commit
8c8b8bbfa7
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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. |
||||
|
||||
![System-admin-guide-backup-11.3](openproject-backup.png) |
||||
|
||||
## 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. |
After Width: | Height: | Size: 120 KiB |
@ -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 |
||||
} |
||||
); |
||||
} |
||||
} |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
Loading…
Reference in new issue