Commit 35212d80 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '215684-security-bot-profile' into 'master'

Auto-Remediation - Bot profile - Frontend

See merge request gitlab-org/gitlab!44085
parents a32b94cc c8d9b6b9
...@@ -42,6 +42,7 @@ const populateUserInfo = user => { ...@@ -42,6 +42,7 @@ const populateUserInfo = user => {
bio: userData.bio, bio: userData.bio,
bioHtml: sanitize(userData.bio_html), bioHtml: sanitize(userData.bio_html),
workInformation: userData.work_information, workInformation: userData.work_information,
websiteUrl: userData.website_url,
loaded: true, loaded: true,
}); });
} }
......
<script> <script>
/* eslint-disable vue/no-v-html */ /* eslint-disable vue/no-v-html */
import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlIcon } from '@gitlab/ui'; import {
GlPopover,
GlLink,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlIcon,
} from '@gitlab/ui';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji'; import { glEmojiTag } from '../../../emoji';
const MAX_SKELETON_LINES = 4; const MAX_SKELETON_LINES = 4;
const SECURITY_BOT_USER_DATA = {
username: 'GitLab-Security-Bot',
name: 'GitLab Security Bot',
};
export default { export default {
name: 'UserPopover', name: 'UserPopover',
maxSkeletonLines: MAX_SKELETON_LINES, maxSkeletonLines: MAX_SKELETON_LINES,
components: { components: {
GlIcon, GlIcon,
GlLink,
GlPopover, GlPopover,
GlSkeletonLoading, GlSkeletonLoading,
UserAvatarImage, UserAvatarImage,
...@@ -43,6 +54,15 @@ export default { ...@@ -43,6 +54,15 @@ export default {
userIsLoading() { userIsLoading() {
return !this.user?.loaded; return !this.user?.loaded;
}, },
isSecurityBot() {
const { username, name, websiteUrl = '' } = this.user;
return (
gon.features?.securityAutoFix &&
username === SECURITY_BOT_USER_DATA.username &&
name === SECURITY_BOT_USER_DATA.name &&
websiteUrl.length
);
},
}, },
}; };
</script> </script>
...@@ -89,6 +109,12 @@ export default { ...@@ -89,6 +109,12 @@ export default {
<div v-if="statusHtml" class="js-user-status gl-mt-3"> <div v-if="statusHtml" class="js-user-status gl-mt-3">
<span v-html="statusHtml"></span> <span v-html="statusHtml"></span>
</div> </div>
<div v-if="isSecurityBot" class="gl-text-blue-500">
<gl-icon name="question" />
<gl-link data-testid="user-popover-bot-docs-link" :href="user.websiteUrl">
{{ sprintf(__('Learn more about %{username}'), { username: user.name }) }}
</gl-link>
</div>
</template> </template>
</div> </div>
</div> </div>
......
...@@ -15,3 +15,5 @@ ...@@ -15,3 +15,5 @@
.row-second-line.str-truncated-100 .row-second-line.str-truncated-100
= mail_to user.email, user.email, class: 'text-secondary' = mail_to user.email, user.email, class: 'text-secondary'
- unless Feature.disabled?(:security_auto_fix) || !user.internal? || user.website_url.blank?
= link_to "(#{_('more information')})", user.website_url
- activity_pane_class = Feature.enabled?(:security_auto_fix) && @user.bot? ? "col-12" : "col-md-12 col-lg-6"
.row .row
.col-12 .col-12
.calendar-block.gl-mt-3.gl-mb-3 .calendar-block.gl-mt-3.gl-mb-3
...@@ -6,23 +8,24 @@ ...@@ -6,23 +8,24 @@
.spinner.spinner-md .spinner.spinner-md
.user-calendar-activities.d-none.d-sm-block .user-calendar-activities.d-none.d-sm-block
.row .row
.col-md-12.col-lg-6 %div{ class: activity_pane_class }
- if can?(current_user, :read_cross_project) - if can?(current_user, :read_cross_project)
.activities-block .activities-block
.gl-mt-5 .gl-mt-5
.d-flex.align-items-center.border-bottom .gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid
%h4.flex-grow %h4.gl-flex-grow-1
= s_('UserProfile|Activity') = Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all" = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_path } } .overview-content-list{ data: { href: user_path } }
.center.light.loading .center.light.loading
.spinner.spinner-md .spinner.spinner-md
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
.col-md-12.col-lg-6 .col-md-12.col-lg-6
.projects-block .projects-block
.gl-mt-5 .gl-mt-5
.d-flex.align-items-center.border-bottom .gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid
%h4.flex-grow %h4.gl-flex-grow-1
= s_('UserProfile|Personal projects') = s_('UserProfile|Personal projects')
= link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all" = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_projects_path } } .overview-content-list{ data: { href: user_projects_path } }
......
...@@ -78,6 +78,8 @@ ...@@ -78,6 +78,8 @@
= sprite_icon('twitter') = sprite_icon('twitter')
- unless @user.website_url.blank? - unless @user.website_url.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0 .profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
- if Feature.enabled?(:security_auto_fix) && @user.bot?
= sprite_icon('question', css_class: 'gl-text-blue-600')
= link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow' = link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow'
- unless @user.public_email.blank? - unless @user.public_email.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0 .profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
...@@ -101,6 +103,7 @@ ...@@ -101,6 +103,7 @@
%li.js-activity-tab %li.js-activity-tab
= link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do = link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
= s_('UserProfile|Activity') = s_('UserProfile|Activity')
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
- if profile_tab?(:groups) - if profile_tab?(:groups)
%li.js-groups-tab %li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
...@@ -136,7 +139,7 @@ ...@@ -136,7 +139,7 @@
.content_list{ data: { href: user_path } } .content_list{ data: { href: user_path } }
.loading .loading
.spinner.spinner-md .spinner.spinner-md
- unless @user.bot?
- if profile_tab?(:groups) - if profile_tab?(:groups)
#groups.tab-pane #groups.tab-pane
-# This tab is always loaded via AJAX -# This tab is always loaded via AJAX
......
...@@ -46,6 +46,7 @@ module Gitlab ...@@ -46,6 +46,7 @@ module Gitlab
push_frontend_feature_flag(:webperf_experiment, default_enabled: false) push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false) push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
push_frontend_feature_flag(:usage_data_api, default_enabled: true) push_frontend_feature_flag(:usage_data_api, default_enabled: true)
push_frontend_feature_flag(:security_auto_fix, default_enabled: false)
# Startup CSS feature is a special one as it can be enabled by means of cookies and params # Startup CSS feature is a special one as it can be enabled by means of cookies and params
gon.push({ features: { 'startupCss' => use_startup_css? } }, true) gon.push({ features: { 'startupCss' => use_startup_css? } }, true)
......
...@@ -15252,6 +15252,9 @@ msgstr "" ...@@ -15252,6 +15252,9 @@ msgstr ""
msgid "Learn more" msgid "Learn more"
msgstr "" msgstr ""
msgid "Learn more about %{username}"
msgstr ""
msgid "Learn more about Auto DevOps" msgid "Learn more about Auto DevOps"
msgstr "" msgstr ""
...@@ -28614,6 +28617,9 @@ msgstr "" ...@@ -28614,6 +28617,9 @@ msgstr ""
msgid "UserProfile|Blocked user" msgid "UserProfile|Blocked user"
msgstr "" msgstr ""
msgid "UserProfile|Bot activity"
msgstr ""
msgid "UserProfile|Contributed projects" msgid "UserProfile|Contributed projects"
msgstr "" msgstr ""
...@@ -31231,6 +31237,9 @@ msgstr "" ...@@ -31231,6 +31237,9 @@ msgstr ""
msgid "missing" msgid "missing"
msgstr "" msgstr ""
msgid "more information"
msgstr ""
msgid "most recent deployment" msgid "most recent deployment"
msgstr "" msgstr ""
......
...@@ -21,7 +21,6 @@ RSpec.describe 'Overview tab on a user profile', :js do ...@@ -21,7 +21,6 @@ RSpec.describe 'Overview tab on a user profile', :js do
sign_in user sign_in user
end end
describe 'activities section' do
shared_context 'visit overview tab' do shared_context 'visit overview tab' do
before do before do
visit user.username visit user.username
...@@ -30,6 +29,7 @@ RSpec.describe 'Overview tab on a user profile', :js do ...@@ -30,6 +29,7 @@ RSpec.describe 'Overview tab on a user profile', :js do
end end
end end
describe 'activities section' do
describe 'user has no activities' do describe 'user has no activities' do
include_context 'visit overview tab' include_context 'visit overview tab'
...@@ -84,14 +84,6 @@ RSpec.describe 'Overview tab on a user profile', :js do ...@@ -84,14 +84,6 @@ RSpec.describe 'Overview tab on a user profile', :js do
end end
describe 'projects section' do describe 'projects section' do
shared_context 'visit overview tab' do
before do
visit user.username
page.find('.js-overview-tab a').click
wait_for_requests
end
end
describe 'user has no personal projects' do describe 'user has no personal projects' do
include_context 'visit overview tab' include_context 'visit overview tab'
...@@ -158,4 +150,52 @@ RSpec.describe 'Overview tab on a user profile', :js do ...@@ -158,4 +150,52 @@ RSpec.describe 'Overview tab on a user profile', :js do
end end
end end
end end
describe 'bot user' do
let(:bot_user) { create(:user, user_type: :security_bot) }
shared_context "visit bot's overview tab" do
before do
visit bot_user.username
page.find('.js-overview-tab a').click
wait_for_requests
end
end
describe 'feature flag enabled' do
before do
stub_feature_flags(security_auto_fix: true)
end
include_context "visit bot's overview tab"
it "activity panel's title is 'Bot activity'" do
page.within('.activities-block') do
expect(page).to have_text('Bot activity')
end
end
it 'does not show projects panel' do
expect(page).not_to have_selector('.projects-block')
end
end
describe 'feature flag disabled' do
before do
stub_feature_flags(security_auto_fix: false)
end
include_context "visit bot's overview tab"
it "activity panel's title is not 'Bot activity'" do
page.within('.activities-block') do
expect(page).not_to have_text('Bot activity')
end
end
it 'shows projects panel' do
expect(page).to have_selector('.projects-block')
end
end
end
end end
...@@ -182,4 +182,46 @@ RSpec.describe 'User page' do ...@@ -182,4 +182,46 @@ RSpec.describe 'User page' do
it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
end end
context 'with a bot user' do
let(:user) { create(:user, user_type: :security_bot) }
describe 'feature flag enabled' do
before do
stub_feature_flags(security_auto_fix: true)
end
it 'only shows Overview and Activity tabs' do
visit(user_path(user))
page.within '.nav-links' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).not_to have_link('Groups')
expect(page).not_to have_link('Contributed projects')
expect(page).not_to have_link('Personal projects')
expect(page).not_to have_link('Snippets')
end
end
end
describe 'feature flag disabled' do
before do
stub_feature_flags(security_auto_fix: false)
end
it 'only shows Overview and Activity tabs' do
visit(user_path(user))
page.within '.nav-links' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).to have_link('Groups')
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
end
end
end
end
end end
...@@ -21,6 +21,9 @@ describe('User Popover Component', () => { ...@@ -21,6 +21,9 @@ describe('User Popover Component', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
window.gon.features = {
securityAutoFix: true,
};
loadFixtures(fixtureTemplate); loadFixtures(fixtureTemplate);
}); });
...@@ -28,6 +31,7 @@ describe('User Popover Component', () => { ...@@ -28,6 +31,7 @@ describe('User Popover Component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const findByTestId = testid => wrapper.find(`[data-testid="${testid}"]`);
const findUserStatus = () => wrapper.find('.js-user-status'); const findUserStatus = () => wrapper.find('.js-user-status');
const findTarget = () => document.querySelector('.js-user-link'); const findTarget = () => document.querySelector('.js-user-link');
...@@ -196,4 +200,30 @@ describe('User Popover Component', () => { ...@@ -196,4 +200,30 @@ describe('User Popover Component', () => {
expect(findUserStatus().exists()).toBe(false); expect(findUserStatus().exists()).toBe(false);
}); });
}); });
describe('security bot', () => {
const SECURITY_BOT_USER = {
...DEFAULT_PROPS.user,
name: 'GitLab Security Bot',
username: 'GitLab-Security-Bot',
websiteUrl: '/security/bot/docs',
};
const findSecurityBotDocsLink = () => findByTestId('user-popover-bot-docs-link');
it("shows a link to the bot's documentation", () => {
createWrapper({ user: SECURITY_BOT_USER });
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.exists()).toBe(true);
expect(securityBotDocsLink.attributes('href')).toBe(SECURITY_BOT_USER.websiteUrl);
});
it('does not show the link if the feature flag is disabled', () => {
window.gon.features = {
securityAutoFix: false,
};
createWrapper({ user: SECURITY_BOT_USER });
expect(findSecurityBotDocsLink().exists()).toBe(false);
});
});
}); });
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