Commit 7c57de4e authored by peterhegman's avatar peterhegman

Only show 2FA badge to project maintainers and group owners

Doesn't make sense to show 2FA badge to everyone
parent 77239704
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
GlBadge, GlBadge,
GlSafeHtmlDirective as SafeHtml, GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { mapState } from 'vuex';
import { generateBadges } from 'ee_else_ce/members/utils'; import { generateBadges } from 'ee_else_ce/members/utils';
import { glEmojiTag } from '~/emoji'; import { glEmojiTag } from '~/emoji';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -34,11 +35,16 @@ export default { ...@@ -34,11 +35,16 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['canManageMembers']),
user() { user() {
return this.member.user; return this.member.user;
}, },
badges() { badges() {
return generateBadges(this.member, this.isCurrentUser).filter((badge) => badge.show); return generateBadges({
member: this.member,
isCurrentUser: this.isCurrentUser,
canManageMembers: this.canManageMembers,
}).filter((badge) => badge.show);
}, },
statusEmoji() { statusEmoji() {
return this.user?.status?.emoji; return this.user?.status?.emoji;
......
...@@ -13,7 +13,7 @@ import { ...@@ -13,7 +13,7 @@ import {
GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME, GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
} from './constants'; } from './constants';
export const generateBadges = (member, isCurrentUser) => [ export const generateBadges = ({ member, isCurrentUser, canManageMembers }) => [
{ {
show: isCurrentUser, show: isCurrentUser,
text: __("It's you"), text: __("It's you"),
...@@ -25,7 +25,7 @@ export const generateBadges = (member, isCurrentUser) => [ ...@@ -25,7 +25,7 @@ export const generateBadges = (member, isCurrentUser) => [
variant: 'danger', variant: 'danger',
}, },
{ {
show: member.user?.twoFactorEnabled, show: member.user?.twoFactorEnabled && (canManageMembers || isCurrentUser),
text: __('2FA'), text: __('2FA'),
variant: 'info', variant: 'info',
}, },
......
...@@ -40,7 +40,7 @@ module Projects::ProjectMembersHelper ...@@ -40,7 +40,7 @@ module Projects::ProjectMembersHelper
members: project_members_data_json(project, members), members: project_members_data_json(project, members),
member_path: project_project_member_path(project, ':id'), member_path: project_project_member_path(project, ':id'),
source_id: project.id, source_id: project.id,
can_manage_members: can_manage_project_members?(project) can_manage_members: can_manage_project_members?(project).to_s
} }
end end
...@@ -49,7 +49,7 @@ module Projects::ProjectMembersHelper ...@@ -49,7 +49,7 @@ module Projects::ProjectMembersHelper
members: project_group_links_data_json(group_links), members: project_group_links_data_json(group_links),
member_path: project_group_link_path(project, ':id'), member_path: project_group_link_path(project, ':id'),
source_id: project.id, source_id: project.id,
can_manage_members: can_manage_project_members?(project) can_manage_members: can_manage_project_members?(project).to_s
} }
end end
end end
---
title: Only show 2FA badge to project maintainers and group owners
merge_request: 54646
author:
type: changed
...@@ -173,6 +173,7 @@ The following table depicts the various user permission levels in a project. ...@@ -173,6 +173,7 @@ The following table depicts the various user permission levels in a project.
| View project Audit Events | | | ✓ (*12*) | ✓ | ✓ | | View project Audit Events | | | ✓ (*12*) | ✓ | ✓ |
| Manage [push rules](../push_rules/push_rules.md) | | | | ✓ | ✓ | | Manage [push rules](../push_rules/push_rules.md) | | | | ✓ | ✓ |
| Manage [project access tokens](project/settings/project_access_tokens.md) **(FREE SELF)** | | | | ✓ | ✓ | | Manage [project access tokens](project/settings/project_access_tokens.md) **(FREE SELF)** | | | | ✓ | ✓ |
| View 2FA status of members | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ | | Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ |
| Rename project | | | | | ✓ | | Rename project | | | | | ✓ |
...@@ -293,6 +294,7 @@ group. ...@@ -293,6 +294,7 @@ group.
| View Value Stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ | | View Value Stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Billing **(FREE SAAS)** | | | | | ✓ (4) | | View Billing **(FREE SAAS)** | | | | | ✓ (4) |
| View Usage Quotas **(FREE SAAS)** | | | | | ✓ (4) | | View Usage Quotas **(FREE SAAS)** | | | | | ✓ (4) |
| View 2FA status of members | | | | | ✓ |
| Filter members by 2FA status | | | | | ✓ | | Filter members by 2FA status | | | | | ✓ |
| Administer project compliance frameworks | | | | | ✓ | | Administer project compliance frameworks | | | | | ✓ |
......
...@@ -14,8 +14,8 @@ export { ...@@ -14,8 +14,8 @@ export {
canUpdate, canUpdate,
} from '~/members/utils'; } from '~/members/utils';
export const generateBadges = (member, isCurrentUser) => [ export const generateBadges = ({ member, isCurrentUser, canManageMembers }) => [
...CEGenerateBadges(member, isCurrentUser), ...CEGenerateBadges({ member, isCurrentUser, canManageMembers }),
{ {
show: member.usingLicense, show: member.usingLicense,
text: __('Is using seat'), text: __('Is using seat'),
......
import { GlBadge } from '@gitlab/ui'; import { GlBadge } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { member as memberMock } from 'jest/members/mock_data'; import { member as memberMock } from 'jest/members/mock_data';
import UserAvatar from '~/members/components/avatars/user_avatar.vue'; import UserAvatar from '~/members/components/avatars/user_avatar.vue';
Vue.use(Vuex);
describe('UserAvatar', () => { describe('UserAvatar', () => {
let wrapper; let wrapper;
...@@ -12,6 +16,11 @@ describe('UserAvatar', () => { ...@@ -12,6 +16,11 @@ describe('UserAvatar', () => {
isCurrentUser: false, isCurrentUser: false,
...propsData, ...propsData,
}, },
store: new Vuex.Store({
state: {
canManageMembers: true,
},
}),
}); });
}; };
......
...@@ -10,7 +10,11 @@ import { ...@@ -10,7 +10,11 @@ import {
describe('Members Utils', () => { describe('Members Utils', () => {
describe('generateBadges', () => { describe('generateBadges', () => {
it('has correct properties for each badge', () => { it('has correct properties for each badge', () => {
const badges = generateBadges(memberMock, true); const badges = generateBadges({
member: memberMock,
isCurrentUser: true,
canManageMembers: true,
});
badges.forEach((badge) => { badges.forEach((badge) => {
expect(badge).toEqual( expect(badge).toEqual(
...@@ -30,7 +34,9 @@ describe('Members Utils', () => { ...@@ -30,7 +34,9 @@ describe('Members Utils', () => {
${{ ...memberMock, groupManagedAccount: true }} | ${{ show: true, text: 'Managed Account', variant: 'info' }} ${{ ...memberMock, groupManagedAccount: true }} | ${{ show: true, text: 'Managed Account', variant: 'info' }}
${{ ...memberMock, canOverride: true }} | ${{ show: true, text: 'LDAP', variant: 'info' }} ${{ ...memberMock, canOverride: true }} | ${{ show: true, text: 'LDAP', variant: 'info' }}
`('returns expected output for "$expected.text" badge', ({ member, expected }) => { `('returns expected output for "$expected.text" badge', ({ member, expected }) => {
expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected)); expect(
generateBadges({ member, isCurrentUser: true, canManageMembers: true }),
).toContainEqual(expect.objectContaining(expected));
}); });
}); });
......
...@@ -47,4 +47,36 @@ RSpec.describe 'Groups > Members > List members', :js do ...@@ -47,4 +47,36 @@ RSpec.describe 'Groups > Members > List members', :js do
expect(first_row).to have_selector('gl-emoji[data-name="smirk"]') expect(first_row).to have_selector('gl-emoji[data-name="smirk"]')
end end
end end
describe 'when user has 2FA enabled' do
let_it_be(:user_with_2fa) { create(:user, :two_factor_via_otp) }
before do
group.add_guest(user_with_2fa)
end
it 'shows 2FA badge to user with "Owner" access level' do
group.add_owner(user1)
visit group_group_members_path(group)
expect(find_member_row(user_with_2fa)).to have_content('2FA')
end
it 'does not show 2FA badge to users with access level below "Owner"' do
group.add_maintainer(user1)
visit group_group_members_path(group)
expect(find_member_row(user_with_2fa)).not_to have_content('2FA')
end
it 'shows 2FA badge to themselves' do
sign_in(user_with_2fa)
visit group_group_members_path(group)
expect(find_member_row(user_with_2fa)).to have_content('2FA')
end
end
end end
...@@ -117,6 +117,38 @@ RSpec.describe 'Project members list' do ...@@ -117,6 +117,38 @@ RSpec.describe 'Project members list' do
end end
end end
end end
describe 'when user has 2FA enabled' do
let_it_be(:user_with_2fa) { create(:user, :two_factor_via_otp) }
before do
project.add_guest(user_with_2fa)
end
it 'shows 2FA badge to user with "Maintainer" access level' do
project.add_maintainer(user1)
visit_members_page
expect(find_member_row(user_with_2fa)).to have_content('2FA')
end
it 'does not show 2FA badge to users with access level below "Maintainer"' do
group.add_developer(user1)
visit_members_page
expect(find_member_row(user_with_2fa)).not_to have_content('2FA')
end
it 'shows 2FA badge to themselves' do
sign_in(user_with_2fa)
visit_members_page
expect(find_member_row(user_with_2fa)).to have_content('2FA')
end
end
end end
context 'when `vue_project_members_list` feature flag is disabled' do context 'when `vue_project_members_list` feature flag is disabled' do
......
import { GlAvatarLink, GlBadge } from '@gitlab/ui'; import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { within } from '@testing-library/dom'; import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils'; import { mount, createWrapper } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import UserAvatar from '~/members/components/avatars/user_avatar.vue'; import UserAvatar from '~/members/components/avatars/user_avatar.vue';
import { member as memberMock, orphanedMember } from '../../mock_data'; import { member as memberMock, member2faEnabled, orphanedMember } from '../../mock_data';
Vue.use(Vuex);
describe('UserAvatar', () => { describe('UserAvatar', () => {
let wrapper; let wrapper;
const { user } = memberMock; const { user } = memberMock;
const createComponent = (propsData = {}) => { const createComponent = (propsData = {}, state = {}) => {
wrapper = mount(UserAvatar, { wrapper = mount(UserAvatar, {
propsData: { propsData: {
member: memberMock, member: memberMock,
isCurrentUser: false, isCurrentUser: false,
...propsData, ...propsData,
}, },
store: new Vuex.Store({
state: {
canManageMembers: true,
...state,
},
}),
}); });
}; };
...@@ -69,9 +79,9 @@ describe('UserAvatar', () => { ...@@ -69,9 +79,9 @@ describe('UserAvatar', () => {
describe('badges', () => { describe('badges', () => {
it.each` it.each`
member | badgeText member | badgeText
${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${'Blocked'} ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${'Blocked'}
${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${'2FA'} ${member2faEnabled} | ${'2FA'}
`('renders the "$badgeText" badge', ({ member, badgeText }) => { `('renders the "$badgeText" badge', ({ member, badgeText }) => {
createComponent({ member }); createComponent({ member });
...@@ -83,6 +93,12 @@ describe('UserAvatar', () => { ...@@ -83,6 +93,12 @@ describe('UserAvatar', () => {
expect(getByText("It's you").exists()).toBe(true); expect(getByText("It's you").exists()).toBe(true);
}); });
it('does not render 2FA badge when `canManageMembers` is `false`', () => {
createComponent({ member: member2faEnabled }, { canManageMembers: false });
expect(within(wrapper.element).queryByText('2FA')).toBe(null);
});
}); });
describe('user status', () => { describe('user status', () => {
......
...@@ -75,3 +75,5 @@ export const membersJsonString = JSON.stringify(members); ...@@ -75,3 +75,5 @@ export const membersJsonString = JSON.stringify(members);
export const directMember = { ...member, isDirectMember: true }; export const directMember = { ...member, isDirectMember: true };
export const inheritedMember = { ...member, isDirectMember: false }; export const inheritedMember = { ...member, isDirectMember: false };
export const member2faEnabled = { ...member, user: { ...member.user, twoFactorEnabled: true } };
...@@ -17,6 +17,7 @@ import { ...@@ -17,6 +17,7 @@ import {
member as memberMock, member as memberMock,
directMember, directMember,
inheritedMember, inheritedMember,
member2faEnabled,
group, group,
invite, invite,
membersJsonString, membersJsonString,
...@@ -30,7 +31,11 @@ const URL_HOST = 'https://localhost/'; ...@@ -30,7 +31,11 @@ const URL_HOST = 'https://localhost/';
describe('Members Utils', () => { describe('Members Utils', () => {
describe('generateBadges', () => { describe('generateBadges', () => {
it('has correct properties for each badge', () => { it('has correct properties for each badge', () => {
const badges = generateBadges(memberMock, true); const badges = generateBadges({
member: memberMock,
isCurrentUser: true,
canManageMembers: true,
});
badges.forEach((badge) => { badges.forEach((badge) => {
expect(badge).toEqual( expect(badge).toEqual(
...@@ -44,12 +49,40 @@ describe('Members Utils', () => { ...@@ -44,12 +49,40 @@ describe('Members Utils', () => {
}); });
it.each` it.each`
member | expected member | expected
${memberMock} | ${{ show: true, text: "It's you", variant: 'success' }} ${memberMock} | ${{ show: true, text: "It's you", variant: 'success' }}
${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: 'Blocked', variant: 'danger' }} ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: 'Blocked', variant: 'danger' }}
${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${{ show: true, text: '2FA', variant: 'info' }} ${member2faEnabled} | ${{ show: true, text: '2FA', variant: 'info' }}
`('returns expected output for "$expected.text" badge', ({ member, expected }) => { `('returns expected output for "$expected.text" badge', ({ member, expected }) => {
expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected)); expect(
generateBadges({ member, isCurrentUser: true, canManageMembers: true }),
).toContainEqual(expect.objectContaining(expected));
});
describe('when `canManageMembers` argument is `false`', () => {
describe('when member is not the current user', () => {
it('sets `show` to `false` for 2FA badge', () => {
const badges = generateBadges({
member: member2faEnabled,
isCurrentUser: false,
canManageMembers: false,
});
expect(badges.find((badge) => badge.text === '2FA').show).toBe(false);
});
});
describe('when member is the current user', () => {
it('sets `show` to `false` for 2fA badge', () => {
const badges = generateBadges({
member: member2faEnabled,
isCurrentUser: true,
canManageMembers: false,
});
expect(badges.find((badge) => badge.text === '2FA').show).toBe(true);
});
});
}); });
}); });
......
...@@ -166,7 +166,7 @@ RSpec.describe Projects::ProjectMembersHelper do ...@@ -166,7 +166,7 @@ RSpec.describe Projects::ProjectMembersHelper do
members: helper.project_members_data_json(project, present_members(project_members)), members: helper.project_members_data_json(project, present_members(project_members)),
member_path: '/foo-bar/-/project_members/:id', member_path: '/foo-bar/-/project_members/:id',
source_id: project.id, source_id: project.id,
can_manage_members: true can_manage_members: 'true'
}) })
end end
end end
...@@ -193,7 +193,7 @@ RSpec.describe Projects::ProjectMembersHelper do ...@@ -193,7 +193,7 @@ RSpec.describe Projects::ProjectMembersHelper do
members: helper.project_group_links_data_json(project_group_links), members: helper.project_group_links_data_json(project_group_links),
member_path: '/foo-bar/-/group_links/:id', member_path: '/foo-bar/-/group_links/:id',
source_id: project.id, source_id: project.id,
can_manage_members: true can_manage_members: 'true'
}) })
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