diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js index 6606271c4fa7bdd919254ec39a8e83ffe120189c..8fefad448f842d78686e7227215c35dbc134e503 100644 --- a/app/assets/javascripts/snippet/snippet_embed.js +++ b/app/assets/javascripts/snippet/snippet_embed.js @@ -1,28 +1,30 @@ import { __ } from '~/locale'; +import { parseUrlPathname, parseUrl } from '../lib/utils/common_utils'; export default () => { const shareBtn = document.querySelector('.js-share-btn'); if (shareBtn) { - const { protocol, host, pathname } = window.location; - const embedBtn = document.querySelector('.js-embed-btn'); - const snippetUrlArea = document.querySelector('.js-snippet-url-area'); const embedAction = document.querySelector('.js-embed-action'); - const url = `${protocol}//${host + pathname}`; + const dataUrl = snippetUrlArea.getAttribute('data-url'); shareBtn.addEventListener('click', () => { shareBtn.classList.add('is-active'); embedBtn.classList.remove('is-active'); - snippetUrlArea.value = url; + snippetUrlArea.value = dataUrl; embedAction.innerText = __('Share'); }); embedBtn.addEventListener('click', () => { + const parser = parseUrl(dataUrl); + const url = `${parser.origin + parseUrlPathname(dataUrl)}`; + const params = parser.search; + const scriptTag = ``; + embedBtn.classList.add('is-active'); shareBtn.classList.remove('is-active'); - const scriptTag = ``; snippetUrlArea.value = scriptTag; embedAction.innerText = __('Embed'); }); diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index 605d70d440be4bab7c9cdd36369846cea4487935..f91c256253fb0aab0a489e0c3381ef221a8372e4 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -28,7 +28,7 @@ def commands end def snippets - render json: autocomplete_service.snippets + render json: autocomplete_service.snippets.to_json(only: [:id, :title]) end private diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 5805d068e211ed4c109c6e532c80163580471a6d..55abcbd9197951e39b8e793cb7ab9e711e3063d3 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -123,13 +123,10 @@ def spammable_path end def authorize_read_snippet! - return if can?(current_user, :read_personal_snippet, @snippet) + return if can?(current_user, :read_personal_snippet, snippet) + return if snippet&.secret? && snippet&.valid_secret_token?(params[:token]) - if current_user - render_404 - else - authenticate_user! - end + current_user ? render_404 : authenticate_user! end def authorize_update_snippet! diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index bd6b6190fb538d3eba229e27834983e434cce056..fc063d7e5fc528ad65212443c1880884401b0a51 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -161,6 +161,8 @@ def visibility_from_scope case scope when 'are_private' Snippet::PRIVATE + when 'are_secret' + Snippet::SECRET when 'are_internal' Snippet::INTERNAL when 'are_public' diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 5c24b0e1704e31089c0891ce6b7e155eef701225..6c44a52d473cf64103016bf88c521fb90dd8ef01 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -141,11 +141,7 @@ def blob_raw_url(**kwargs) if @build && @entry raw_project_job_artifacts_url(@project, @build, path: @entry.path, **kwargs) elsif @snippet - if @snippet.project_id - raw_project_snippet_url(@project, @snippet, **kwargs) - else - raw_snippet_url(@snippet, **kwargs) - end + reliable_raw_snippet_url(@snippet) elsif @blob project_raw_url(@project, @id, **kwargs) end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 4f73270577fc60683526a4c514978e05a4615247..ecf483c1233c9f03c63439b3643c3da8d0d4491a 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -94,6 +94,8 @@ def visibility_level_icon(level, fw: true, options: {}) case level when Gitlab::VisibilityLevel::PRIVATE 'lock' + when Gitlab::VisibilityLevel::SECRET + 'user-secret' when Gitlab::VisibilityLevel::INTERNAL 'shield' else # Gitlab::VisibilityLevel::PUBLIC diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 6ccc1fb2ed1ffb2562f5692c9d6fa8a4a1f02206..ab2080386a1bdde71dad86a84b8f8f5309e90db3 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -11,22 +11,45 @@ def snippets_upload_path(snippet, user) end end - def reliable_snippet_path(snippet, opts = nil) - if snippet.project_id? - project_snippet_path(snippet.project, snippet, opts) - else - snippet_path(snippet, opts) + def reliable_snippet_path(snippet, opts = {}) + reliable_snippet_url(snippet, opts.merge(only_path: true)) + end + + def reliable_raw_snippet_path(snippet, opts = {}) + reliable_raw_snippet_url(snippet, opts.merge(only_path: true)) + end + + def reliable_snippet_url(snippet, opts = {}) + reliable_snippet_helper(snippet, opts) do |updated_opts| + if snippet.project_id? + project_snippet_url(snippet.project, snippet, nil, updated_opts) + else + snippet_url(snippet, nil, updated_opts) + end end end - def download_snippet_path(snippet) - if snippet.project_id - raw_project_snippet_path(@project, snippet, inline: false) - else - raw_snippet_path(snippet, inline: false) + def reliable_raw_snippet_url(snippet, opts = {}) + reliable_snippet_helper(snippet, opts) do |updated_opts| + if snippet.project_id? + raw_project_snippet_url(snippet.project, snippet, nil, updated_opts) + else + raw_snippet_url(snippet, nil, updated_opts) + end end end + def reliable_snippet_helper(snippet, opts) + opts[:token] = snippet.secret_token if snippet.secret? + opts[:only_path] = opts.fetch(:only_path, false) + + yield(opts) + end + + def download_raw_snippet_button(snippet) + link_to(icon('download'), reliable_raw_snippet_path(snippet, inline: false), target: '_blank', rel: 'noopener noreferrer', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }) + end + # Return the path of a snippets index for a user or for a project # # @returns String, path to snippet index @@ -114,8 +137,28 @@ def chunk_snippet(snippet, query, surrounding_lines = 3) { snippet_object: snippet, snippet_chunks: snippet_chunks } end - def snippet_embed - "" + def snippet_embed_url(snippet) + content_tag(:script, nil, src: reliable_snippet_url(snippet, format: :js, only_path: false)) + end + + def snippet_badge(snippet) + attrs = snippet_badge_attributes(snippet) + if attrs + css_class, text = attrs + tag.span(class: ['badge', 'badge-gray']) do + concat(tag.i(class: ['fa', css_class])) + concat(' ') + concat(_(text)) + end + end + end + + def snippet_badge_attributes(snippet) + if snippet.private? + ['fa-lock', 'private'] + elsif snippet.secret? + ['fa-user-secret', 'secret'] + end end def embedded_snippet_raw_button diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index a36de5dc548289f8a04df1e30e1a6e1b8f33899e..f8615ae0aad2d7ec70bba3020ab7091619b6bead 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -58,6 +58,8 @@ def snippet_visibility_level_description(level, snippet = nil) else _("The snippet is visible only to me.") end + when Gitlab::VisibilityLevel::SECRET + _("The snippet can be accessed without any authentication, but is not searchable.") when Gitlab::VisibilityLevel::INTERNAL _("The snippet is visible to any logged in user.") when Gitlab::VisibilityLevel::PUBLIC @@ -130,9 +132,7 @@ def project_visibility_icon_description(level) 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/personal_snippet.rb b/app/models/personal_snippet.rb index 1b5be8698b152f85ea96ace18ed1a40f8e47b9d5..f0cc1ed46cfa555bdf5abefd575be670c4ca7a58 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -2,4 +2,28 @@ class PersonalSnippet < Snippet include WithUploads + + before_save :update_secret_token + + def embeddable? + super || secret? + end + + alias_method :visibility_secret?, :secret? + def secret? + visibility_secret? && secret_token? + end + + private + + def update_secret_token + # secret? checks the visibility and also if the token exists + return if secret? + + # If the visibility is secret assign a random value, otherwise + # assign a nil value + self.secret_token = if visibility_secret? + SecureRandom.hex + end + end end diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index e732c1bd86fe62b54450191a5da13b012f26b16e..a5ce96ffdee9a63ec998abc039bcf65be1c343c0 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -4,4 +4,5 @@ class ProjectSnippet < Snippet belongs_to :project validates :project, presence: true + validates :visibility_level, exclusion: { in: [Gitlab::VisibilityLevel::SECRET] } end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 4010a3e216722006b0832a08f960c0797c103b2a..efa7bea7fa68cd4e9cd03336ac5196e39e0809a9 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -46,11 +46,12 @@ def content_html_invalidated? 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_secret, -> { where(visibility_level: Snippet::SECRET) } + scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) } scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } @@ -63,6 +64,12 @@ def content_html_invalidated? attr_spammable :title, spam_title: true attr_spammable :content, spam_description: true + attr_encrypted :secret_token, + key: Settings.attr_encrypted_db_key_base_truncated, + mode: :per_attribute_iv_and_salt, + insecure_mode: true, + algorithm: 'aes-256-cbc' + def self.with_optional_visibility(value = nil) if value where(visibility_level: value) @@ -222,6 +229,10 @@ def to_ability_name model_name.singular end + def valid_secret_token?(secret) + ActiveSupport::SecurityUtils.secure_compare(secret.to_s, secret_token) + end + class << self # Searches for snippets with a matching title or file name. # diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 2caa8e0cac4b6a60ba8c37fb16468527672094bf..db6bb160c6b29c1071fb2404189a460b7da2599b 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -4,7 +4,7 @@ = render 'dashboard/snippets_head' - if current_user.snippets.exists? - = render partial: 'snippets/snippets_scope_menu', locals: { include_private: true } + = render partial: 'snippets/snippets_scope_menu', locals: { include_private: true, include_secret: true } - if current_user.snippets.exists? = render partial: 'shared/snippets/list', locals: { link_project: true } diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 7682d01a5a1e76a1f9e4109a4353360d1f73ce9f..a0ceb88ee7d86079f9409b3d935b243f0bdbe0a6 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -4,7 +4,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 } - if can?(current_user, :create_project_snippet, @project) .nav-controls diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index f17dae0a94ccc69562e9ce4869d06c7f61f06236..267996d305ed4fb46cb36fa3fb1e0fa1a1cf8e6e 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -3,14 +3,20 @@ - snippet_chunks = snippet_blob[:snippet_chunks] .search-result-row - %span - = snippet.title - by - = link_to user_snippets_path(snippet.author) do - = image_tag avatar_icon_for_user(snippet.author), class: "avatar avatar-inline s16", alt: '' - = snippet.author_name - %span.light= time_ago_with_tooltip(snippet.created_at) %h4.snippet-title + = link_to reliable_snippet_path(snippet) do + = snippet.title + + .snippet-box.has-tooltip.inline.append-right-5.append-bottom-10{ title: snippet_visibility_level_description(snippet.visibility_level, snippet), data: { container: "body" } } + %span.sr-only + = visibility_level_label(snippet.visibility_level) + = visibility_level_icon(snippet.visibility_level, fw: false) + %span.creator + Authored + = time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') + by #{link_to_member(snippet.project, snippet.author, size: 24, author_class: "author item-title", avatar_class: "d-none d-sm-inline")} + = user_status(snippet.author) + - snippet_path = reliable_snippet_path(snippet) .file-holder .js-file-title.file-title diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 1e01088d9e69369ea5b0e740529c77dfd8504de8..7280146720efa88671b99f8418386924cdb75ab5 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -2,10 +2,7 @@ %h4.snippet-title.term = link_to reliable_snippet_path(snippet_title) do = truncate(snippet_title.title, length: 60) - - if snippet_title.private? - %span.badge.badge-gray - %i.fa.fa-lock - = _("private") + = snippet_badge(snippet_title) %span.cgray.monospace.tiny.float-right.term = snippet_title.file_name diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index 80532c9187bd5589c2fa4a72bd4d4471a927ba12..fcc8a2ab8a3eb2002471187f2501e6cb7a6b39f0 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) - next if disallowed || restricted diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 2132fcbccc57e899abc65024c58379a130c28df4..6a5e777706c868f7a202ec981678851bc025da74 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -8,7 +8,6 @@ .btn-group{ role: "group" }< = copy_blob_source_button(blob) = open_raw_blob_button(blob) - - = link_to icon('download'), download_snippet_path(@snippet), target: '_blank', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } + = download_raw_snippet_button(@snippet) = render 'projects/blob/content', blob: blob diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 8d94a87a77518966dcd0010af9d44ec5592eb7c3..fe99b5d15098d8e9b129798668dec7ee1bea55ae 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -44,7 +44,7 @@ %li %button.js-share-btn.btn.btn-transparent{ type: 'button' } %strong.embed-toggle-list-item= _("Share") - %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed } + %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', data: { url: reliable_snippet_url(@snippet) }, value: snippet_embed_url(@snippet) } .input-group-append = clipboard_button(title: _('Copy'), class: 'js-clipboard-btn snippet-clipboard-btn btn btn-default', target: '.js-snippet-url-area') .clearfix diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml index c312226dd6c9878acd5e7b0d16e77408fea00140..1eb3484288129487c51cb57fd8918fa7720cc7ed 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/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 36b4e00e8d51b4c7f0b366fed0a56e4334a77ef7..afa564652583fb0bf51b81681c5bc60bc2e49e0e 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -10,7 +10,7 @@ %article.file-holder.snippet-file-content = render 'shared/snippets/blob' - .row-content-block.top-block.content-component-block + .row-content-block.top-block.content-component-block.append-bottom-10 = render 'award_emoji/awards_block', awardable: @snippet, inline: true #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false diff --git a/changelogs/unreleased/13235-secret-snippets.yml b/changelogs/unreleased/13235-secret-snippets.yml new file mode 100644 index 0000000000000000000000000000000000000000..e2635abe6977daba43cdac117b544d8ad9b4e31c --- /dev/null +++ b/changelogs/unreleased/13235-secret-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Support Secret Snippets +merge_request: 24042 +author: +type: changed diff --git a/db/migrate/20191025092748_add_secret_to_snippet.rb b/db/migrate/20191025092748_add_secret_to_snippet.rb new file mode 100644 index 0000000000000000000000000000000000000000..4135302a2e0672e918b799cf2e4e78fcf40e120f --- /dev/null +++ b/db/migrate/20191025092748_add_secret_to_snippet.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddSecretToSnippet < 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 + add_column :snippets, :encrypted_secret_token_salt, :string, limit: 255 + end +end diff --git a/db/schema.rb b/db/schema.rb index f6a445f3da6d7b5bef6f986f676b96ca4fde4b10..ac7c338c78bb9305e90780ecda5d1e0fdb9aa868 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -3490,6 +3490,9 @@ t.integer "cached_markdown_version" t.text "description" t.text "description_html" + t.string "encrypted_secret_token", limit: 255 + t.string "encrypted_secret_token_iv", limit: 255 + t.string "encrypted_secret_token_salt", limit: 255 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 ["file_name"], name: "index_snippets_on_file_name_trigram", opclass: :gin_trgm_ops, using: :gin diff --git a/ee/lib/gitlab/elastic/snippet_search_results.rb b/ee/lib/gitlab/elastic/snippet_search_results.rb index d146930dfed80a54860f2c9d77fd44169a87cc5a..56596992db5d5523e93ae25481cae073a6c2d376 100644 --- a/ee/lib/gitlab/elastic/snippet_search_results.rb +++ b/ee/lib/gitlab/elastic/snippet_search_results.rb @@ -8,9 +8,9 @@ def objects(scope, page = 1) case scope when 'snippet_titles' - eager_load(snippet_titles, page, eager: { project: [:route, :namespace] }) + eager_load(snippet_titles, page, eager: { project: [:route, :namespace], author: :status }) when 'snippet_blobs' - eager_load(snippet_blobs, page, eager: { project: [:route, :namespace] }) + eager_load(snippet_blobs, page, eager: { project: [:route, :namespace], author: :status }) end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 9faae4605270b3a83b43ef6a1f0a49dc42c8a3fc..c23199eda1699ef174d04b821e3e1b2f5304ef28 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -532,6 +532,7 @@ class PersonalSnippet < Snippet expose :raw_url do |snippet| Gitlab::UrlBuilder.build(snippet) + "/raw" end + expose :secret_token, if: -> (entity, _) { entity.secret? } end class IssuableEntity < Grape::Entity diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 1aafe5804c00c4a7a6a28e68c8fd2779abec8526..0088e43b60d9741159118cf61f73ca8b35147143 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -163,6 +163,9 @@ excluded_attributes: - :identifier snippets: - :expired_at + - :encrypted_secret_token + - :encrypted_secret_token_iv + - :encrypted_secret_token_salt merge_request_diff: - :external_diff - :stored_externally diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index e955ccd35daed9affbaf2c352714ed8876350228..44ac0dd21dbe7e10fc450f069a671acedc1701bf 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -53,11 +53,11 @@ def snippets # rubocop: enable CodeReuse/ActiveRecord def snippet_titles - snippets.search(query) + snippets.search(query).inc_relations_for_view end def snippet_blobs - snippets.search_code(query) + snippets.search_code(query).inc_relations_for_view end def default_scope diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index e2787744f09dd29f6aa6e656dd0957f76ca1c752..1d290687e7eb79b04af56e9ef6176ad952d0f388 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -20,12 +20,11 @@ module VisibilityLevel end PRIVATE = 0 unless const_defined?(:PRIVATE) + SECRET = 5 unless const_defined?(:SECRET) INTERNAL = 10 unless const_defined?(:INTERNAL) PUBLIC = 20 unless const_defined?(:PUBLIC) class << self - delegate :values, to: :options - def levels_for_user(user = nil) return [PUBLIC] unless user @@ -38,8 +37,13 @@ def levels_for_user(user = nil) end end - def string_values - string_options.keys + def values_for(model) + case model + when PersonalSnippet + all_values + else + values + end end def options @@ -50,14 +54,36 @@ def options } end + def values + options.values + end + + def all_options + { + N_('VisibilityLevel|Private') => PRIVATE, + N_('VisibilityLevel|Secret') => SECRET, + N_('VisibilityLevel|Internal') => INTERNAL, + N_('VisibilityLevel|Public') => PUBLIC + } + end + + def all_values + all_options.values + end + def string_options { 'private' => PRIVATE, + 'secret' => SECRET, 'internal' => INTERNAL, 'public' => PUBLIC } end + def string_values + string_options.keys + end + def allowed_levels restricted_levels = Gitlab::CurrentSettings.restricted_visibility_levels @@ -127,6 +153,10 @@ def public? visibility_level_value == PUBLIC end + def secret? + visibility_level_value == SECRET + end + def visibility_level_value self[visibility_level_field] end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index dde99a8583ac347c322aa7c06238ae68f75b5f91..f6a7f5b177948843594554b133f1046dacc97302 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16760,6 +16760,9 @@ msgstr "" msgid "The schedule time must be in the future!" msgstr "" +msgid "The snippet can be accessed without any authentication, but is not searchable." +msgstr "" + msgid "The snippet can be accessed without any authentication." msgstr "" @@ -18743,6 +18746,9 @@ msgstr "" msgid "VisibilityLevel|Public" msgstr "" +msgid "VisibilityLevel|Secret" +msgstr "" + msgid "VisibilityLevel|Unknown" msgstr "" @@ -20570,9 +20576,6 @@ msgid_plural "points" msgstr[0] "" msgstr[1] "" -msgid "private" -msgstr "" - msgid "private key does not match certificate." msgstr "" diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index e892c736c69f6698775975b7ec7d3b88e150faf6..bebe9831cb29c7b6f5f664549fd132eebd568ff0 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -5,9 +5,79 @@ describe SnippetsController do let(:user) { create(:user) } - describe 'GET #index' do - let(:user) { create(:user) } + shared_examples 'allow read access to secret snippets' do + let_it_be(:secret_snippet) { create(:personal_snippet, :secret) } + let(:author) { secret_snippet.author } + let(:snippet_params) { { id: secret_snippet.to_param } } + let(:secret_token) { secret_snippet.secret_token } + let(:params) { snippet_params } + + before do + sign_in(user) if user + + subject + end + + context 'when not signed in' do + let(:user) { nil } + + it 'redirects to the sign in page' do + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when user is not the author' do + context 'when the token is not present' do + it 'responds with status 404' do + expect(response).to have_gitlab_http_status(404) + end + end + context 'when the token is not valid' do + let(:params) { snippet_params.merge(token: 'foo') } + + it 'responds with status 404' do + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when the token is valid' do + let(:params) { snippet_params.merge(token: secret_token) } + + it 'responds with status 200' do + expect(response).to have_gitlab_http_status(200) + end + end + end + + context 'when user is the author' do + let(:user) { author } + + context 'when the token is not present' do + it 'responds with status 200' do + expect(response).to have_gitlab_http_status(200) + end + end + + context 'when the token is not valid' do + let(:params) { snippet_params.merge(token: 'foo') } + + it 'responds with status 200' do + expect(response).to have_gitlab_http_status(200) + end + end + + context 'when the token is valid' do + let(:params) { snippet_params.merge(token: secret_token) } + + it 'responds with status 200' do + expect(response).to have_gitlab_http_status(200) + end + end + end + end + + describe 'GET #index' do context 'when username parameter is present' do it_behaves_like 'paginated collection' do let(:collection) { Snippet.all } @@ -65,6 +135,10 @@ end describe 'GET #show' do + it_behaves_like 'allow read access to secret snippets' do + subject { get :show, params: params } + end + context 'when the personal snippet is private' do let(:personal_snippet) { create(:personal_snippet, :private, author: user) } @@ -306,12 +380,59 @@ def create_snippet(snippet_params = {}, additional_params = {}) end end + describe 'GET #edit' do + context 'when the snippet is secret' do + let_it_be(:snippet) { create :personal_snippet, :secret } + let(:user) { snippet.author } + let(:params) { { id: snippet.id, token: snippet.secret_token } } + + subject { get :edit, params: params } + + before do + sign_in(user) if user + + subject + end + + context 'when the user is not signed in' do + let(:user) { nil } + + it 'redirects to the sign in page' do + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when the user not the author' do + let(:user) { create(:user) } + + it 'responds with 404' do + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when the user is the author' do + it 'responds with 200' do + expect(response).to have_gitlab_http_status(200) + end + + context 'without the token param' do + let(:params) { { id: snippet.id } } + + it 'responds with 200' do + expect(response).to have_gitlab_http_status(200) + end + end + end + end + end + describe 'PUT #update' do let(:project) { create :project } - let(:snippet) { create :personal_snippet, author: user, project: project, visibility_level: visibility_level } + let(:snippet) { create :personal_snippet, visibility_level: visibility_level } + let(:user) { snippet.author } def update_snippet(snippet_params = {}, additional_params = {}) - sign_in(user) + sign_in(user) if user put :update, params: { id: snippet.id, @@ -425,6 +546,39 @@ def update_snippet(snippet_params = {}, additional_params = {}) end end end + + context 'when the snippet is secret' do + let(:visibility_level) { Snippet::SECRET } + + subject { update_snippet({ title: 'Foo' }, { token: snippet.secret_token }) } + + before do + subject + end + + context 'when the user is not authenticated' do + let(:user) { nil } + + it 'redirects to the sign in page' do + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when the user is not the author' do + let(:user) { create(:user) } + + it 'responds with 404' do + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when the user is the author' do + it 'responds with 302' do + expect(response).to have_gitlab_http_status(302) + expect(snippet.title).to eq 'Foo' + end + end + end end describe 'POST #mark_as_spam' do @@ -451,6 +605,10 @@ def mark_as_spam end describe "GET #raw" do + it_behaves_like 'allow read access to secret snippets' do + subject { get :raw, params: params } + end + context 'when the personal snippet is private' do let(:personal_snippet) { create(:personal_snippet, :private, author: user) } @@ -634,4 +792,50 @@ def mark_as_spam expect(json_response.keys).to match_array(%w(body references)) end end + + describe "DELETE #destroy" do + context 'when the snippet is secret' do + let(:snippet) { create :personal_snippet, :secret } + let(:user) { snippet.author } + let(:params) { { id: snippet.id, token: snippet.secret_token } } + + subject { delete :destroy, params: params } + + before do + sign_in(user) if user + + subject + end + + context 'when the user is not signed in' do + let(:user) { nil } + + it 'redirects to the sign in page' do + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when the user not the author' do + let(:user) { create(:user) } + + it 'responds with 404' do + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when the user is the author' do + it 'responds with 302' do + expect(response).to have_gitlab_http_status(302) + end + + context 'without the token param' do + let(:params) { { id: snippet.id } } + + it 'responds with 200' do + expect(response).to have_gitlab_http_status(302) + end + end + end + end + end end diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb index ede071ae70c3bd243169ae0a0c18d480583c7eb3..e399b918863a4913a10e73d18b8d2f01b52ac436 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -12,6 +12,10 @@ visibility_level { Snippet::PUBLIC } end + trait :secret do + visibility_level { Snippet::SECRET } + end + trait :internal do visibility_level { Snippet::INTERNAL } end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index bcb762664f7627ffb8f1950ca02ee9a3130077e0..6a1f5378966bbf08d9a7929e449ca50c695fd69d 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -18,6 +18,7 @@ describe '#execute' do let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } let_it_be(:admin) { create(:admin) } let_it_be(:group) { create(:group, :public) } let_it_be(:project) { create(:project, :public, group: group) } @@ -25,6 +26,7 @@ let_it_be(:private_personal_snippet) { create(:personal_snippet, :private, author: user) } let_it_be(:internal_personal_snippet) { create(:personal_snippet, :internal, author: user) } let_it_be(:public_personal_snippet) { create(:personal_snippet, :public, author: user) } + let_it_be(:secret_personal_snippet) { create(:personal_snippet, :secret, author: user) } let_it_be(:private_project_snippet) { create(:project_snippet, :private, project: project) } let_it_be(:internal_project_snippet) { create(:project_snippet, :internal, project: project) } @@ -32,94 +34,93 @@ context 'filter by scope' do it "returns all snippets for 'all' scope" do - snippets = described_class.new(user, scope: :all).execute - - expect(snippets).to contain_exactly( + expect(find_snippets(:all)).to contain_exactly( private_personal_snippet, internal_personal_snippet, public_personal_snippet, - internal_project_snippet, public_project_snippet + internal_project_snippet, public_project_snippet, secret_personal_snippet ) end it "returns all snippets for 'are_private' scope" do - snippets = described_class.new(user, scope: :are_private).execute - - expect(snippets).to contain_exactly(private_personal_snippet) + expect(find_snippets(:are_private)).to contain_exactly(private_personal_snippet) end it "returns all snippets for 'are_internal' scope" do - snippets = described_class.new(user, scope: :are_internal).execute - - expect(snippets).to contain_exactly(internal_personal_snippet, internal_project_snippet) + expect(find_snippets(:are_internal)).to contain_exactly(internal_personal_snippet, internal_project_snippet) end it "returns all snippets for 'are_public' scope" do - snippets = described_class.new(user, scope: :are_public).execute + expect(find_snippets(:are_public)).to contain_exactly(public_personal_snippet, public_project_snippet) + end + + it "returns all snippets for 'are_secret' scope" do + expect(find_snippets(:are_secret)).to contain_exactly(secret_personal_snippet) + end + + context 'when the user it not the author' do + let(:user) { other_user } + + it "returns all snippets for 'all' scope" do + expect(find_snippets(:all)).to contain_exactly( + internal_personal_snippet, public_personal_snippet, + internal_project_snippet, public_project_snippet + ) + end + end - expect(snippets).to contain_exactly(public_personal_snippet, public_project_snippet) + def find_snippets(scope) + described_class.new(user, scope: scope).execute end end context 'filter by author' do it 'returns all public and internal snippets' do - snippets = described_class.new(create(:user), author: user).execute - - expect(snippets).to contain_exactly(internal_personal_snippet, public_personal_snippet) + expect(find_snippets(other_user)).to contain_exactly(internal_personal_snippet, public_personal_snippet) end it 'returns internal snippets' do - snippets = described_class.new(user, author: user, scope: :are_internal).execute - - expect(snippets).to contain_exactly(internal_personal_snippet) + expect(find_snippets(user, scope: :are_internal)).to contain_exactly(internal_personal_snippet) end it 'returns private snippets' do - snippets = described_class.new(user, author: user, scope: :are_private).execute - - expect(snippets).to contain_exactly(private_personal_snippet) + expect(find_snippets(user, scope: :are_private)).to contain_exactly(private_personal_snippet) end it 'returns public snippets' do - snippets = described_class.new(user, author: user, scope: :are_public).execute + expect(find_snippets(user, scope: :are_public)).to contain_exactly(public_personal_snippet) + end - expect(snippets).to contain_exactly(public_personal_snippet) + it 'returns secret snippets' do + expect(find_snippets(user, scope: :are_secret)).to contain_exactly(secret_personal_snippet) end it 'returns all snippets' do - snippets = described_class.new(user, author: user).execute - - expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet) + expect(find_snippets(user)).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet, secret_personal_snippet) end it 'returns only public snippets if unauthenticated user' do - snippets = described_class.new(nil, author: user).execute - - expect(snippets).to contain_exactly(public_personal_snippet) + expect(find_snippets(nil)).to contain_exactly(public_personal_snippet) end it 'returns all snippets for an admin' do - snippets = described_class.new(admin, author: user).execute + expect(find_snippets(admin)).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet) + end - expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet) + def find_snippets(search_user, scope: nil) + described_class.new(search_user, author: user, scope: scope).execute end end context 'project snippets' do it 'returns public personal and project snippets for unauthorized user' do - snippets = described_class.new(nil, project: project).execute - - expect(snippets).to contain_exactly(public_project_snippet) + expect(find_snippets(nil)).to contain_exactly(public_project_snippet) end it 'returns public and internal snippets for non project members' do - snippets = described_class.new(user, project: project).execute - - expect(snippets).to contain_exactly(internal_project_snippet, public_project_snippet) + expect(find_snippets(user)).to contain_exactly(internal_project_snippet, public_project_snippet) end it 'returns public snippets for non project members' do - snippets = described_class.new(user, project: project, scope: :are_public).execute - - expect(snippets).to contain_exactly(public_project_snippet) + expect(find_snippets(user, scope: :are_public)).to contain_exactly(public_project_snippet) end it 'returns internal snippets for non project members' do @@ -129,31 +130,23 @@ end it 'does not return private snippets for non project members' do - snippets = described_class.new(user, project: project, scope: :are_private).execute - - expect(snippets).to be_empty + expect(find_snippets(user, scope: :are_private)).to be_empty end it 'returns all snippets for project members' do project.add_developer(user) - snippets = described_class.new(user, project: project).execute - - expect(snippets).to contain_exactly(private_project_snippet, internal_project_snippet, public_project_snippet) + expect(find_snippets(user)).to contain_exactly(private_project_snippet, internal_project_snippet, public_project_snippet) end it 'returns private snippets for project members' do project.add_developer(user) - snippets = described_class.new(user, project: project, scope: :are_private).execute - - expect(snippets).to contain_exactly(private_project_snippet) + expect(find_snippets(user, scope: :are_private)).to contain_exactly(private_project_snippet) end it 'returns all snippets for an admin' do - snippets = described_class.new(admin, project: project).execute - - expect(snippets).to contain_exactly(private_project_snippet, internal_project_snippet, public_project_snippet) + expect(find_snippets(admin)).to contain_exactly(private_project_snippet, internal_project_snippet, public_project_snippet) end context 'filter by author' do @@ -175,30 +168,32 @@ ) end end + + def find_snippets(search_user, author: nil, scope: nil) + described_class.new(search_user, author: author, scope: scope, project: project).execute + end end context 'explore snippets' do it 'returns only public personal snippets for unauthenticated users' do - snippets = described_class.new(nil, explore: true).execute - - expect(snippets).to contain_exactly(public_personal_snippet) + expect(find_snippets(nil)).to contain_exactly(public_personal_snippet) end it 'also returns internal personal snippets for authenticated users' do - snippets = described_class.new(user, explore: true).execute - - expect(snippets).to contain_exactly( + expect(find_snippets(user)).to contain_exactly( internal_personal_snippet, public_personal_snippet ) end it 'returns all personal snippets for admins' do - snippets = described_class.new(admin, explore: true).execute - - expect(snippets).to contain_exactly( + expect(find_snippets(admin)).to contain_exactly( private_personal_snippet, internal_personal_snippet, public_personal_snippet ) end + + def find_snippets(search_user) + described_class.new(search_user, explore: true).execute + end end context 'when the user cannot read cross project' do @@ -208,7 +203,9 @@ end it 'returns only personal snippets when the user cannot read cross project' do - expect(described_class.new(user).execute).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet) + expect(described_class.new(user).execute).to contain_exactly( + private_personal_snippet, internal_personal_snippet, public_personal_snippet, secret_personal_snippet + ) end end end @@ -220,6 +217,8 @@ let(:project) { create(:project) } let!(:snippet) { create(:project_snippet, :public, project: project) } + subject { described_class.new(user, project: project).execute } + before do project.add_maintainer(user) end @@ -232,17 +231,13 @@ it 'includes the result if the external service allows access' do external_service_allow_access(user, project) - results = described_class.new(user, project: project).execute - - expect(results).to contain_exactly(snippet) + expect(subject).to contain_exactly(snippet) end it 'does not include any results if the external service denies access' do external_service_deny_access(user, project) - results = described_class.new(user, project: project).execute - - expect(results).to be_empty + expect(subject).to be_empty end end end diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb index 66c8d576a4c9c6f1b8c8bbc18688191d593c38e3..0a4720c96f2468379e5be5052679e26f75c6c4e2 100644 --- a/spec/helpers/snippets_helper_spec.rb +++ b/spec/helpers/snippets_helper_spec.rb @@ -3,33 +3,104 @@ require 'spec_helper' describe SnippetsHelper do + include Gitlab::Routing include IconsHelper - describe '#embedded_snippet_raw_button' do + let_it_be(:public_personal_snippet) { create(:personal_snippet, :public) } + let_it_be(:secret_snippet) { create(:personal_snippet, :secret) } + let_it_be(:public_project_snippet) { create(:project_snippet, :public) } + + describe '.reliable_snippet_path' do + context 'personal snippets' do + context 'public' do + it 'gives a full path' do + expect(reliable_snippet_path(public_personal_snippet)).to eq("/snippets/#{public_personal_snippet.id}") + end + end + + context 'secret' do + it 'gives a full path, including secret word' do + expect(reliable_snippet_path(secret_snippet)).to match(%r{/snippets/#{secret_snippet.id}\?token=\w+}) + end + end + end + + context 'project snippets' do + it 'gives a full path' do + expect(reliable_snippet_path(public_project_snippet)).to eq("/#{public_project_snippet.project.full_path}/snippets/#{public_project_snippet.id}") + end + end + end + + describe '.reliable_snippet_url' do + context 'personal snippets' do + context 'public' do + it 'gives a full url' do + expect(reliable_snippet_url(public_personal_snippet)).to eq("http://test.host/snippets/#{public_personal_snippet.id}") + end + end + + context 'secret' do + it 'gives a full url, including secret word' do + expect(reliable_snippet_url(secret_snippet)).to match(%r{http://test.host/snippets/#{secret_snippet.id}\?token=\w+}) + end + end + end + + context 'project snippets' do + it 'gives a full url' do + expect(reliable_snippet_url(public_project_snippet)).to eq("http://test.host/#{public_project_snippet.project.full_path}/snippets/#{public_project_snippet.id}") + 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) + @snippet = public_project_snippet expect(embedded_snippet_raw_button.to_s).to eq("#{external_snippet_icon('doc-code')}") end it 'gives view raw button of embedded snippets for personal snippets' do - @snippet = create(:personal_snippet, :public) + @snippet = public_personal_snippet expect(embedded_snippet_raw_button.to_s).to eq("#{external_snippet_icon('doc-code')}") end end - describe '#embedded_snippet_download_button' do + describe '.embedded_snippet_download_button' do it 'gives download button of embedded snippets for project snippets' do - @snippet = create(:project_snippet, :public) + @snippet = public_project_snippet expect(embedded_snippet_download_button.to_s).to eq("#{external_snippet_icon('download')}") end it 'gives download button of embedded snippets for personal snippets' do - @snippet = create(:personal_snippet, :public) + @snippet = public_personal_snippet expect(embedded_snippet_download_button.to_s).to eq("#{external_snippet_icon('download')}") end end + + describe '.snippet_embed_url' do + context 'personal snippets' do + context 'public' do + it 'gives a full link' do + expect(snippet_embed_url(public_personal_snippet)).to eq("") + end + end + + context 'secret' do + it 'gives a full link, including secret word' do + expect(snippet_embed_url(secret_snippet)).to eq("") + end + end + end + + context 'project snippets' do + it 'gives a full link' do + expect(snippet_embed_url(public_project_snippet)).to eq("") + end + end + end end diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb index 1a176cfe965d279014ec944320e77253b40ce3e2..b4ebbde442bc8eda13bcc310b147d1221463ee08 100644 --- a/spec/helpers/visibility_level_helper_spec.rb +++ b/spec/helpers/visibility_level_helper_spec.rb @@ -79,6 +79,11 @@ .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." @@ -229,4 +234,30 @@ it { is_expected.to eq(expected) } 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 'SECRET' do + it 'returns Secret' do + expect(visibility_level_label(Gitlab::VisibilityLevel::SECRET)).to eq('Secret') + 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 + end end diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 75dc7d8e6d139f4fa6ebecc7de7fe48084abcf44..4950636021d1139949c8db0f883c2a2ce63fdaa5 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -95,4 +95,99 @@ expect(described_class.valid_level?(described_class::PUBLIC)).to be_truthy end end + + describe '.values_for' do + context 'PersonalSnippet' do + it 'returns PRIVATE, SECERT, INTERNAL and PUBLIC' do + expect(described_class.values_for(PersonalSnippet.new)) + .to eq([ + Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::SECRET, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC + ]) + 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 + + describe '.options' do + it 'returns a Hash of localized level name to const value mapping (excluding Secret)' do + expect(described_class.options) + .to eq( + 'VisibilityLevel|Private' => Gitlab::VisibilityLevel::PRIVATE, + 'VisibilityLevel|Internal' => Gitlab::VisibilityLevel::INTERNAL, + 'VisibilityLevel|Public' => Gitlab::VisibilityLevel::PUBLIC + ) + end + end + + describe '.values' do + it 'returns an Array of const values (excluding Secret)' do + expect(described_class.values) + .to eq([ + Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC + ]) + end + end + + describe '.all_options' do + it 'returns a Hash of localized level name to const value mapping (including Secret)' do + expect(described_class.all_options) + .to eq( + 'VisibilityLevel|Private' => Gitlab::VisibilityLevel::PRIVATE, + 'VisibilityLevel|Secret' => Gitlab::VisibilityLevel::SECRET, + 'VisibilityLevel|Internal' => Gitlab::VisibilityLevel::INTERNAL, + 'VisibilityLevel|Public' => Gitlab::VisibilityLevel::PUBLIC + ) + end + end + + describe '.all_values' do + it 'returns an Array of const values (including Secret)' do + expect(described_class.all_values) + .to eq([ + Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::SECRET, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC + ]) + end + end + + describe '.string_options' do + it 'returns a Hash of level name to const value mapping' do + expect(described_class.string_options) + .to eq( + 'private' => Gitlab::VisibilityLevel::PRIVATE, + 'secret' => Gitlab::VisibilityLevel::SECRET, + 'internal' => Gitlab::VisibilityLevel::INTERNAL, + 'public' => Gitlab::VisibilityLevel::PUBLIC + ) + end + end + + describe '.string_values' do + it 'returns an Array of const values (including Secret)' do + expect(described_class.string_values) + .to eq(%w{ + private + secret + internal + public + }) + end + end end diff --git a/spec/models/personal_snippet_spec.rb b/spec/models/personal_snippet_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a256a965a137aac969ad4f59ae1e699730ac1495 --- /dev/null +++ b/spec/models/personal_snippet_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe PersonalSnippet do + describe '#update_secret_token' do + let(:snippet) { create(:personal_snippet, :public) } + + context 'visibility_level is NOT SECRET' do + it 'does not update the secret_token' do + expect(snippet.secret_token).to be_nil + end + end + + context 'visibility_level is SECRET' do + let(:snippet) { create(:personal_snippet, :secret) } + + it 'assigns a random hex value' do + expect(snippet.secret_token).not_to be_nil + end + + context 'when the visibility_level changes to any other level' do + it 'sets the secret_token to nil' do + snippet.visibility_level = Gitlab::VisibilityLevel::PUBLIC + snippet.save + + expect(snippet.secret_token).to be_nil + end + end + end + end + + describe '#visibility_secret?' do + let(:snippet) { create(:personal_snippet, :public) } + + context 'when snippet visibility is not Secret' do + it 'returns false' do + expect(snippet.visibility_secret?).to be_falsey + end + end + + context 'when snippet visibility is Secret' do + let(:snippet) { create(:personal_snippet, :secret) } + + it 'returns true' do + expect(snippet.visibility_secret?).to be_truthy + end + end + end + + describe '#secret?' do + let(:snippet) { create(:personal_snippet, :public) } + + context 'when snippet visibility is not Secret' do + it 'returns false' do + expect(snippet.secret?).to be_falsey + end + end + + context 'when snippet visibility is Secret' do + let(:snippet) { create(:personal_snippet, :secret) } + + it 'returns true' do + expect(snippet.secret?).to be_truthy + end + + context 'when secret_token is empty' do + let(:snippet) { create(:personal_snippet, :public) } + + it 'returns false' do + snippet.update_column(:visibility_level, Gitlab::VisibilityLevel::SECRET) + + expect(snippet.visibility_secret?).to be_truthy + expect(snippet.secret?).to be_falsey + end + end + end + end + + describe '#embeddable?' do + [ + { snippet: :public, embeddable: true, secret_token: nil }, + { snippet: :internal, embeddable: false, secret_token: nil }, + { snippet: :private, embeddable: false, secret_token: nil } + ].each do |combination| + it 'returns true when snippet is public' do + snippet = create(:personal_snippet, combination[:snippet], secret_token: combination[:secret_token]) + + expect(snippet.embeddable?).to eq(combination[:embeddable]) + end + end + + context 'when visibility_level is Secret' do + let(:snippet) { create(:personal_snippet, :secret) } + + it 'returns true' do + expect(snippet.embeddable?).to be_truthy + end + + context 'when secret_token is not present' do + let(:snippet) { create(:personal_snippet, :public) } + + it 'returns false' do + snippet.update_column(:visibility_level, Gitlab::VisibilityLevel::SECRET) + + expect(snippet.embeddable?).to be_falsey + end + end + end + end +end diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index e87b4f41f4da4703910e56477c80de5a3a8bf0a0..e7449ab6da1c410302062a87b0057a56c726207f 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -9,5 +9,27 @@ describe "Validation" do it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_exclusion_of(:visibility_level).in_array([Gitlab::VisibilityLevel::SECRET]) } + end + + describe '#embeddable?' do + [ + { project: :public, snippet: :public, embeddable: true }, + { project: :internal, snippet: :public, embeddable: false }, + { project: :private, snippet: :public, embeddable: false }, + { project: :public, snippet: :internal, embeddable: false }, + { project: :internal, snippet: :internal, embeddable: false }, + { project: :private, snippet: :internal, embeddable: false }, + { project: :public, snippet: :private, embeddable: false }, + { project: :internal, snippet: :private, embeddable: false }, + { project: :private, snippet: :private, embeddable: false } + ].each do |combination| + it 'only returns true when both project and snippet are public' do + project = create(:project, combination[:project]) + snippet = create(:project_snippet, combination[:snippet], project: project) + + expect(snippet.embeddable?).to eq(combination[:embeddable]) + end + end end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index f4dcbfbc190299a7d7da16b4f794593a03ff6622..e4cc89318409f00fade104e5c7915cb88349bbe6 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -451,41 +451,4 @@ expect(blob.data).to eq(snippet.content) end end - - describe '#embeddable?' do - context 'project snippet' do - [ - { project: :public, snippet: :public, embeddable: true }, - { project: :internal, snippet: :public, embeddable: false }, - { project: :private, snippet: :public, embeddable: false }, - { project: :public, snippet: :internal, embeddable: false }, - { project: :internal, snippet: :internal, embeddable: false }, - { project: :private, snippet: :internal, embeddable: false }, - { project: :public, snippet: :private, embeddable: false }, - { project: :internal, snippet: :private, embeddable: false }, - { project: :private, snippet: :private, embeddable: false } - ].each do |combination| - it 'only returns true when both project and snippet are public' do - project = create(:project, combination[:project]) - snippet = create(:project_snippet, combination[:snippet], project: project) - - expect(snippet.embeddable?).to eq(combination[:embeddable]) - end - end - end - - context 'personal snippet' do - [ - { snippet: :public, embeddable: true }, - { snippet: :internal, embeddable: false }, - { snippet: :private, embeddable: false } - ].each do |combination| - it 'only returns true when snippet is public' do - snippet = create(:personal_snippet, combination[:snippet]) - - expect(snippet.embeddable?).to eq(combination[:embeddable]) - end - end - end - end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 36d2a0d7ea7ca2fd3e5bd6e8be432f4191bd5976..4ccce142ff8f9ab05a2f79132d398e6b29e4a08d 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -10,6 +10,7 @@ public_snippet = create(:personal_snippet, :public, author: user) private_snippet = create(:personal_snippet, :private, author: user) internal_snippet = create(:personal_snippet, :internal, author: user) + secret_snippet = create(:personal_snippet, :secret, author: user) get api("/snippets/", user) @@ -19,7 +20,8 @@ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( public_snippet.id, internal_snippet.id, - private_snippet.id) + private_snippet.id, + secret_snippet.id) expect(json_response.last).to have_key('web_url') expect(json_response.last).to have_key('raw_url') expect(json_response.last).to have_key('visibility') @@ -27,6 +29,7 @@ it 'hides private snippets from regular user' do create(:personal_snippet, :private) + create(:personal_snippet, :secret) get api("/snippets/", user) @@ -38,6 +41,7 @@ it 'returns 404 for non-authenticated' do create(:personal_snippet, :internal) + create(:personal_snippet, :secret) get api("/snippets/") @@ -63,6 +67,7 @@ let!(:public_snippet) { create(:personal_snippet, :public, author: user) } let!(:private_snippet) { create(:personal_snippet, :private, author: user) } let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) } + let!(:secret_snippet) { create(:personal_snippet, :secret, author: user) } let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) } let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) } let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) } @@ -134,10 +139,63 @@ end describe 'GET /snippets/:id' do - set(:admin) { create(:user, :admin) } - set(:author) { create(:user) } - set(:private_snippet) { create(:personal_snippet, :private, author: author) } - set(:internal_snippet) { create(:personal_snippet, :internal, author: author) } + let_it_be(:admin) { create(:user, :admin) } + let_it_be(:author) { create(:user) } + let_it_be(:public_snippet) { create(:personal_snippet, :public, author: author) } + let_it_be(:private_snippet) { create(:personal_snippet, :private, author: author) } + let_it_be(:internal_snippet) { create(:personal_snippet, :internal, author: author) } + let_it_be(:secret_snippet) { create(:personal_snippet, :internal, author: author) } + + context 'attributes' do + subject { get api("/snippets/#{snippet.id}", author) } + + before do + subject + end + + shared_examples 'returns basic attributes' do + it do + expect(json_response['title']).to eq(snippet.title) + expect(json_response['description']).to eq(snippet.description) + expect(json_response['file_name']).to eq(snippet.file_name) + expect(json_response['visibility']).to eq(snippet.visibility) + expect(json_response['raw_url']).to eq(snippet.raw_url) + expect(json_response['web_url']).to eq(snippet.web_url) + end + end + + shared_examples 'does not include secret_token attribute' do + it do + expect(json_response).not_to include('secret_token') + end + end + + context 'public snippet' do + let(:snippet) { public_snippet } + + it_behaves_like 'does not include secret_token attribute' + end + + context 'private snippet' do + let(:snippet) { private_snippet } + + it_behaves_like 'does not include secret_token attribute' + end + + context 'internal snippet' do + let(:snippet) { internal_snippet } + + it_behaves_like 'does not include secret_token attribute' + end + + context 'secret snippet' do + let(:snippet) { secret_snippet } + + it 'includes secret_token attribute' do + expect(json_response['secret_token']).to eq(snippet.secret_token) + end + end + end it 'requires authentication' do get api("/snippets/#{private_snippet.id}", nil) @@ -149,11 +207,6 @@ get api("/snippets/#{private_snippet.id}", author) expect(response).to have_gitlab_http_status(200) - - expect(json_response['title']).to eq(private_snippet.title) - expect(json_response['description']).to eq(private_snippet.description) - expect(json_response['file_name']).to eq(private_snippet.file_name) - expect(json_response['visibility']).to eq(private_snippet.visibility) end it 'shows private snippets to an admin' do