Commit bcdcff74 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 5277f8e6
...@@ -249,7 +249,7 @@ ...@@ -249,7 +249,7 @@
} }
.filtered-search-input-dropdown-menu { .filtered-search-input-dropdown-menu {
max-height: $dropdown-max-height; max-height: $dropdown-max-height-lg;
max-width: 280px; max-width: 280px;
overflow: auto; overflow: auto;
......
...@@ -23,6 +23,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -23,6 +23,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action do before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, @project.group) push_frontend_feature_flag(:vue_issuable_sidebar, @project.group)
push_frontend_feature_flag(:release_search_filter, @project)
end end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
......
...@@ -1454,7 +1454,7 @@ class User < ApplicationRecord ...@@ -1454,7 +1454,7 @@ class User < ApplicationRecord
# Does the user have access to all private groups & projects? # Does the user have access to all private groups & projects?
# Overridden in EE to also check auditor? # Overridden in EE to also check auditor?
def full_private_access? def full_private_access?
admin? can?(:read_all_resources)
end end
def update_two_factor_requirement def update_two_factor_requirement
......
...@@ -21,10 +21,6 @@ class BasePolicy < DeclarativePolicy::Base ...@@ -21,10 +21,6 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:deactivated) { @user&.deactivated? } condition(:deactivated) { @user&.deactivated? }
desc "User has access to all private groups & projects"
with_options scope: :user, score: 0
condition(:full_private_access) { @user&.full_private_access? }
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:external_user) { @user.nil? || @user.external? } condition(:external_user) { @user.nil? || @user.external? }
...@@ -40,10 +36,12 @@ class BasePolicy < DeclarativePolicy::Base ...@@ -40,10 +36,12 @@ class BasePolicy < DeclarativePolicy::Base
::Gitlab::ExternalAuthorization.perform_check? ::Gitlab::ExternalAuthorization.perform_check?
end end
rule { external_authorization_enabled & ~full_private_access }.policy do rule { external_authorization_enabled & ~can?(:read_all_resources) }.policy do
prevent :read_cross_project prevent :read_cross_project
end end
rule { admin }.enable :read_all_resources
rule { default }.enable :read_cross_project rule { default }.enable :read_cross_project
end end
......
...@@ -30,5 +30,5 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -30,5 +30,5 @@ class PersonalSnippetPolicy < BasePolicy
rule { can?(:create_note) }.enable :award_emoji rule { can?(:create_note) }.enable :award_emoji
rule { full_private_access }.enable :read_personal_snippet rule { can?(:read_all_resources) }.enable :read_personal_snippet
end end
...@@ -28,7 +28,7 @@ class ProjectSnippetPolicy < BasePolicy ...@@ -28,7 +28,7 @@ class ProjectSnippetPolicy < BasePolicy
all?(private_snippet | (internal_snippet & external_user), all?(private_snippet | (internal_snippet & external_user),
~project.guest, ~project.guest,
~is_author, ~is_author,
~full_private_access) ~can?(:read_all_resources))
end.prevent :read_project_snippet end.prevent :read_project_snippet
rule { internal_snippet & ~is_author & ~admin }.policy do rule { internal_snippet & ~is_author & ~admin }.policy do
......
---
title: Hide projects without access to admin user when admin mode is disabled
merge_request: 18530
author: Diego Louzán
type: changed
---
title: Add "release" filter to merge request search page
merge_request: 19315
author:
type: added
# frozen_string_literal: true
class ReplaceIndexOnMetricsMergedAt < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :merge_request_metrics, :merged_at
remove_concurrent_index :merge_request_metrics, [:merged_at, :id]
end
def down
add_concurrent_index :merge_request_metrics, [:merged_at, :id]
remove_concurrent_index :merge_request_metrics, :merged_at
end
end
...@@ -6,7 +6,7 @@ class ScheduleProductivityAnalyticsBackfill < ActiveRecord::Migration[5.2] ...@@ -6,7 +6,7 @@ class ScheduleProductivityAnalyticsBackfill < ActiveRecord::Migration[5.2]
DOWNTIME = false DOWNTIME = false
def up def up
# no-op since the scheduling times out on GitLab.com # no-op since the migration was removed
end end
def down def down
......
...@@ -2290,7 +2290,7 @@ ActiveRecord::Schema.define(version: 2019_11_05_094625) do ...@@ -2290,7 +2290,7 @@ ActiveRecord::Schema.define(version: 2019_11_05_094625) do
t.index ["latest_closed_by_id"], name: "index_merge_request_metrics_on_latest_closed_by_id" t.index ["latest_closed_by_id"], name: "index_merge_request_metrics_on_latest_closed_by_id"
t.index ["merge_request_id", "merged_at"], name: "index_merge_request_metrics_on_merge_request_id_and_merged_at", where: "(merged_at IS NOT NULL)" t.index ["merge_request_id", "merged_at"], name: "index_merge_request_metrics_on_merge_request_id_and_merged_at", where: "(merged_at IS NOT NULL)"
t.index ["merge_request_id"], name: "index_merge_request_metrics" t.index ["merge_request_id"], name: "index_merge_request_metrics"
t.index ["merged_at", "id"], name: "index_merge_request_metrics_on_merged_at_and_id" t.index ["merged_at"], name: "index_merge_request_metrics_on_merged_at"
t.index ["merged_by_id"], name: "index_merge_request_metrics_on_merged_by_id" t.index ["merged_by_id"], name: "index_merge_request_metrics_on_merged_by_id"
t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id" t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id"
end end
......
...@@ -89,6 +89,10 @@ your GitLab installation has three repository storages: `default`, ...@@ -89,6 +89,10 @@ your GitLab installation has three repository storages: `default`,
`storage1` and `storage2`. You can use as little as just one server with one `storage1` and `storage2`. You can use as little as just one server with one
repository storage if desired. repository storage if desired.
Note: **Note:** The token referred to throughout the Gitaly documentation is
just an arbitrary password selected by the administrator. It is unrelated to
tokens created for the GitLab API or other similar web API tokens.
### 1. Installation ### 1. Installation
First install Gitaly on each Gitaly server using either First install Gitaly on each Gitaly server using either
......
...@@ -245,47 +245,29 @@ end ...@@ -245,47 +245,29 @@ end
#### Use self-descriptive wrapper methods #### Use self-descriptive wrapper methods
When it's not possible/logical to modify the implementation of a When it's not possible/logical to modify the implementation of a method, then
method. Wrap it in a self-descriptive method and use that method. wrap it in a self-descriptive method and use that method.
For example, in CE only an `admin` is allowed to access all private For example, in GitLab-FOSS, the only user created by the system is `User.ghost`
projects/groups, but in EE also an `auditor` has full private but in EE there are several types of bot-users that aren't really users. It would
access. It would be incorrect to override the implementation of be incorrect to override the implementation of `User#ghost?`, so instead we add
`User#admin?`, so instead add a method `full_private_access?` to a method `#internal?` to `app/models/user.rb`. The implementation will be:
`app/models/users.rb`. The implementation in CE will be:
```ruby ```ruby
def full_private_access? def internal?
admin? ghost?
end end
``` ```
In EE, the implementation `ee/app/models/ee/users.rb` would be: In EE, the implementation `ee/app/models/ee/users.rb` would be:
```ruby ```ruby
override :full_private_access? override :internal?
def full_private_access? def internal?
super || auditor? super || bot?
end end
``` ```
In `lib/gitlab/visibility_level.rb` this method is used to return the
allowed visibility levels:
```ruby
def levels_for_user(user = nil)
if user.full_private_access?
[PRIVATE, INTERNAL, PUBLIC]
elsif # ...
end
```
See [CE MR][ce-mr-full-private] and [EE MR][ee-mr-full-private] for
full implementation details.
[ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12373
[ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab/merge_requests/2199
### Code in `config/routes` ### Code in `config/routes`
When we add `draw :admin` in `config/routes.rb`, the application will try to When we add `draw :admin` in `config/routes.rb`, the application will try to
......
...@@ -184,3 +184,83 @@ class Commit ...@@ -184,3 +184,83 @@ class Commit
request_cache(:author) { author_email } request_cache(:author) { author_email }
end end
``` ```
## `ReactiveCaching`
The `ReactiveCaching` concern is used to fetch some data in the background and
store it in the Rails cache, keeping it up-to-date for as long as it is being
requested. If the data hasn't been requested for `reactive_cache_lifetime`,
it will stop being refreshed, and then be removed.
Example of use:
```ruby
class Foo < ApplicationRecord
include ReactiveCaching
after_save :clear_reactive_cache!
def calculate_reactive_cache
# Expensive operation here. The return value of this method is cached
end
def result
with_reactive_cache do |data|
# ...
end
end
end
```
In this example, the first time `#result` is called, it will return `nil`.
However, it will enqueue a background worker to call `#calculate_reactive_cache`
and set an initial cache lifetime of ten minutes.
The background worker needs to find or generate the object on which
`with_reactive_cache` was called.
The default behaviour can be overridden by defining a custom
`reactive_cache_worker_finder`.
Otherwise, the background worker will use the class name and primary key to get
the object using the ActiveRecord `find_by` method.
```ruby
class Bar
include ReactiveCaching
self.reactive_cache_key = ->() { ["bar", "thing"] }
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
def self.from_cache(var1, var2)
# This method will be called by the background worker with "bar1" and
# "bar2" as arguments.
new(var1, var2)
end
def initialize(var1, var2)
# ...
end
def calculate_reactive_cache
# Expensive operation here. The return value of this method is cached
end
def result
with_reactive_cache("bar1", "bar2") do |data|
# ...
end
end
end
```
Each time the background job completes, it stores the return value of
`#calculate_reactive_cache`. It is also re-enqueued to run again after
`reactive_cache_refresh_interval`, therefore, it will keep the stored value up to date.
Calculations are never run concurrently.
Calling `#result` while a value is cached will call the block given to
`#with_reactive_cache`, yielding the cached value. It will also extend the
lifetime by the `reactive_cache_lifetime` value.
Once the lifetime has expired, no more background jobs will be enqueued and
calling `#result` will again return `nil` - starting the process all over
again.
...@@ -176,7 +176,7 @@ Here's a list of what you can't do with subgroups: ...@@ -176,7 +176,7 @@ Here's a list of what you can't do with subgroups:
- [GitLab Pages](../../project/pages/index.md) supports projects hosted under - [GitLab Pages](../../project/pages/index.md) supports projects hosted under
a subgroup, but not subgroup websites. a subgroup, but not subgroup websites.
That means that only the highest-level group supports That means that only the highest-level group supports
[group websites](../../project/pages/getting_started_part_one.md#gitlab-pages-domain-names), [group websites](../../project/pages/getting_started_part_one.md#gitlab-pages-default-domain-names),
although you can have project websites under a subgroup. although you can have project websites under a subgroup.
- It is not possible to share a project with a group that's an ancestor of - It is not possible to share a project with a group that's an ancestor of
the group the project is in. That means you can only share as you walk down the group the project is in. That means you can only share as you walk down
......
...@@ -37,12 +37,12 @@ Example request: ...@@ -37,12 +37,12 @@ Example request:
```sh ```sh
curl --request POST \ curl --request POST \
--data '{"title": "Incident title"}' \ --data '{"title": "Incident title"}' \
--header "Authorization: Bearer <autorization_key>" \ --header "Authorization: Bearer <authorization_key>" \
--header "Content-Type: application/json" \ --header "Content-Type: application/json" \
<url> <url>
``` ```
The `<autorization_key>` and `<url>` values can be found when [setting up generic alerts](#setting-up-generic-alerts). The `<authorization_key>` and `<url>` values can be found when [setting up generic alerts](#setting-up-generic-alerts).
Example payload: Example payload:
......
...@@ -14,7 +14,7 @@ To do so, follow the steps below. ...@@ -14,7 +14,7 @@ To do so, follow the steps below.
1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, 1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**,
click **New project**, and name it according to the click **New project**, and name it according to the
[Pages domain names](../getting_started_part_one.md#gitlab-pages-domain-names). [Pages domain names](../getting_started_part_one.md#gitlab-pages-default-domain-names).
1. Clone it to your local computer, add your website 1. Clone it to your local computer, add your website
files to your project, add, commit and push to GitLab. files to your project, add, commit and push to GitLab.
Alternativelly, you can run `git init` in your local directory, Alternativelly, you can run `git init` in your local directory,
......
...@@ -8,7 +8,7 @@ type: concepts, reference ...@@ -8,7 +8,7 @@ type: concepts, reference
On this document, learn how to name your project for GitLab Pages On this document, learn how to name your project for GitLab Pages
according to your intended website's URL. according to your intended website's URL.
## GitLab Pages domain names ## GitLab Pages default domain names
>**Note:** >**Note:**
If you use your own GitLab instance to deploy your If you use your own GitLab instance to deploy your
...@@ -80,7 +80,7 @@ To understand Pages domains clearly, read the examples below. ...@@ -80,7 +80,7 @@ To understand Pages domains clearly, read the examples below.
- On your GitLab instance, replace `gitlab.io` above with your - On your GitLab instance, replace `gitlab.io` above with your
Pages server domain. Ask your sysadmin for this information. Pages server domain. Ask your sysadmin for this information.
## URLs and Baseurls ## URLs and baseurls
Every Static Site Generator (SSG) default configuration expects Every Static Site Generator (SSG) default configuration expects
to find your website under a (sub)domain (`example.com`), not to find your website under a (sub)domain (`example.com`), not
...@@ -108,7 +108,7 @@ example we've just mentioned, you'd have to change Jekyll's `_config.yml` to: ...@@ -108,7 +108,7 @@ example we've just mentioned, you'd have to change Jekyll's `_config.yml` to:
baseurl: "" baseurl: ""
``` ```
## Custom Domains ## Custom domains
GitLab Pages supports custom domains and subdomains, served under HTTP or HTTPS. GitLab Pages supports custom domains and subdomains, served under HTTP or HTTPS.
See [GitLab Pages custom domains and SSL/TLS Certificates](custom_domains_ssl_tls_certification/index.md) for more information. See [GitLab Pages custom domains and SSL/TLS Certificates](custom_domains_ssl_tls_certification/index.md) for more information.
...@@ -83,7 +83,7 @@ to build your site and publish it to the GitLab Pages server. The sequence of ...@@ -83,7 +83,7 @@ to build your site and publish it to the GitLab Pages server. The sequence of
scripts that GitLab CI/CD runs to accomplish this task is created from a file named scripts that GitLab CI/CD runs to accomplish this task is created from a file named
`.gitlab-ci.yml`, which you can [create and modify](getting_started_part_four.md) at will. A specific `job` called `pages` in the configuration file will make GitLab aware that you are deploying a GitLab Pages website. `.gitlab-ci.yml`, which you can [create and modify](getting_started_part_four.md) at will. A specific `job` called `pages` in the configuration file will make GitLab aware that you are deploying a GitLab Pages website.
You can either use GitLab's [default domain for GitLab Pages websites](getting_started_part_one.md#gitlab-pages-domain-names), You can either use GitLab's [default domain for GitLab Pages websites](getting_started_part_one.md#gitlab-pages-default-domain-names),
`*.gitlab.io`, or your own domain (`example.com`). In that case, you'll `*.gitlab.io`, or your own domain (`example.com`). In that case, you'll
need admin access to your domain's registrar (or control panel) to set it up with Pages. need admin access to your domain's registrar (or control panel) to set it up with Pages.
......
...@@ -8,10 +8,15 @@ ...@@ -8,10 +8,15 @@
# https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong # https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong
# #
# It currently supports sorting on two columns, but the last column must # It currently supports sorting on two columns, but the last column must
# be the primary key. For example # be the primary key. If it's not already included, an order on the
# primary key will be added automatically, like `order(id: :desc)`
# #
# Issue.order(created_at: :asc).order(:id) # Issue.order(created_at: :asc).order(:id)
# Issue.order(due_date: :asc).order(:id) # Issue.order(due_date: :asc)
#
# You can also use `Gitlab::Database.nulls_last_order`:
#
# Issue.reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC'))
# #
# It will tolerate non-attribute ordering, but only attributes determine the cursor. # It will tolerate non-attribute ordering, but only attributes determine the cursor.
# For example, this is legitimate: # For example, this is legitimate:
......
...@@ -5,12 +5,15 @@ module Gitlab ...@@ -5,12 +5,15 @@ module Gitlab
module Connections module Connections
module Keyset module Keyset
class OrderInfo class OrderInfo
def initialize(order_value) attr_reader :attribute_name, :sort_direction
@order_value = order_value
end
def attribute_name def initialize(order_value)
order_value.expr.name if order_value.is_a?(String)
@attribute_name, @sort_direction = extract_nulls_last_order(order_value)
else
@attribute_name = order_value.expr.name
@sort_direction = order_value.direction
end
end end
def operator_for(before_or_after) def operator_for(before_or_after)
...@@ -22,10 +25,10 @@ module Gitlab ...@@ -22,10 +25,10 @@ module Gitlab
end end
end end
# Only allow specific node types. For example ignore String nodes # Only allow specific node types
def self.build_order_list(relation) def self.build_order_list(relation)
order_list = relation.order_values.select do |value| order_list = relation.order_values.select do |value|
value.is_a?(Arel::Nodes::Ascending) || value.is_a?(Arel::Nodes::Descending) supported_order_value?(value)
end end
order_list.map { |info| OrderInfo.new(info) } order_list.map { |info| OrderInfo.new(info) }
...@@ -52,12 +55,21 @@ module Gitlab ...@@ -52,12 +55,21 @@ module Gitlab
end end
end end
def self.supported_order_value?(order_value)
return true if order_value.is_a?(Arel::Nodes::Ascending) || order_value.is_a?(Arel::Nodes::Descending)
return false unless order_value.is_a?(String)
tokens = order_value.downcase.split
tokens.last(2) == %w(nulls last) && tokens.count == 4
end
private private
attr_reader :order_value def extract_nulls_last_order(order_value)
tokens = order_value.downcase.split
def sort_direction [tokens.first, (tokens[1] == 'asc' ? :asc : :desc)]
order_value.direction
end end
end end
end end
......
...@@ -6271,6 +6271,12 @@ msgstr "" ...@@ -6271,6 +6271,12 @@ msgstr ""
msgid "Environment:" msgid "Environment:"
msgstr "" msgstr ""
msgid "EnvironmentDashboard|API"
msgstr ""
msgid "EnvironmentDashboard|Created through the Deployment API"
msgstr ""
msgid "Environments" msgid "Environments"
msgstr "" msgstr ""
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
require 'spec_helper' require 'spec_helper'
describe ProjectsFinder do describe ProjectsFinder, :do_not_mock_admin_mode do
include AdminModeHelper
describe '#execute' do describe '#execute' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group, :public) } let(:group) { create(:group, :public) }
...@@ -188,5 +190,21 @@ describe ProjectsFinder do ...@@ -188,5 +190,21 @@ describe ProjectsFinder do
it { is_expected.to eq([internal_project, public_project]) } it { is_expected.to eq([internal_project, public_project]) }
end end
describe 'with admin user' do
let(:user) { create(:admin) }
context 'admin mode enabled' do
before do
enable_admin_mode!(current_user)
end
it { is_expected.to match_array([public_project, internal_project, private_project, shared_project]) }
end
context 'admin mode disabled' do
it { is_expected.to match_array([public_project, internal_project]) }
end
end
end end
end end
...@@ -17,6 +17,26 @@ describe Gitlab::Graphql::Connections::Keyset::OrderInfo do ...@@ -17,6 +17,26 @@ describe Gitlab::Graphql::Connections::Keyset::OrderInfo do
expect(order_list.last.operator_for(:after)).to eq '>' expect(order_list.last.operator_for(:after)).to eq '>'
end end
end end
context 'when order contains NULLS LAST' do
let(:relation) { Project.order(Arel.sql('projects.updated_at Asc Nulls Last')).order(:id) }
it 'does not ignore the SQL order' do
expect(order_list.count).to eq 2
expect(order_list.first.attribute_name).to eq 'projects.updated_at'
expect(order_list.first.operator_for(:after)).to eq '>'
expect(order_list.last.attribute_name).to eq 'id'
expect(order_list.last.operator_for(:after)).to eq '>'
end
end
context 'when order contains invalid formatted NULLS LAST ' do
let(:relation) { Project.order(Arel.sql('projects.updated_at created_at Asc Nulls Last')).order(:id) }
it 'ignores the SQL order' do
expect(order_list.count).to eq 1
end
end
end end
describe '#validate_ordering' do describe '#validate_ordering' do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe User do describe User, :do_not_mock_admin_mode do
include ProjectForksHelper include ProjectForksHelper
include TermsHelper include TermsHelper
...@@ -2797,10 +2797,26 @@ describe User do ...@@ -2797,10 +2797,26 @@ describe User do
expect(user.full_private_access?).to be_falsy expect(user.full_private_access?).to be_falsy
end end
it 'returns true for admin user' do context 'for admin user' do
user = build(:user, :admin) include_context 'custom session'
expect(user.full_private_access?).to be_truthy let(:user) { build(:user, :admin) }
context 'when admin mode is disabled' do
it 'returns false' do
expect(user.full_private_access?).to be_falsy
end
end
context 'when admin mode is enabled' do
before do
Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password)
end
it 'returns true' do
expect(user.full_private_access?).to be_truthy
end
end
end end
end end
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
require 'spec_helper' require 'spec_helper'
describe BasePolicy do describe BasePolicy, :do_not_mock_admin_mode do
include ExternalAuthorizationServiceHelpers include ExternalAuthorizationServiceHelpers
include AdminModeHelper
describe '.class_for' do describe '.class_for' do
it 'detects policy class based on the subject ancestors' do it 'detects policy class based on the subject ancestors' do
...@@ -36,8 +37,42 @@ describe BasePolicy do ...@@ -36,8 +37,42 @@ describe BasePolicy do
it { is_expected.not_to be_allowed(:read_cross_project) } it { is_expected.not_to be_allowed(:read_cross_project) }
it 'allows admins' do context 'for admins' do
expect(described_class.new(build(:admin), nil)).to be_allowed(:read_cross_project) let(:current_user) { build(:admin) }
subject { described_class.new(current_user, nil) }
it 'allowed when in admin mode' do
enable_admin_mode!(current_user)
is_expected.to be_allowed(:read_cross_project)
end
it 'prevented when not in admin mode' do
is_expected.not_to be_allowed(:read_cross_project)
end
end
end
end
describe 'full private access' do
let(:current_user) { create(:user) }
subject { described_class.new(current_user, nil) }
it { is_expected.not_to be_allowed(:read_all_resources) }
context 'for admins' do
let(:current_user) { build(:admin) }
it 'allowed when in admin mode' do
enable_admin_mode!(current_user)
is_expected.to be_allowed(:read_all_resources)
end
it 'prevented when not in admin mode' do
is_expected.not_to be_allowed(:read_all_resources)
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment