Commit 867151ac authored by Sean Arnold's avatar Sean Arnold Committed by Heinrich Lee Yu

Add StatusPageSetting model

- Migration
- EE model & factory
parent 7a0cfc82
---
title: Create table & setup operations endpoint for Status Page Settings
merge_request: 25863
author:
type: added
# frozen_string_literal: true
class AddStatusPageSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :status_page_settings, id: false do |t|
t.references :project, index: true, primary_key: true, foreign_key: { on_delete: :cascade }, unique: true, null: false
t.timestamps_with_timezone null: false
t.boolean :enabled, default: false, null: false
t.string :aws_s3_bucket_name, limit: 63, null: false
t.string :aws_region, limit: 255, null: false
t.string :aws_access_key, limit: 255, null: false
t.string :encrypted_aws_secret_key, limit: 255, null: false
t.string :encrypted_aws_secret_key_iv, limit: 255, null: false
end
end
end
......@@ -4002,6 +4002,18 @@ ActiveRecord::Schema.define(version: 2020_02_27_165129) do
t.boolean "recaptcha_verified", default: false, null: false
end
create_table "status_page_settings", primary_key: "project_id", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.boolean "enabled", default: false, null: false
t.string "aws_s3_bucket_name", limit: 63, null: false
t.string "aws_region", limit: 255, null: false
t.string "aws_access_key", limit: 255, null: false
t.string "encrypted_aws_secret_key", limit: 255, null: false
t.string "encrypted_aws_secret_key_iv", limit: 255, null: false
t.index ["project_id"], name: "index_status_page_settings_on_project_id"
end
create_table "subscriptions", id: :serial, force: :cascade do |t|
t.integer "user_id"
t.integer "subscribable_id"
......@@ -5018,6 +5030,7 @@ ActiveRecord::Schema.define(version: 2020_02_27_165129) do
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
add_foreign_key "software_license_policies", "projects", on_delete: :cascade
add_foreign_key "software_license_policies", "software_licenses", on_delete: :cascade
add_foreign_key "status_page_settings", "projects", on_delete: :cascade
add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "suggestions", "notes", on_delete: :cascade
add_foreign_key "system_note_metadata", "description_versions", name: "fk_fbd87415c9", on_delete: :nullify
......
......@@ -58,9 +58,15 @@ module EE
permitted_params[:tracing_setting_attributes] = [:external_url]
end
permitted_params.merge!(status_page_setting_params)
permitted_params
end
def status_page_setting_params
{ status_page_setting_attributes: [:aws_s3_bucket_name, :aws_region, :aws_access_key, :aws_secret_key, :enabled] }
end
override :track_events
def track_events(result)
super
......
......@@ -19,4 +19,14 @@ module OperationsHelper
'environments-dashboard-help-path' => help_page_path('ci/environments/environments_dashboard.md')
}
end
def status_page_settings_data(status_page_setting)
{
'setting-enabled' => status_page_setting&.enabled?,
'setting-aws-access-key' => status_page_setting&.aws_access_key,
'setting-masked-aws-secret-key' => status_page_setting&.masked_aws_secret_key,
'setting-aws-region' => status_page_setting&.aws_region,
'setting-aws-s3-bucket-name' => status_page_setting&.aws_s3_bucket_name
}
end
end
......@@ -48,6 +48,7 @@ module EE
has_one :tracing_setting, class_name: 'ProjectTracingSetting'
has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting'
has_one :feature_usage, class_name: 'ProjectFeatureUsage'
has_one :status_page_setting, inverse_of: :project
has_many :reviews, inverse_of: :project
has_many :approvers, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......@@ -179,6 +180,7 @@ module EE
accepts_nested_attributes_for :tracing_setting, update_only: true, allow_destroy: true
accepts_nested_attributes_for :alerting_setting, update_only: true
accepts_nested_attributes_for :status_page_setting, update_only: true, allow_destroy: true
alias_attribute :fallback_approvals_required, :approvals_before_merge
end
......
# frozen_string_literal: true
class StatusPageSetting < ApplicationRecord
# AWS validations. See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25863#note_295772553
AWS_BUCKET_NAME_REGEXP = /\A[a-z0-9][a-z0-9\-.]*\z/.freeze
AWS_ACCESS_KEY_REGEXP = /\A[A-Z0-9]{20}\z/.freeze
AWS_SECRET_KEY_REGEXP = /\A[A-Za-z0-9\/+=]{40}\z/.freeze
belongs_to :project
attr_encrypted :aws_secret_key,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32
before_validation :check_secret_changes
validates :aws_s3_bucket_name,
length: { minimum: 3, maximum: 63 },
presence: true,
format: { with: AWS_BUCKET_NAME_REGEXP }
validates :aws_access_key,
presence: true,
format: { with: AWS_ACCESS_KEY_REGEXP }
validates :aws_secret_key,
presence: true,
format: { with: AWS_SECRET_KEY_REGEXP }
validates :project, :aws_region, :encrypted_aws_secret_key,
presence: true
validates :enabled, inclusion: { in: [true, false] }
scope :enabled, -> { where(enabled: true) }
def masked_aws_secret_key
'*' * 40
end
private
def check_secret_changes
return unless masked_aws_secret_key == aws_secret_key
restore_attributes [:aws_secret_key, :encrypted_aws_secret_key, :encrypted_aws_secret_key_iv]
end
end
......@@ -13,6 +13,7 @@ module EE
.merge(tracing_setting_params)
.merge(alerting_setting_params)
.merge(incident_management_setting_params)
.merge(status_page_setting_params)
end
private
......@@ -46,6 +47,17 @@ module EE
def incident_management_setting_params
params.slice(:incident_management_setting_attributes)
end
def status_page_setting_params
return {} unless attrs = params[:status_page_setting_attributes]
destroy = attrs[:aws_s3_bucket_name].blank? &&
attrs[:aws_region].blank? &&
attrs[:aws_access_key].blank? &&
attrs[:aws_secret_key].blank?
{ status_page_setting_attributes: attrs.merge(_destroy: destroy) }
end
end
end
end
......
......@@ -271,6 +271,55 @@ describe Projects::Settings::OperationsController do
update_project(project, tracing_params: { external_url: "http://example.com" } )
end
end
context 'without existing status page setting' do
let(:project) { create(:project) }
before do
project.add_maintainer(user)
end
it 'creates a status page setting' do
valid_attributes = attributes_for(:status_page_setting).except(:enabled)
update_project(project, status_page_params: valid_attributes )
expect(project.status_page_setting).not_to eq(nil)
expect(project.status_page_setting).to be_a(StatusPageSetting)
end
end
context 'with existing status page setting' do
let(:project) { create(:project) }
let(:status_page_attributes) { attributes_for(:status_page_setting) }
before do
project.add_maintainer(user)
project.create_status_page_setting!(status_page_attributes)
end
it 'updates the fields' do
update_project(project, status_page_params: status_page_attributes.merge(aws_s3_bucket_name: 'test'))
expect(project.status_page_setting.aws_s3_bucket_name).to eq('test')
end
it 'respects the model validations' do
old_name = project.status_page_setting.aws_s3_bucket_name
update_project(project, status_page_params: status_page_attributes.merge(aws_s3_bucket_name: ''))
expect(project.status_page_setting.aws_s3_bucket_name).to eq(old_name)
end
it 'deletes the setting if keys removed' do
update_project(
project,
status_page_params: status_page_attributes.merge(aws_access_key: '',
aws_secret_key: '',
aws_s3_bucket_name: '',
aws_region: '')
)
expect(project.status_page_setting).to be_nil
end
end
end
context 'without a license' do
......@@ -285,11 +334,12 @@ describe Projects::Settings::OperationsController do
private
def update_project(project, tracing_params: nil, incident_management_params: nil)
def update_project(project, tracing_params: nil, incident_management_params: nil, status_page_params: nil)
patch :update, params: project_params(
project,
tracing_params: tracing_params,
incident_management_params: incident_management_params
incident_management_params: incident_management_params,
status_page_params: status_page_params
)
project.reload
......@@ -399,13 +449,14 @@ describe Projects::Settings::OperationsController do
private
def project_params(project, tracing_params: nil, incident_management_params: nil)
def project_params(project, tracing_params: nil, incident_management_params: nil, status_page_params: nil)
{
namespace_id: project.namespace,
project_id: project,
project: {
tracing_setting_attributes: tracing_params,
incident_management_setting_attributes: incident_management_params
incident_management_setting_attributes: incident_management_params,
status_page_setting_attributes: status_page_params
}
}
end
......
# frozen_string_literal: true
FactoryBot.define do
factory :status_page_setting, class: 'StatusPageSetting' do
project
aws_s3_bucket_name { 'bucket-name' }
aws_region { 'ap-southeast-2' }
aws_access_key { FFaker::String.from_regexp(StatusPageSetting::AWS_ACCESS_KEY_REGEXP) }
aws_secret_key { FFaker::String.from_regexp(StatusPageSetting::AWS_SECRET_KEY_REGEXP) }
enabled { false }
trait :enabled do
enabled { true }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe OperationsHelper do
describe '#status_page_settings_data' do
subject { helper.status_page_settings_data(status_page_setting) }
context 'setting does not exist' do
let(:status_page_setting) { nil }
it 'returns the correct values' do
expect(subject.keys)
.to contain_exactly(
'setting-enabled',
'setting-aws-access-key',
'setting-masked-aws-secret-key',
'setting-aws-region',
'setting-aws-s3-bucket-name'
)
end
it 'returns nil for the values' do
expect(subject.values.uniq).to contain_exactly(nil)
end
end
context 'setting exists' do
let(:status_page_setting) { create(:status_page_setting) }
it 'returns the correct values' do
aggregate_failures do
expect(subject['setting-enabled']).to eq(status_page_setting.enabled)
expect(subject['setting-aws-access-key']).to eq(status_page_setting.aws_access_key)
expect(subject['setting-masked-aws-secret-key']).to eq(status_page_setting.masked_aws_secret_key)
expect(subject['setting-aws-region']).to eq(status_page_setting.aws_region)
expect(subject['setting-aws-s3-bucket-name']).to eq(status_page_setting.aws_s3_bucket_name)
end
end
end
end
end
......@@ -23,6 +23,7 @@ describe Project do
it { is_expected.to have_one(:import_state).class_name('ProjectImportState') }
it { is_expected.to have_one(:repository_state).class_name('ProjectRepositoryState').inverse_of(:project) }
it { is_expected.to have_one(:alerting_setting).class_name('Alerting::ProjectAlertingSetting') }
it { is_expected.to have_one(:status_page_setting).class_name('StatusPageSetting') }
it { is_expected.to have_many(:reviews).inverse_of(:project) }
it { is_expected.to have_many(:path_locks) }
......
# frozen_string_literal: true
require 'spec_helper'
describe StatusPageSetting do
describe 'associations' do
it { is_expected.to belong_to(:project) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:aws_s3_bucket_name) }
it { is_expected.to validate_length_of(:aws_s3_bucket_name).is_at_least(3).is_at_most(63) }
it { is_expected.to validate_presence_of(:aws_region) }
it { is_expected.to validate_presence_of(:aws_access_key) }
it { is_expected.to validate_presence_of(:encrypted_aws_secret_key) }
describe 'aws_s3_bucket_name' do
it { is_expected.to allow_value('bucket-name').for(:aws_s3_bucket_name) }
it { is_expected.to allow_value('3ucket-name').for(:aws_s3_bucket_name) }
it { is_expected.to allow_value('bucket.name').for(:aws_s3_bucket_name) }
it { is_expected.to allow_value('b' * 63).for(:aws_s3_bucket_name) }
it { is_expected.to allow_value('1' * 63).for(:aws_s3_bucket_name) }
it { is_expected.not_to allow_value('Bucket-name').for(:aws_s3_bucket_name) }
it { is_expected.not_to allow_value('bucKet-Name').for(:aws_s3_bucket_name) }
it { is_expected.not_to allow_value('bb').for(:aws_s3_bucket_name) }
it { is_expected.not_to allow_value('b' * 64).for(:aws_s3_bucket_name) }
end
describe 'aws_access_key' do
it { is_expected.to allow_value('A' * 20).for(:aws_access_key) }
it { is_expected.to allow_value('1' * 20).for(:aws_access_key) }
it { is_expected.not_to allow_value('A' * 19).for(:aws_access_key) }
it { is_expected.not_to allow_value('1' * 19).for(:aws_access_key) }
it { is_expected.not_to allow_value('A' * 21).for(:aws_access_key) }
it { is_expected.not_to allow_value('1' * 21).for(:aws_access_key) }
end
describe 'aws_secret_key' do
let(:status_page_setting) { build(:status_page_setting) }
it 'tests the validation' do
expect(status_page_setting.valid?).to eq(true)
status_page_setting.aws_secret_key = 'a' * 40
expect(status_page_setting.valid?).to eq(true)
status_page_setting.aws_secret_key = 'a' * 39
expect(status_page_setting.valid?).to eq(false)
status_page_setting.aws_secret_key = 'a' * 41
expect(status_page_setting.valid?).to eq(false)
end
context 'existing setting' do
let(:status_page_setting) { create(:status_page_setting) }
it 'passes validation and removes changes if setting to masked key' do
status_page_setting.aws_secret_key = status_page_setting.masked_aws_secret_key
expect(status_page_setting.valid?).to eq(true)
end
end
end
end
describe 'attribute encryption' do
let(:new_secret) { FFaker::String.from_regexp(StatusPageSetting::AWS_SECRET_KEY_REGEXP) }
subject(:status_page_setting) { create(:status_page_setting, aws_secret_key: new_secret) }
context 'token' do
it 'encrypts original value into encrypted_token attribute' do
expect(status_page_setting.encrypted_aws_secret_key).not_to be_nil
end
end
end
end
......@@ -188,5 +188,86 @@ describe Projects::Operations::UpdateService do
it_behaves_like 'no operation'
end
end
context 'status page setting' do
before do
project.add_maintainer(user)
end
shared_examples 'no operation' do
it 'does nothing' do
expect(result[:status]).to eq(:success)
expect(project.reload.status_page_setting).to be_nil
end
end
context 'with valid params' do
let(:params) do
{
status_page_setting_attributes: attributes_for(:status_page_setting, aws_s3_bucket_name: 'test')
}
end
context 'with an existing setting' do
before do
create(:status_page_setting, project: project)
end
it 'updates the setting' do
expect(project.status_page_setting).not_to be_nil
expect(result[:status]).to eq(:success)
expect(project.reload.status_page_setting.aws_s3_bucket_name)
.to eq('test')
end
context 'with aws key and secret blank' do
let(:params) do
{
status_page_setting_attributes: {
aws_access_key: '',
aws_secret_key: '',
aws_s3_bucket_name: '',
aws_region: ''
}
}
end
it 'destroys the status_page_setting entry in DB' do
expect(result[:status]).to eq(:success)
expect(project.reload.status_page_setting).to be_nil
end
end
context 'with not all keys blank' do
let(:params) do
{
status_page_setting_attributes: {
aws_s3_bucket_name: 'test',
aws_region: 'ap-southeast-2',
aws_access_key: '',
aws_secret_key: project.reload.status_page_setting.masked_aws_secret_key
}
}
end
it 'returns a validation error' do
expect(result[:status]).to eq(:error)
end
end
end
context 'without an existing setting' do
it 'creates a setting' do
expect(project.status_page_setting).to be_nil
expect(result[:status]).to eq(:success)
expect(project.reload.status_page_setting.aws_s3_bucket_name)
.to eq('test')
end
end
end
end
end
end
......@@ -466,6 +466,7 @@ project:
- container_expiration_policy
- resource_groups
- autoclose_referenced_issues
- status_page_setting
award_emoji:
- awardable
- user
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment