diff options
author | Ash McKenzie <amckenzie@gitlab.com> | 2018-06-10 22:56:17 +1000 |
---|---|---|
committer | Ash McKenzie <amckenzie@gitlab.com> | 2018-06-13 12:11:38 +1000 |
commit | 2f297034036055aaf37f86ece22723aedf8741bf (patch) | |
tree | 731fab3c82dd23d63ba9b89ad101d11e409d2fe0 | |
parent | f646a8b9bc95fd6cecaa754f7dd0e8370c201502 (diff) | |
download | gitlab-ce-2f297034036055aaf37f86ece22723aedf8741bf.tar.gz |
-rw-r--r-- | app/controllers/concerns/snippets_url.rb | 39 | ||||
-rw-r--r-- | app/controllers/snippets_controller.rb | 10 | ||||
-rw-r--r-- | app/finders/snippets_finder.rb | 2 | ||||
-rw-r--r-- | app/helpers/icons_helper.rb | 2 | ||||
-rw-r--r-- | app/helpers/snippets_helper.rb | 19 | ||||
-rw-r--r-- | app/helpers/visibility_level_helper.rb | 6 | ||||
-rw-r--r-- | app/models/snippet.rb | 13 | ||||
-rw-r--r-- | app/views/dashboard/snippets/index.html.haml | 2 | ||||
-rw-r--r-- | app/views/projects/snippets/index.html.haml | 2 | ||||
-rw-r--r-- | app/views/shared/_visibility_radios.html.haml | 2 | ||||
-rw-r--r-- | app/views/shared/snippets/_header.html.haml | 4 | ||||
-rw-r--r-- | app/views/snippets/_snippets_scope_menu.html.haml | 7 | ||||
-rw-r--r-- | db/migrate/20180610102249_add_secret_word_to_snippet.rb | 9 | ||||
-rw-r--r-- | db/schema.rb | 3 | ||||
-rw-r--r-- | lib/gitlab/visibility_level.rb | 40 | ||||
-rw-r--r-- | spec/factories/snippets.rb | 3 | ||||
-rw-r--r-- | spec/helpers/snippets_helper_spec.rb | 84 | ||||
-rw-r--r-- | spec/helpers/visibility_level_helper_spec.rb | 31 | ||||
-rw-r--r-- | spec/lib/gitlab/visibility_level_spec.rb | 25 | ||||
-rw-r--r-- | spec/models/snippet_spec.rb | 19 |
20 files changed, 303 insertions, 19 deletions
diff --git a/app/controllers/concerns/snippets_url.rb b/app/controllers/concerns/snippets_url.rb new file mode 100644 index 00000000000..e79b7194b27 --- /dev/null +++ b/app/controllers/concerns/snippets_url.rb @@ -0,0 +1,39 @@ +module SnippetsUrl + extend ActiveSupport::Concern + + private + + attr_reader :snippet + + def authorize_secret_snippet! + if snippet.secret? + return if params[:secret] == snippet.secret_word + + return render_404 + end + + current_user ? render_404 : authenticate_user! + end + + def ensure_complete_url + redirect_to complete_url unless url_contains_secret? + end + + def url_contains_secret? + request.query_parameters['secret'] == snippet.secret_word + end + + def complete_url + @complete_url ||= begin + url = current_url + query_hash = Rack::Utils.parse_nested_query(url.query) + query_hash['secret'] = snippet.secret_word + url.query = query_hash.to_query + url.to_s + end + end + + def current_url + @current_url ||= URI.parse(request.original_url) + end +end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 3d51520ddf4..890614c0e28 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -3,6 +3,7 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji include SpammableActions include SnippetsActions + include SnippetsUrl include RendersBlob include PreviewMarkdown @@ -13,6 +14,9 @@ class SnippetsController < ApplicationController # Allow read snippet before_action :authorize_read_snippet!, only: [:show, :raw] + # Ensure we're displaying the correct url, specifically for secret snippets + before_action :ensure_complete_url, only: [:show, :raw] + # Allow modify snippet before_action :authorize_update_snippet!, only: [:edit, :update] @@ -108,11 +112,7 @@ class SnippetsController < ApplicationController def authorize_read_snippet! return if can?(current_user, :read_personal_snippet, @snippet) - if current_user - render_404 - else - authenticate_user! - end + authorize_secret_snippet! end def authorize_update_snippet! diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index d498a2d6d11..b678f481ca5 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -124,6 +124,8 @@ class SnippetsFinder < UnionFinder Snippet::PRIVATE when 'are_internal' Snippet::INTERNAL + when 'are_secret' + Snippet::SECRET when 'are_public' Snippet::PUBLIC else diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 2f304b040c7..12406673836 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -82,6 +82,8 @@ module IconsHelper 'lock' when Gitlab::VisibilityLevel::INTERNAL 'shield' + when Gitlab::VisibilityLevel::SECRET + 'user-secret' else # Gitlab::VisibilityLevel::PUBLIC 'globe' end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 733832c1bbb..f5db47cd6a3 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -1,5 +1,7 @@ module SnippetsHelper - def reliable_snippet_path(snippet, opts = nil) + def reliable_snippet_path(snippet, opts = {}) + opts[:secret] = snippet.secret_word if snippet.secret? + if snippet.project_id? project_snippet_path(snippet.project, snippet, opts) else @@ -7,6 +9,21 @@ module SnippetsHelper end end + def reliable_snippet_url(snippet, opts = {}) + opts[:secret] = snippet.secret_word if snippet.secret? + + if snippet.project_id? + project_snippet_url(snippet.project, snippet, opts) + else + snippet_url(snippet, opts) + end + end + + def shareable_snippets_link(snippet) + url = reliable_snippet_url(snippet) + link_to(url, url, id: 'shareable_link_url', title: 'Open') + end + def download_snippet_path(snippet) if snippet.project_id raw_project_snippet_path(@project, snippet, inline: false) diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index e395cda03d3..f2c81c87f3b 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -60,6 +60,8 @@ module VisibilityLevelHelper "The snippet is visible to any logged in user." when Gitlab::VisibilityLevel::PUBLIC "The snippet can be accessed without any authentication." + when Gitlab::VisibilityLevel::SECRET + "The snippet can be accessed without any authentication, but is not searchable." end end @@ -143,9 +145,7 @@ module VisibilityLevelHelper end def visibility_level_label(level) - # The visibility level can be: - # 'VisibilityLevel|Private', 'VisibilityLevel|Internal', 'VisibilityLevel|Public' - s_(Project.visibility_levels.key(level)) + s_(::Gitlab::VisibilityLevel.all_options.key(level)) end def restricted_visibility_levels(show_all = false) diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 644120453cf..4ee229e3013 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -35,18 +35,21 @@ class Snippet < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true, allow_nil: true + before_save :ensure_secret_word_added_if_needed + validates :author, presence: true validates :title, presence: true, length: { maximum: 255 } validates :file_name, length: { maximum: 255 } validates :content, presence: true - validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } + validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.all_values } # Scopes scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) } scope :are_private, -> { where(visibility_level: Snippet::PRIVATE) } scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } + scope :are_secret, -> { where(visibility_level: Snippet::SECRET) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } @@ -173,4 +176,12 @@ class Snippet < ActiveRecord::Base ::Project end end + + private + + def ensure_secret_word_added_if_needed + return if secret? && self.secret_word + + self.secret_word = secret? ? SecureRandom.hex : nil + end end diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 4391624196b..7c2638baee2 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -3,7 +3,7 @@ - header_title "Snippets", dashboard_snippets_path = render 'dashboard/snippets_head' -= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true } += render partial: 'snippets/snippets_scope_menu', locals: { include_private: true, include_secret: true } .d-block.d-sm-none diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 65efc083fdd..7a3a16ad44e 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -3,7 +3,7 @@ - if current_user .top-area - include_private = @project.team.member?(current_user) || current_user.admin? - = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } + = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private, include_secret: false } .nav-controls - if can?(current_user, :create_project_snippet, @project) diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index dd6b9cce58e..8a95ab52f41 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -1,4 +1,4 @@ -- Gitlab::VisibilityLevel.values.each do |level| +- Gitlab::VisibilityLevel.values_for(form_model).each do |level| - disallowed = disallowed_visibility_level?(form_model, level) - restricted = restricted_visibility_levels.include?(level) - disabled = disallowed || restricted diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 828ec870dc0..f644d4ca871 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -26,6 +26,10 @@ %textarea.hidden.js-task-list-field = @snippet.description + .shareable_link + = shareable_snippets_link(@snippet) + = clipboard_button(title: s_('Copy to clipboard'), class: 'js-clipboard-btn snippet-clipboard-btn', target: '#shareable_link_url') + - if @snippet.updated_at != @snippet.created_at = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true) diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml index dc4b0fd9ba0..6c1ed174bc0 100644 --- a/app/views/snippets/_snippets_scope_menu.html.haml +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -24,6 +24,13 @@ %span.badge.badge-pill = subject.snippets.are_internal.count + - if include_secret + %li{ class: active_when(params[:scope] == "are_secret") } + = link_to subject_snippets_path(subject, scope: 'are_secret') do + Secret + %span.badge.badge-pill + = subject.snippets.are_secret.count + %li{ class: active_when(params[:scope] == "are_public") } = link_to subject_snippets_path(subject, scope: 'are_public') do Public diff --git a/db/migrate/20180610102249_add_secret_word_to_snippet.rb b/db/migrate/20180610102249_add_secret_word_to_snippet.rb new file mode 100644 index 00000000000..315bd48c8aa --- /dev/null +++ b/db/migrate/20180610102249_add_secret_word_to_snippet.rb @@ -0,0 +1,9 @@ +class AddSecretWordToSnippet < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :snippets, :secret_word, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index d05c6afbb9f..6fc0e16a768 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180608201435) do +ActiveRecord::Schema.define(version: 20180610102249) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1833,6 +1833,7 @@ ActiveRecord::Schema.define(version: 20180608201435) do t.integer "cached_markdown_version" t.text "description" t.text "description_html" + t.string "secret_word" end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 2612208a927..d01b2ed0ced 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -20,10 +20,9 @@ module Gitlab PRIVATE = 0 unless const_defined?(:PRIVATE) INTERNAL = 10 unless const_defined?(:INTERNAL) PUBLIC = 20 unless const_defined?(:PUBLIC) + SECRET = 30 unless const_defined?(:SECRET) class << self - delegate :values, to: :options - def levels_for_user(user = nil) return [PUBLIC] unless user @@ -36,8 +35,13 @@ module Gitlab end end - def string_values - string_options.keys + def values_for(model) + case model + when PersonalSnippet + all_values + else + values + end end def options @@ -48,11 +52,33 @@ module Gitlab } end + def values + options.values + end + + def all_options + { + N_('VisibilityLevel|Private') => PRIVATE, + N_('VisibilityLevel|Internal') => INTERNAL, + N_('VisibilityLevel|Public') => PUBLIC, + N_('VisibilityLevel|Secret') => SECRET + } + end + + def all_values + all_options.values + end + + def string_values + string_options.keys + end + def string_options { 'private' => PRIVATE, 'internal' => INTERNAL, - 'public' => PUBLIC + 'public' => PUBLIC, + 'secret' => SECRET } end @@ -125,6 +151,10 @@ module Gitlab visibility_level_value == PUBLIC end + def secret? + visibility_level_value == SECRET + end + def visibility_level_value self[visibility_level_field] end diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb index dc12b562108..3ac445d5587 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -25,5 +25,8 @@ FactoryBot.define do end factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do + trait :secret do + visibility_level Snippet::SECRET + end end end diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb index 0323ffb641c..1860fee89a0 100644 --- a/spec/helpers/snippets_helper_spec.rb +++ b/spec/helpers/snippets_helper_spec.rb @@ -3,6 +3,90 @@ require 'spec_helper' describe SnippetsHelper do include IconsHelper + describe '#reliable_snippet_path' do + context 'personal snippets' do + context 'public' do + it 'gives a full path' do + snippet = create(:personal_snippet, :public) + + expect(reliable_snippet_path(snippet)).to eq('/snippets/1') + end + end + + context 'secret' do + it 'gives a full path, including secret word' do + snippet = create(:personal_snippet, :secret) + + expect(reliable_snippet_path(snippet)).to match(%r{/snippets/2\?secret=\w+}) + end + end + end + + context 'project snippets' do + it 'gives a full path' do + snippet = create(:project_snippet, :public) + + expect(reliable_snippet_path(snippet)).to eq('/namespace1/project1/snippets/3') + end + end + end + + describe '#reliable_snippet_url' do + context 'personal snippets' do + context 'public' do + it 'gives a full url' do + snippet = create(:personal_snippet, :public) + + expect(reliable_snippet_url(snippet)).to eq('http://test.host/snippets/1') + end + end + + context 'secret' do + it 'gives a full url, including secret word' do + snippet = create(:personal_snippet, :secret) + + expect(reliable_snippet_url(snippet)).to match(%r{http://test.host/snippets/2\?secret=\w+}) + end + end + end + + context 'project snippets' do + it 'gives a full url' do + snippet = create(:project_snippet, :public) + + expect(reliable_snippet_url(snippet)).to eq('http://test.host/namespace1/project1/snippets/3') + end + end + end + + describe '#shareable_snippets_link' do + context 'personal snippets' do + context 'public' do + it 'gives a full link' do + snippet = create(:personal_snippet, :public) + + expect(reliable_snippet_url(snippet)).to eq('/snippets/1') + end + end + + context 'secret' do + it 'gives a full link, including secret word' do + snippet = create(:personal_snippet, :secret) + + expect(reliable_snippet_url(snippet)).to eq(%r{/snippets/2\?secret=\w+}) + end + end + end + + context 'project snippets' do + it 'gives a full link' do + snippet = create(:project_snippet, :public) + + expect(reliable_snippet_url(snippet)).to eq('/namespace1/project1/snippets/3') + end + end + end + describe '#embedded_snippet_raw_button' do it 'gives view raw button of embedded snippets for project snippets' do @snippet = create(:project_snippet, :public) diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb index 5077c89d7b4..fb132bae73b 100644 --- a/spec/helpers/visibility_level_helper_spec.rb +++ b/spec/helpers/visibility_level_helper_spec.rb @@ -52,6 +52,11 @@ describe VisibilityLevelHelper do .to eq "The snippet is visible only to project members." end + it 'describes visibility for secret snippets' do + expect(snippet_visibility_level_description(Gitlab::VisibilityLevel::SECRET, personal_snippet)) + .to eq "The snippet can be accessed without any authentication, but is not searchable." + end + it 'defaults to personal snippet' do expect(snippet_visibility_level_description(Gitlab::VisibilityLevel::PRIVATE)) .to eq "The snippet is visible only to me." @@ -137,4 +142,30 @@ describe VisibilityLevelHelper do end end end + + describe '.visibility_level_label' do + context 'PRIVATE' do + it 'returns Private' do + expect(visibility_level_label(Gitlab::VisibilityLevel::PRIVATE)).to eq('Private') + end + end + + context 'INTERNAL' do + it 'returns Internal' do + expect(visibility_level_label(Gitlab::VisibilityLevel::INTERNAL)).to eq('Internal') + end + end + + context 'PUBLIC' do + it 'returns Public' do + expect(visibility_level_label(Gitlab::VisibilityLevel::PUBLIC)).to eq('Public') + end + end + + context 'SECRET' do + it 'returns Secret' do + expect(visibility_level_label(Gitlab::VisibilityLevel::SECRET)).to eq('Secret') + end + end + end end diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 2c1146ceff5..44163ebd05f 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -85,4 +85,29 @@ describe Gitlab::VisibilityLevel do .to eq(described_class::PRIVATE) end end + + describe '.values_for' do + context 'PersonalSnippet' do + it 'returns PRIVATE, INTERNAL, PUBLIC and SECRET' do + expect(described_class.values_for(PersonalSnippet.new)) + .to eq([ + Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC, + Gitlab::VisibilityLevel::SECRET + ]) + end + end + + context 'any other model' do + it 'returns PRIVATE, INTERNAL and PUBLIC' do + expect(described_class.values_for(Project.new)) + .to eq([ + Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC + ]) + end + end + end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index e09d89d235d..81dedc89343 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -212,4 +212,23 @@ describe Snippet do expect(blob.data).to eq(snippet.content) end end + + describe '#ensure_secret_word_added_if_needed' do + let(:snippet) { create(:snippet) } + + context 'visibility_level is SECRET' do + it 'assigns a random hex value' do + snippet.visibility_level = Gitlab::VisibilityLevel::SECRET + snippet.save + expect(snippet.secret_word).not_to be_nil + end + end + + context 'visibility_level is NOT SECRET' do + it 'assigns a random hex value' do + snippet.save + expect(snippet.secret_word).to be_nil + end + end + end end |