Commit 8517148a authored by Andrew Fontaine's avatar Andrew Fontaine

Check if User can Update Protected Environment

If an environment is protected, and a user cannot modify it, they should
not be able to update/delete feature flags on that environment, nor can
they rename, edit, or delete the feature flag.
parent 516145a1
......@@ -53,6 +53,10 @@ export default {
required: false,
default: __('Create'),
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
......@@ -175,10 +179,12 @@ export default {
:aria-label="placeholder"
:value="filter"
:placeholder="placeholder"
:disabled="disabled"
@input="fetchEnvironments"
/>
<gl-button
v-if="!disabled"
class="js-clear-search-input btn-transparent clear-search-input position-right-0"
@click="clearInput"
>
......
......@@ -32,6 +32,9 @@ export default {
};
},
computed: {
permissions() {
return gon && gon.features && gon.features.featureFlagPermissions;
},
modalTitle() {
return sprintf(
s__('FeatureFlags|Delete %{name}?'),
......@@ -65,6 +68,9 @@ export default {
scopeName(name) {
return name === '*' ? s__('FeatureFlags|* (All environments)') : name;
},
canDeleteFlag(flag) {
return !this.permissions || (flag.scopes || []).every(scope => scope.can_update);
},
setDeleteModalData(featureFlag) {
this.deleteFeatureFlagUrl = featureFlag.destroy_path;
this.deleteFeatureFlagName = featureFlag.name;
......@@ -149,6 +155,7 @@ export default {
v-gl-modal-directive="modalId"
class="js-feature-flag-delete-button"
variant="danger"
:disabled="!canDeleteFlag(featureFlag)"
@click="setDeleteModalData(featureFlag)"
>
<icon name="remove" :size="16" />
......
<script>
import _ from 'underscore';
import { GlButton } from '@gitlab/ui';
import { GlButton, GlBadge } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
......@@ -10,6 +10,7 @@ import { internalKeyID } from '../store/modules/helpers';
export default {
components: {
GlButton,
GlBadge,
ToggleButton,
Icon,
EnvironmentsDropdown,
......@@ -71,6 +72,13 @@ export default {
// eslint-disable-next-line no-underscore-dangle
return this.formScopes.filter(scope => !scope._destroy);
},
canUpdateFlag() {
return !this.permissionsFlag || (this.scopes || []).every(scope => scope.can_update);
},
permissionsFlag() {
return gon && gon.features && gon.features.featureFlagPermissions;
},
},
methods: {
isAllEnvironment(name) {
......@@ -108,11 +116,18 @@ export default {
* @param {Boolean} value the toggle value
*/
onChangeNewScopeStatus(value) {
this.formScopes.push({
const newScope = {
active: value,
environment_scope: this.newScope,
id: _.uniqueId(internalKeyID),
});
};
if (this.permissionsFlag) {
newScope.can_update = true;
newScope.protected = false;
}
this.formScopes.push(newScope);
this.newScope = '';
},
......@@ -160,6 +175,10 @@ export default {
scopes: this.formScopes,
});
},
canUpdateScope(scope) {
return !this.permissionsFlag || scope.can_update;
},
},
};
</script>
......@@ -169,7 +188,12 @@ export default {
<div class="row">
<div class="form-group col-md-4">
<label for="feature-flag-name" class="label-bold">{{ s__('FeatureFlags|Name') }}</label>
<input id="feature-flag-name" v-model="formName" class="form-control" />
<input
id="feature-flag-name"
v-model="formName"
:disabled="!canUpdateFlag"
class="form-control"
/>
</div>
</div>
......@@ -181,6 +205,7 @@ export default {
<textarea
id="feature-flag-description"
v-model="formDescription"
:disabled="!canUpdateFlag"
class="form-control"
rows="4"
></textarea>
......@@ -212,7 +237,9 @@ export default {
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Environment Spec') }}
</div>
<div class="table-mobile-content js-feature-flag-status">
<div
class="table-mobile-content js-feature-flag-status d-flex align-items-center justify-content-start"
>
<p v-if="isAllEnvironment(scope.environment_scope)" class="js-scope-all pl-3">
{{ $options.allEnvironments }}
</p>
......@@ -222,10 +249,15 @@ export default {
class="col-md-6"
:value="scope.environment_scope"
:endpoint="environmentsEndpoint"
:disabled="!canUpdateScope(scope)"
@selectEnvironment="env => updateScope(env, scope, index)"
@createClicked="env => updateScope(env, scope, index)"
@clearInput="updateScope('', scope, index)"
/>
<gl-badge v-if="permissionsFlag && scope.protected" variant="success">{{
s__('FeatureFlags|Protected')
}}</gl-badge>
</div>
</div>
......@@ -236,6 +268,7 @@ export default {
<div class="table-mobile-content js-feature-flag-status">
<toggle-button
:value="scope.active"
:disabled-input="!canUpdateScope(scope)"
@change="status => onUpdateScopeStatus(scope, status)"
/>
</div>
......@@ -247,7 +280,7 @@ export default {
</div>
<div class="table-mobile-content js-feature-flag-delete">
<gl-button
v-if="!isAllEnvironment(scope.environment_scope)"
v-if="!isAllEnvironment(scope.environment_scope) && canUpdateScope(scope)"
class="js-delete-scope btn-transparent"
@click="removeScope(scope)"
>
......
......@@ -10,6 +10,10 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
before_action :feature_flag, only: [:edit, :update, :destroy]
before_action do
push_frontend_feature_flag(:feature_flag_permissions)
end
def index
@feature_flags = FeatureFlagsFinder
.new(project, current_user, scope: params[:scope])
......
......@@ -11,6 +11,7 @@ describe 'User creates feature flag', :js do
before do
project.add_developer(user)
stub_licensed_features(feature_flags: true)
stub_feature_flags(feature_flag_permissions: false)
sign_in(user)
end
......
......@@ -16,6 +16,7 @@ describe 'User deletes feature flag', :js do
before do
project.add_developer(user)
stub_licensed_features(feature_flags: true)
stub_feature_flags(feature_flag_permissions: false)
sign_in(user)
visit(project_feature_flags_path(project))
......
......@@ -18,6 +18,7 @@ describe 'User updates feature flag', :js do
before do
project.add_developer(user)
stub_licensed_features(feature_flags: true)
stub_feature_flags(feature_flag_permissions: false)
sign_in(user)
visit(edit_project_feature_flag_path(project, feature_flag))
......
......@@ -23,6 +23,14 @@ describe('feature flag form', () => {
});
};
beforeAll(() => {
gon.features = { featureFlagPermissions: true };
});
afterAll(() => {
gon.features = null;
});
afterEach(() => {
wrapper.destroy();
});
......@@ -87,13 +95,24 @@ describe('feature flag form', () => {
{
environment_scope: 'production',
active: false,
can_update: true,
protected: true,
id: 2,
},
{
environment_scope: 'review',
active: true,
can_update: true,
protected: false,
id: 4,
},
{
environment_scope: 'staging',
active: true,
can_update: false,
protected: true,
id: 5,
},
],
});
});
......@@ -113,8 +132,27 @@ describe('feature flag form', () => {
wrapper.find(ToggleButton).vm.$emit('change', true);
expect(wrapper.vm.formScopes).toEqual([
{ active: true, environment_scope: 'production', id: 2 },
{ active: true, environment_scope: 'review', id: 4 },
{
active: true,
environment_scope: 'production',
id: 2,
can_update: true,
protected: true,
},
{
active: true,
environment_scope: 'review',
id: 4,
can_update: true,
protected: false,
},
{
environment_scope: 'staging',
active: true,
can_update: false,
protected: true,
id: 5,
},
]);
expect(wrapper.vm.newScope).toEqual('');
......@@ -134,14 +172,42 @@ describe('feature flag form', () => {
active: false,
_destroy: true,
id: 2,
can_update: true,
protected: true,
},
{
active: true,
environment_scope: 'review',
id: 4,
can_update: true,
protected: false,
},
{
environment_scope: 'staging',
active: true,
can_update: false,
protected: true,
id: 5,
},
{ active: true, environment_scope: 'review', id: 4 },
]);
});
it('should not render deleted scopes', () => {
expect(wrapper.vm.filteredScopes).toEqual([
{ active: true, environment_scope: 'review', id: 4 },
{
active: true,
environment_scope: 'review',
id: 4,
can_update: true,
protected: false,
},
{
environment_scope: 'staging',
active: true,
can_update: false,
protected: true,
id: 5,
},
]);
});
});
......@@ -157,6 +223,8 @@ describe('feature flag form', () => {
environment_scope: 'new_scope',
active: false,
id: _.uniqueId(internalKeyID),
can_update: true,
protected: false,
},
],
});
......@@ -186,6 +254,28 @@ describe('feature flag form', () => {
expect(wrapper.find('.js-scope-all').exists()).toEqual(true);
});
});
describe('without permission to update', () => {
it('should have the flag name input disabled', () => {
const input = wrapper.find('#feature-flag-name');
expect(input.element.disabled).toBe(true);
});
it('should have the flag discription text area disabled', () => {
const textarea = wrapper.find('#feature-flag-description');
expect(textarea.element.disabled).toBe(true);
});
it('should have the scope that cannot be updated be disabled', () => {
const row = wrapper.findAll('.gl-responsive-table-row').wrappers[3];
expect(row.find(EnvironmentsDropdown).vm.disabled).toBe(true);
expect(row.find(ToggleButton).vm.disabledInput).toBe(true);
expect(row.find('.js-delete-scope').exists()).toBe(false);
});
});
});
describe('on submit', () => {
......@@ -197,6 +287,8 @@ describe('feature flag form', () => {
scopes: [
{
environment_scope: 'production',
can_update: true,
protected: true,
active: false,
},
],
......@@ -226,6 +318,8 @@ describe('feature flag form', () => {
expect(data.scopes[0]).toEqual({
active: false,
environment_scope: 'production',
can_update: true,
protected: true,
});
});
});
......
......@@ -4468,6 +4468,9 @@ msgstr ""
msgid "FeatureFlags|New Feature Flag"
msgstr ""
msgid "FeatureFlags|Protected"
msgstr ""
msgid "FeatureFlags|Status"
msgstr ""
......
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