Commit ca7c6a8e authored by Douwe Maan's avatar Douwe Maan Committed by Bob Van Landuyt

Merge branch 'snippets-finder-visibility' into 'security'

Refactor snippets finder & dont return internal snippets for external users

See merge request !2094
parent 24c76c0e
class Dashboard::SnippetsController < Dashboard::ApplicationController class Dashboard::SnippetsController < Dashboard::ApplicationController
def index def index
@snippets = SnippetsFinder.new.execute( @snippets = SnippetsFinder.new(
current_user, current_user,
filter: :by_user, author: current_user,
user: current_user,
scope: params[:scope] scope: params[:scope]
) ).execute
@snippets = @snippets.page(params[:page]) @snippets = @snippets.page(params[:page])
end end
end end
class Explore::SnippetsController < Explore::ApplicationController class Explore::SnippetsController < Explore::ApplicationController
def index def index
@snippets = SnippetsFinder.new.execute(current_user, filter: :all) @snippets = SnippetsFinder.new(current_user).execute
@snippets = @snippets.page(params[:page]) @snippets = @snippets.page(params[:page])
end end
end end
...@@ -23,12 +23,11 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -23,12 +23,11 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html respond_to :html
def index def index
@snippets = SnippetsFinder.new.execute( @snippets = SnippetsFinder.new(
current_user, current_user,
filter: :by_project,
project: @project, project: @project,
scope: params[:scope] scope: params[:scope]
) ).execute
@snippets = @snippets.page(params[:page]) @snippets = @snippets.page(params[:page])
if @snippets.out_of_range? && @snippets.total_pages != 0 if @snippets.out_of_range? && @snippets.total_pages != 0
redirect_to namespace_project_snippets_path(page: @snippets.total_pages) redirect_to namespace_project_snippets_path(page: @snippets.total_pages)
......
...@@ -27,12 +27,8 @@ class SnippetsController < ApplicationController ...@@ -27,12 +27,8 @@ class SnippetsController < ApplicationController
return render_404 unless @user return render_404 unless @user
@snippets = SnippetsFinder.new.execute(current_user, { @snippets = SnippetsFinder.new(current_user, author: @user, scope: params[:scope])
filter: :by_user, .execute.page(params[:page])
user: @user,
scope: params[:scope]
})
.page(params[:page])
render 'index' render 'index'
else else
......
...@@ -128,12 +128,11 @@ class UsersController < ApplicationController ...@@ -128,12 +128,11 @@ class UsersController < ApplicationController
end end
def load_snippets def load_snippets
@snippets = SnippetsFinder.new.execute( @snippets = SnippetsFinder.new(
current_user, current_user,
filter: :by_user, author: user,
user: user,
scope: params[:scope] scope: params[:scope]
).page(params[:page]) ).execute.page(params[:page])
end end
def projects_for_current_user def projects_for_current_user
......
...@@ -67,7 +67,7 @@ class NotesFinder ...@@ -67,7 +67,7 @@ class NotesFinder
when "merge_request" when "merge_request"
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet" when "snippet", "project_snippet"
SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) SnippetsFinder.new(@current_user, project: @project).execute
when "personal_snippet" when "personal_snippet"
PersonalSnippet.all PersonalSnippet.all
else else
......
class SnippetsFinder class SnippetsFinder < UnionFinder
def execute(current_user, params = {}) attr_accessor :current_user, :params
filter = params[:filter]
user = params.fetch(:user, current_user) def initialize(current_user, params = {})
@current_user = current_user
case filter @params = params
when :all then
snippets(current_user).fresh
when :public then
Snippet.are_public.fresh
when :by_user then
by_user(current_user, user, params[:scope])
when :by_project
by_project(current_user, params[:project], params[:scope])
end end
def execute
items = init_collection
items = by_project(items)
items = by_author(items)
items = by_visibility(items)
items.fresh
end end
private private
def snippets(current_user) def init_collection
if current_user items = Snippet.all
Snippet.public_and_internal
else accessible(items)
# Not authenticated
#
# Return only:
# public snippets
Snippet.are_public
end
end end
def by_user(current_user, user, scope) def accessible(items)
snippets = user.snippets.fresh segments = []
segments << items.public_to_user(current_user)
segments << authorized_to_user(items) if current_user
if current_user find_union(segments, Snippet)
include_private = user == current_user
by_scope(snippets, scope, include_private)
else
snippets.are_public
end end
def authorized_to_user(items)
items.where(
'author_id = :author_id
OR project_id IN (:project_ids)',
author_id: current_user.id,
project_ids: current_user.authorized_projects.select(:id))
end end
def by_project(current_user, project, scope) def by_visibility(items)
snippets = project.snippets.fresh visibility = params[:visibility] || visibility_from_scope
if current_user return items unless visibility
include_private = project.team.member?(current_user) || current_user.admin_or_auditor?
by_scope(snippets, scope, include_private) items.where(visibility_level: visibility)
else
snippets.are_public
end end
def by_author(items)
return items unless params[:author]
items.where(author_id: params[:author].id)
end
def by_project(items)
return items unless params[:project]
items.where(project_id: params[:project].id)
end end
def by_scope(snippets, scope = nil, include_private = false) def visibility_from_scope
case scope.to_s case params[:scope].to_s
when 'are_private' when 'are_private'
include_private ? snippets.are_private : Snippet.none Snippet::PRIVATE
when 'are_internal' when 'are_internal'
snippets.are_internal Snippet::INTERNAL
when 'are_public' when 'are_public'
snippets.are_public Snippet::PUBLIC
else else
include_private ? snippets : snippets.public_and_internal nil
end end
end end
end end
...@@ -153,18 +153,5 @@ class Snippet < ActiveRecord::Base ...@@ -153,18 +153,5 @@ class Snippet < ActiveRecord::Base
where(table[:content].matches(pattern)) where(table[:content].matches(pattern))
end end
def accessible_to(user)
return are_public unless user.present?
return all if user.admin?
where(
'visibility_level IN (:visibility_levels)
OR author_id = :author_id
OR project_id IN (:project_ids)',
visibility_levels: [Snippet::PUBLIC, Snippet::INTERNAL],
author_id: user.id,
project_ids: user.authorized_projects.select(:id))
end
end end
end end
...@@ -17,7 +17,7 @@ class ProjectSnippetPolicy < BasePolicy ...@@ -17,7 +17,7 @@ class ProjectSnippetPolicy < BasePolicy
can! :read_project_snippet can! :read_project_snippet
end end
if @subject.private? && @subject.project.team.member?(@user) if @subject.project.team.member?(@user)
can! :read_project_snippet can! :read_project_snippet
end end
end end
......
module Search module Search
class SnippetService class SnippetService
include Gitlab::CurrentSettings
attr_accessor :current_user, :params attr_accessor :current_user, :params
def initialize(user, params) def initialize(user, params)
...@@ -8,14 +7,10 @@ module Search ...@@ -8,14 +7,10 @@ module Search
end end
def execute def execute
if current_application_settings.elasticsearch_search? snippets = SnippetsFinder.new(current_user).execute
Gitlab::Elastic::SnippetSearchResults.new(current_user,
params[:search])
else
snippets = Snippet.accessible_to(current_user)
Gitlab::SnippetSearchResults.new(snippets, params[:search]) Gitlab::SnippetSearchResults.new(snippets, params[:search])
end end
end
def scope def scope
@scope ||= %w[snippet_titles].delete(params[:scope]) { 'snippet_blobs' } @scope ||= %w[snippet_titles].delete(params[:scope]) { 'snippet_blobs' }
......
---
title: Refactor snippets finder & dont return internal snippets for external users
merge_request:
author:
...@@ -17,8 +17,7 @@ module API ...@@ -17,8 +17,7 @@ module API
end end
def snippets_for_current_user def snippets_for_current_user
finder_params = { filter: :by_project, project: user_project } SnippetsFinder.new(current_user, project: user_project).execute
SnippetsFinder.new.execute(current_user, finder_params)
end end
end end
......
...@@ -8,11 +8,11 @@ module API ...@@ -8,11 +8,11 @@ module API
resource :snippets do resource :snippets do
helpers do helpers do
def snippets_for_current_user def snippets_for_current_user
SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user) SnippetsFinder.new(current_user, author: current_user).execute
end end
def public_snippets def public_snippets
SnippetsFinder.new.execute(current_user, filter: :public) SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
end end
end end
......
...@@ -18,8 +18,7 @@ module API ...@@ -18,8 +18,7 @@ module API
end end
def snippets_for_current_user def snippets_for_current_user
finder_params = { filter: :by_project, project: user_project } SnippetsFinder.new(current_user, project: user_project).execute
SnippetsFinder.new.execute(current_user, finder_params)
end end
end end
......
...@@ -8,11 +8,11 @@ module API ...@@ -8,11 +8,11 @@ module API
resource :snippets do resource :snippets do
helpers do helpers do
def snippets_for_current_user def snippets_for_current_user
SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user) SnippetsFinder.new(current_user, author: current_user).execute
end end
def public_snippets def public_snippets
SnippetsFinder.new.execute(current_user, filter: :public) SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
end end
end end
......
...@@ -3,6 +3,34 @@ require 'spec_helper' ...@@ -3,6 +3,34 @@ require 'spec_helper'
describe SnippetsController do describe SnippetsController do
let(:user) { create(:user) } let(:user) { create(:user) }
describe 'GET #index' do
let(:user) { create(:user) }
context 'when username parameter is present' do
it 'renders snippets of a user when username is present' do
get :index, username: user.username
expect(response).to render_template(:index)
end
end
context 'when username parameter is not present' do
it 'redirects to explore snippets page when user is not logged in' do
get :index
expect(response).to redirect_to(explore_snippets_path)
end
it 'redirects to snippets dashboard page when user is logged in' do
sign_in(user)
get :index
expect(response).to redirect_to(dashboard_snippets_path)
end
end
end
describe 'GET #new' do describe 'GET #new' do
context 'when signed in' do context 'when signed in' do
before do before do
......
...@@ -12,4 +12,51 @@ describe 'Dashboard snippets', feature: true do ...@@ -12,4 +12,51 @@ describe 'Dashboard snippets', feature: true do
it_behaves_like 'paginated snippets' it_behaves_like 'paginated snippets'
end end
context 'filtering by visibility' do
let(:user) { create(:user) }
let!(:snippets) do
[
create(:personal_snippet, :public, author: user),
create(:personal_snippet, :internal, author: user),
create(:personal_snippet, :private, author: user),
create(:personal_snippet, :public)
]
end
before do
login_as(user)
visit dashboard_snippets_path
end
it 'contains all snippets of logged user' do
expect(page).to have_selector('.snippet-row', count: 3)
expect(page).to have_content(snippets[0].title)
expect(page).to have_content(snippets[1].title)
expect(page).to have_content(snippets[2].title)
end
it 'contains all private snippets of logged user when clicking on private' do
click_link('Private')
expect(page).to have_selector('.snippet-row', count: 1)
expect(page).to have_content(snippets[2].title)
end
it 'contains all internal snippets of logged user when clicking on internal' do
click_link('Internal')
expect(page).to have_selector('.snippet-row', count: 1)
expect(page).to have_content(snippets[1].title)
end
it 'contains all public snippets of logged user when clicking on public' do
click_link('Public')
expect(page).to have_selector('.snippet-row', count: 1)
expect(page).to have_content(snippets[0].title)
end
end
end end
...@@ -4,11 +4,27 @@ describe 'Project snippets', feature: true do ...@@ -4,11 +4,27 @@ describe 'Project snippets', feature: true do
context 'when the project has snippets' do context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) } let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
let!(:other_snippet) { create(:project_snippet) }
context 'pagination' do
before do before do
allow(Snippet).to receive(:default_per_page).and_return(1) allow(Snippet).to receive(:default_per_page).and_return(1)
visit namespace_project_snippets_path(project.namespace, project) visit namespace_project_snippets_path(project.namespace, project)
end end
it_behaves_like 'paginated snippets' it_behaves_like 'paginated snippets'
end end
context 'list content' do
it 'contains all project snippets' do
visit namespace_project_snippets_path(project.namespace, project)
expect(page).to have_selector('.snippet-row', count: 2)
expect(page).to have_content(snippets[0].title)
expect(page).to have_content(snippets[1].title)
end
end
end
end end
require 'rails_helper' require 'rails_helper'
feature 'Explore Snippets', feature: true do feature 'Explore Snippets', feature: true do
scenario 'User should see snippets that are not private' do let!(:public_snippet) { create(:personal_snippet, :public) }
public_snippet = create(:personal_snippet, :public) let!(:internal_snippet) { create(:personal_snippet, :internal) }
internal_snippet = create(:personal_snippet, :internal) let!(:private_snippet) { create(:personal_snippet, :private) }
private_snippet = create(:personal_snippet, :private)
scenario 'User should see snippets that are not private' do
login_as create(:user) login_as create(:user)
visit explore_snippets_path visit explore_snippets_path
...@@ -13,4 +13,21 @@ feature 'Explore Snippets', feature: true do ...@@ -13,4 +13,21 @@ feature 'Explore Snippets', feature: true do
expect(page).to have_content(internal_snippet.title) expect(page).to have_content(internal_snippet.title)
expect(page).not_to have_content(private_snippet.title) expect(page).not_to have_content(private_snippet.title)
end end
scenario 'External user should see only public snippets' do
login_as create(:user, :external)
visit explore_snippets_path
expect(page).to have_content(public_snippet.title)
expect(page).not_to have_content(internal_snippet.title)
expect(page).not_to have_content(private_snippet.title)
end
scenario 'Not authenticated user should see only public snippets' do
visit explore_snippets_path
expect(page).to have_content(public_snippet.title)
expect(page).not_to have_content(internal_snippet.title)
expect(page).not_to have_content(private_snippet.title)
end
end end
...@@ -3,7 +3,10 @@ require 'spec_helper' ...@@ -3,7 +3,10 @@ require 'spec_helper'
describe 'Snippets tab on a user profile', feature: true, js: true do describe 'Snippets tab on a user profile', feature: true, js: true do
context 'when the user has snippets' do context 'when the user has snippets' do
let(:user) { create(:user) } let(:user) { create(:user) }
context 'pagination' do
let!(:snippets) { create_list(:snippet, 2, :public, author: user) } let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
before do before do
allow(Snippet).to receive(:default_per_page).and_return(1) allow(Snippet).to receive(:default_per_page).and_return(1)
visit user_path(user) visit user_path(user)
...@@ -13,4 +16,33 @@ describe 'Snippets tab on a user profile', feature: true, js: true do ...@@ -13,4 +16,33 @@ describe 'Snippets tab on a user profile', feature: true, js: true do
it_behaves_like 'paginated snippets', remote: true it_behaves_like 'paginated snippets', remote: true
end end
context 'list content' do
let!(:public_snippet) { create(:snippet, :public, author: user) }
let!(:internal_snippet) { create(:snippet, :internal, author: user) }
let!(:private_snippet) { create(:snippet, :private, author: user) }
let!(:other_snippet) { create(:snippet, :public) }
it 'contains only internal and public snippets of a user when a user is logged in' do
login_as(:user)
visit user_path(user)
page.within('.user-profile-nav') { click_link 'Snippets' }
wait_for_ajax
expect(page).to have_selector('.snippet-row', count: 2)
expect(page).to have_content(public_snippet.title)
expect(page).to have_content(internal_snippet.title)
end
it 'contains only public snippets of a user when a user is not logged in' do
visit user_path(user)
page.within('.user-profile-nav') { click_link 'Snippets' }
wait_for_ajax
expect(page).to have_selector('.snippet-row', count: 1)
expect(page).to have_content(public_snippet.title)
end
end
end
end end
This diff is collapsed.
...@@ -131,46 +131,6 @@ describe Snippet, models: true do ...@@ -131,46 +131,6 @@ describe Snippet, models: true do
end end
end end
describe '.accessible_to' do
let(:author) { create(:author) }
let(:project) { create(:empty_project) }
let!(:public_snippet) { create(:snippet, :public) }
let!(:internal_snippet) { create(:snippet, :internal) }
let!(:private_snippet) { create(:snippet, :private, author: author) }
let!(:project_public_snippet) { create(:snippet, :public, project: project) }
let!(:project_internal_snippet) { create(:snippet, :internal, project: project) }
let!(:project_private_snippet) { create(:snippet, :private, project: project) }
it 'returns only public snippets when user is blank' do
expect(described_class.accessible_to(nil)).to match_array [public_snippet, project_public_snippet]
end
it 'returns only public, and internal snippets for regular users' do
user = create(:user)
expect(described_class.accessible_to(user)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet]
end
it 'returns public, internal snippets and project private snippets for project members' do
member = create(:user)
project.team << [member, :developer]
expect(described_class.accessible_to(member)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
end
it 'returns private snippets where the user is the author' do
expect(described_class.accessible_to(author)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet]
end
it 'returns all snippets when for admins' do
admin = create(:admin)
expect(described_class.accessible_to(admin)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
end
end
describe '#participants' do describe '#participants' do
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
let(:snippet) { create(:snippet, content: 'foo', project: project) } let(:snippet) { create(:snippet, content: 'foo', project: project) }
......
require 'spec_helper' require 'spec_helper'
describe ProjectSnippetPolicy, models: true do describe ProjectSnippetPolicy, models: true do
let(:current_user) { create(:user) } let(:regular_user) { create(:user) }
let(:external_user) { create(:user, :external) }
let(:project) { create(:empty_project) }
let(:author_permissions) do let(:author_permissions) do
[ [
...@@ -10,13 +12,15 @@ describe ProjectSnippetPolicy, models: true do ...@@ -10,13 +12,15 @@ describe ProjectSnippetPolicy, models: true do
] ]
end end
subject { described_class.abilities(current_user, project_snippet).to_set } def abilities(user, snippet_visibility)
snippet = create(:project_snippet, snippet_visibility, project: project)
context 'public snippet' do described_class.abilities(user, snippet).to_set
let(:project_snippet) { create(:project_snippet, :public) } end
context 'public snippet' do
context 'no user' do context 'no user' do
let(:current_user) { nil } subject { abilities(nil, :public) }
it do it do
is_expected.to include(:read_project_snippet) is_expected.to include(:read_project_snippet)
...@@ -25,6 +29,17 @@ describe ProjectSnippetPolicy, models: true do ...@@ -25,6 +29,17 @@ describe ProjectSnippetPolicy, models: true do
end end
context 'regular user' do context 'regular user' do
subject { abilities(regular_user, :public) }
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'external user' do
subject { abilities(external_user, :public) }
it do it do
is_expected.to include(:read_project_snippet) is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions) is_expected.not_to include(*author_permissions)
...@@ -33,10 +48,8 @@ describe ProjectSnippetPolicy, models: true do ...@@ -33,10 +48,8 @@ describe ProjectSnippetPolicy, models: true do
end end
context 'internal snippet' do context 'internal snippet' do
let(:project_snippet) { create(:project_snippet, :internal) }
context 'no user' do context 'no user' do
let(:current_user) { nil } subject { abilities(nil, :internal) }
it do it do
is_expected.not_to include(:read_project_snippet) is_expected.not_to include(:read_project_snippet)
...@@ -45,6 +58,28 @@ describe ProjectSnippetPolicy, models: true do ...@@ -45,6 +58,28 @@ describe ProjectSnippetPolicy, models: true do
end end
context 'regular user' do context 'regular user' do
subject { abilities(regular_user, :internal) }
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'external user' do
subject { abilities(external_user, :internal) }
it do
is_expected.not_to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'project team member external user' do
subject { abilities(external_user, :internal) }
before { project.team << [external_user, :developer] }
it do it do
is_expected.to include(:read_project_snippet) is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions) is_expected.not_to include(*author_permissions)
...@@ -53,6 +88,7 @@ describe ProjectSnippetPolicy, models: true do ...@@ -53,6 +88,7 @@ describe ProjectSnippetPolicy, models: true do
context 'external user' do context 'external user' do
let(:current_user) { create(:user, :external) } let(:current_user) { create(:user, :external) }
subject { abilities(current_user, :private) }
it do it do
is_expected.not_to include(:read_project_snippet) is_expected.not_to include(:read_project_snippet)
...@@ -62,10 +98,8 @@ describe ProjectSnippetPolicy, models: true do ...@@ -62,10 +98,8 @@ describe ProjectSnippetPolicy, models: true do
end end
context 'private snippet' do context 'private snippet' do
let(:project_snippet) { create(:project_snippet, :private) }
context 'no user' do context 'no user' do
let(:current_user) { nil } subject { abilities(nil, :private) }
it do it do
is_expected.not_to include(:read_project_snippet) is_expected.not_to include(:read_project_snippet)
...@@ -74,6 +108,8 @@ describe ProjectSnippetPolicy, models: true do ...@@ -74,6 +108,8 @@ describe ProjectSnippetPolicy, models: true do
end end
context 'regular user' do context 'regular user' do
subject { abilities(regular_user, :private) }
it do it do
is_expected.not_to include(:read_project_snippet) is_expected.not_to include(:read_project_snippet)
is_expected.not_to include(*author_permissions) is_expected.not_to include(*author_permissions)
...@@ -81,7 +117,9 @@ describe ProjectSnippetPolicy, models: true do ...@@ -81,7 +117,9 @@ describe ProjectSnippetPolicy, models: true do
end end
context 'snippet author' do context 'snippet author' do
let(:project_snippet) { create(:project_snippet, :private, author: current_user) } let(:snippet) { create(:project_snippet, :private, author: regular_user) }
subject { described_class.abilities(regular_user, snippet).to_set }
it do it do
is_expected.to include(:read_project_snippet) is_expected.to include(:read_project_snippet)
...@@ -89,8 +127,21 @@ describe ProjectSnippetPolicy, models: true do ...@@ -89,8 +127,21 @@ describe ProjectSnippetPolicy, models: true do
end end
end end
context 'project team member' do context 'project team member normal user' do
before { project_snippet.project.team << [current_user, :developer] } subject { abilities(regular_user, :private) }
before { project.team << [regular_user, :developer] }
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
end
end
context 'project team member external user' do
subject { abilities(external_user, :private) }
before { project.team << [external_user, :developer] }
it do it do
is_expected.to include(:read_project_snippet) is_expected.to include(:read_project_snippet)
...@@ -100,6 +151,7 @@ describe ProjectSnippetPolicy, models: true do ...@@ -100,6 +151,7 @@ describe ProjectSnippetPolicy, models: true do
context 'auditor user' do context 'auditor user' do
let(:current_user) { create(:user, :auditor) } let(:current_user) { create(:user, :auditor) }
subject { abilities(current_user, :private) }
it do it do
is_expected.to include(:read_project_snippet) is_expected.to include(:read_project_snippet)
...@@ -108,7 +160,7 @@ describe ProjectSnippetPolicy, models: true do ...@@ -108,7 +160,7 @@ describe ProjectSnippetPolicy, models: true do
end end
context 'admin user' do context 'admin user' do
let(:current_user) { create(:admin) } subject { abilities(create(:admin), :private) }
it do it do
is_expected.to include(:read_project_snippet) is_expected.to include(:read_project_snippet)
......
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