Commit 4f3648a4 authored by Francisco Javier López's avatar Francisco Javier López Committed by Heinrich Lee Yu

Added migrations for secret snippets

For the new secret snippets feature we need to add a couple
of migrations and also a new index to include the new fields.
parent 698aae94
...@@ -4,4 +4,5 @@ class ProjectSnippet < Snippet ...@@ -4,4 +4,5 @@ class ProjectSnippet < Snippet
belongs_to :project belongs_to :project
validates :project, presence: true validates :project, presence: true
validates :secret, inclusion: { in: [false] }
end end
...@@ -51,8 +51,8 @@ class Snippet < ApplicationRecord ...@@ -51,8 +51,8 @@ class Snippet < ApplicationRecord
# Scopes # Scopes
scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) } scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) }
scope :are_private, -> { where(visibility_level: Snippet::PRIVATE) } scope :are_private, -> { where(visibility_level: Snippet::PRIVATE) }
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :are_public, -> { public_only }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :are_secret, -> { public_only.where(secret: true) }
scope :fresh, -> { order("created_at DESC") } scope :fresh, -> { order("created_at DESC") }
scope :inc_author, -> { includes(:author) } scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> { includes(author: :status) } scope :inc_relations_for_view, -> { includes(author: :status) }
...@@ -63,6 +63,11 @@ class Snippet < ApplicationRecord ...@@ -63,6 +63,11 @@ class Snippet < ApplicationRecord
attr_spammable :title, spam_title: true attr_spammable :title, spam_title: true
attr_spammable :content, spam_description: true attr_spammable :content, spam_description: true
attr_encrypted :secret_token,
key: Settings.attr_encrypted_db_key_base_truncated,
mode: :per_attribute_iv,
algorithm: 'aes-256-cbc'
def self.with_optional_visibility(value = nil) def self.with_optional_visibility(value = nil)
if value if value
where(visibility_level: value) where(visibility_level: value)
...@@ -112,11 +117,8 @@ class Snippet < ApplicationRecord ...@@ -112,11 +117,8 @@ class Snippet < ApplicationRecord
end end
def self.visible_to_or_authored_by(user) def self.visible_to_or_authored_by(user)
where( query = where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(user))
'snippets.visibility_level IN (?) OR snippets.author_id = ?', query.or(where(author_id: user.id))
Gitlab::VisibilityLevel.levels_for_user(user),
user.id
)
end end
def self.reference_prefix def self.reference_prefix
...@@ -222,6 +224,19 @@ class Snippet < ApplicationRecord ...@@ -222,6 +224,19 @@ class Snippet < ApplicationRecord
model_name.singular model_name.singular
end end
def valid_secret_token?(token)
return false unless token && secret_token
ActiveSupport::SecurityUtils.secure_compare(token.to_s, secret_token.to_s)
end
def as_json(options = {})
options[:except] = Array.wrap(options[:except])
options[:except] << :secret_token
super
end
class << self class << self
# Searches for snippets with a matching title or file name. # Searches for snippets with a matching title or file name.
# #
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
- if include_private - if include_private
= subject.snippets.count = subject.snippets.count
- else - else
= subject.snippets.public_and_internal.count = subject.snippets.public_and_internal_only.count
- if include_private - if include_private
%li{ class: active_when(params[:scope] == "are_private") } %li{ class: active_when(params[:scope] == "are_private") }
......
---
title: Add migrations for secret snippets
merge_request: 19939
author:
type: added
# frozen_string_literal: true
class AddSecretTokenToSnippet < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
add_column :snippets, :encrypted_secret_token, :string, limit: 255
add_column :snippets, :encrypted_secret_token_iv, :string, limit: 255
end
end
# frozen_string_literal: true
class AddSecretToSnippet < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless column_exists?(:snippets, :secret)
add_column_with_default :snippets, :secret, :boolean, default: false
end
add_concurrent_index :snippets, [:visibility_level, :secret]
remove_concurrent_index :snippets, :visibility_level
end
def down
add_concurrent_index :snippets, :visibility_level
remove_concurrent_index :snippets, [:visibility_level, :secret]
if column_exists?(:snippets, :secret)
remove_column :snippets, :secret
end
end
end
...@@ -3555,13 +3555,16 @@ ActiveRecord::Schema.define(version: 2019_11_15_091425) do ...@@ -3555,13 +3555,16 @@ ActiveRecord::Schema.define(version: 2019_11_15_091425) do
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
t.text "description" t.text "description"
t.text "description_html" t.text "description_html"
t.string "encrypted_secret_token", limit: 255
t.string "encrypted_secret_token_iv", limit: 255
t.boolean "secret", default: false, null: false
t.index ["author_id"], name: "index_snippets_on_author_id" t.index ["author_id"], name: "index_snippets_on_author_id"
t.index ["content"], name: "index_snippets_on_content_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["content"], name: "index_snippets_on_content_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["file_name"], name: "index_snippets_on_file_name_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["file_name"], name: "index_snippets_on_file_name_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["project_id", "visibility_level"], name: "index_snippets_on_project_id_and_visibility_level" t.index ["project_id", "visibility_level"], name: "index_snippets_on_project_id_and_visibility_level"
t.index ["title"], name: "index_snippets_on_title_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["title"], name: "index_snippets_on_title_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["updated_at"], name: "index_snippets_on_updated_at" t.index ["updated_at"], name: "index_snippets_on_updated_at"
t.index ["visibility_level"], name: "index_snippets_on_visibility_level" t.index ["visibility_level", "secret"], name: "index_snippets_on_visibility_level_and_secret"
end end
create_table "software_license_policies", id: :serial, force: :cascade do |t| create_table "software_license_policies", id: :serial, force: :cascade do |t|
......
...@@ -163,6 +163,9 @@ excluded_attributes: ...@@ -163,6 +163,9 @@ excluded_attributes:
- :identifier - :identifier
snippets: snippets:
- :expired_at - :expired_at
- :secret
- :encrypted_secret_token
- :encrypted_secret_token_iv
merge_request_diff: merge_request_diff:
- :external_diff - :external_diff
- :stored_externally - :stored_externally
......
...@@ -7,6 +7,7 @@ FactoryBot.define do ...@@ -7,6 +7,7 @@ FactoryBot.define do
content { generate(:title) } content { generate(:title) }
description { generate(:title) } description { generate(:title) }
file_name { generate(:filename) } file_name { generate(:filename) }
secret { false }
trait :public do trait :public do
visibility_level { Snippet::PUBLIC } visibility_level { Snippet::PUBLIC }
...@@ -27,5 +28,9 @@ FactoryBot.define do ...@@ -27,5 +28,9 @@ FactoryBot.define do
end end
factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do
trait :secret do
visibility_level { Snippet::PUBLIC }
secret { true }
end
end end
end end
...@@ -9,6 +9,7 @@ describe ProjectSnippet do ...@@ -9,6 +9,7 @@ describe ProjectSnippet do
describe "Validation" do describe "Validation" do
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_inclusion_of(:secret).in_array([false]) }
end end
describe '#embeddable?' do describe '#embeddable?' do
......
...@@ -451,4 +451,20 @@ describe Snippet do ...@@ -451,4 +451,20 @@ describe Snippet do
expect(blob.data).to eq(snippet.content) expect(blob.data).to eq(snippet.content)
end end
end end
describe '#to_json' do
let(:snippet) { build(:snippet) }
it 'excludes secret_token from generated json' do
expect(JSON.parse(to_json).keys).not_to include("secret_token")
end
it 'does not override existing exclude option value' do
expect(JSON.parse(to_json(except: [:id])).keys).not_to include("secret_token", "id")
end
def to_json(params = {})
snippet.to_json(params)
end
end
end end
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