Skip to content
Snippets Groups Projects
Commit 4de8a3da authored by Douwe Maan's avatar Douwe Maan
Browse files

Merge branch 'jxterry-private-profile' into 'master'

Add an option to have a private profile on GitLab

See merge request gitlab-org/gitlab-ce!20387
parents adc327d3 99011a61
No related branches found
No related tags found
1 merge request!10495Merge Requests - Assignee
Showing
with 257 additions and 64 deletions
Loading
Loading
@@ -99,7 +99,8 @@ class ProfilesController < Profiles::ApplicationController
:username,
:website_url,
:organization,
:preferred_language
:preferred_language,
:private_profile
)
end
end
Loading
Loading
@@ -13,6 +13,8 @@ class UsersController < ApplicationController
 
skip_before_action :authenticate_user!
before_action :user, except: [:exists]
before_action :authorize_read_user_profile!,
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets]
 
def show
respond_to do |format|
Loading
Loading
@@ -148,4 +150,8 @@ class UsersController < ApplicationController
def build_canonical_path(user)
url_for(safe_params.merge(username: user.to_param))
end
def authorize_read_user_profile!
access_denied! unless can?(current_user, :read_user_profile, user)
end
end
class PersonalProjectsFinder < UnionFinder
include Gitlab::Allowable
def initialize(user, params = {})
@user = user
@params = params
Loading
Loading
@@ -14,6 +16,8 @@ class PersonalProjectsFinder < UnionFinder
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
return Project.none unless can?(current_user, :read_user_profile, @user)
segments = all_projects(current_user)
 
find_union(segments, Project).includes(:namespace).order_updated_desc
Loading
Loading
Loading
Loading
@@ -7,6 +7,7 @@
class UserRecentEventsFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include Gitlab::Allowable
 
requires_cross_project_access
 
Loading
Loading
@@ -21,6 +22,8 @@ class UserRecentEventsFinder
end
 
def execute
return Event.none unless can?(current_user, :read_user_profile, target_user)
recent_events(params[:offset] || 0)
.joins(:project)
.with_associations
Loading
Loading
Loading
Loading
@@ -42,7 +42,13 @@ module UsersHelper
private
 
def get_profile_tabs
[:activity, :groups, :contributed, :projects, :snippets]
tabs = []
if can?(current_user, :read_user_profile, @user)
tabs += [:activity, :groups, :contributed, :projects, :snippets]
end
tabs
end
 
def get_current_user_menu_items
Loading
Loading
Loading
Loading
@@ -5,6 +5,9 @@ class UserPolicy < BasePolicy
desc "This is the ghost user"
condition(:subject_ghost, scope: :subject, score: 0) { @subject.ghost? }
 
desc "The profile is private"
condition(:private_profile, scope: :subject, score: 0) { @subject.private_profile? }
rule { ~restricted_public_level }.enable :read_user
rule { ~anonymous }.enable :read_user
 
Loading
Loading
@@ -12,4 +15,7 @@ class UserPolicy < BasePolicy
enable :destroy_user
enable :update_user
end
rule { default }.enable :read_user_profile
rule { private_profile & ~(user_is_self | admin) }.prevent :read_user_profile
end
Loading
Loading
@@ -64,7 +64,8 @@ module Users
:theme_id,
:twitter,
:username,
:website_url
:website_url,
:private_profile
]
end
 
Loading
Loading
Loading
Loading
@@ -69,6 +69,12 @@
= f.text_field :location
= f.text_field :organization
= f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.'
%hr
%h5 Private profile
- private_profile_label = capture do
Don't display activity-related personal information on your profile
= link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile')
= f.check_box :private_profile, label: private_profile_label
.prepend-top-default.append-bottom-default
= f.submit 'Update profile settings', class: 'btn btn-success'
= link_to 'Cancel', user_path(current_user), class: 'btn btn-cancel'
Loading
Loading
Loading
Loading
@@ -23,8 +23,9 @@
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn',
title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
= link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
= icon('rss')
- if can?(current_user, :read_user_profile, @user)
= link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
= icon('rss')
- if current_user && current_user.admin?
= link_to [:admin, @user], class: 'btn btn-default', title: 'View user in admin area',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
Loading
Loading
@@ -40,10 +41,12 @@
= @user.name
 
.cover-desc.member-date
%span.middle-dot-divider
@#{@user.username}
%span.middle-dot-divider
Member since #{@user.created_at.to_date.to_s(:long)}
%p
%span.middle-dot-divider
@#{@user.username}
- if can?(current_user, :read_user_profile, @user)
%span.middle-dot-divider
Member since #{@user.created_at.to_date.to_s(:long)}
 
.cover-desc
- unless @user.public_email.blank?
Loading
Loading
@@ -78,30 +81,31 @@
%p.profile-user-bio
= @user.bio
 
.scrolling-tabs-container
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs
- if profile_tab?(:activity)
%li.js-activity-tab
= link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
Activity
- if profile_tab?(:groups)
%li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
Groups
- if profile_tab?(:contributed)
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
Contributed projects
- if profile_tab?(:projects)
%li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
Personal projects
- if profile_tab?(:snippets)
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
Snippets
- unless profile_tabs.empty?
.scrolling-tabs-container
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs
- if profile_tab?(:activity)
%li.js-activity-tab
= link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
Activity
- if profile_tab?(:groups)
%li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
Groups
- if profile_tab?(:contributed)
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
Contributed projects
- if profile_tab?(:projects)
%li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
Personal projects
- if profile_tab?(:snippets)
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
Snippets
 
%div{ class: container_class }
.tab-content
Loading
Loading
@@ -137,3 +141,13 @@
 
.loading-status
= spinner
- if profile_tabs.empty?
.row
.col-12
.svg-content
= image_tag 'illustrations/profile_private_mode.svg'
.col-12.text-center
.text-content
%h4
This user has a private profile
---
title: Add an option to have a private profile on GitLab.
merge_request: 20387
author: jxterry
type: added
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddPrivateProfileToUsers < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :users, :private_profile, :boolean
end
end
Loading
Loading
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
 
ActiveRecord::Schema.define(version: 20180704204006) do
ActiveRecord::Schema.define(version: 20180722103201) do
 
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Loading
Loading
@@ -2124,6 +2124,7 @@ ActiveRecord::Schema.define(version: 20180704204006) do
t.integer "theme_id", limit: 2
t.integer "accepted_term_id"
t.string "feed_token"
t.boolean "private_profile"
end
 
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
Loading
Loading
Loading
Loading
@@ -105,7 +105,8 @@ GET /users
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
"external": false
"external": false,
"private_profile": false
},
{
"id": 2,
Loading
Loading
@@ -135,7 +136,8 @@ GET /users
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
"external": false
"external": false,
"private_profile": false
}
]
```
Loading
Loading
@@ -248,7 +250,8 @@ Parameters:
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
"external": false
"external": false,
"private_profile": false
}
```
 
Loading
Loading
@@ -288,6 +291,7 @@ Parameters:
- `skip_confirmation` (optional) - Skip confirmation - true or false (default)
- `external` (optional) - Flags the user as external - true or false(default)
- `avatar` (optional) - Image file for user's avatar
- `private_profile (optional) - User's profile is private - true or false
 
## User modification
 
Loading
Loading
@@ -318,6 +322,7 @@ Parameters:
- `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default)
- `external` (optional) - Flags the user as external - true or false(default)
- `avatar` (optional) - Image file for user's avatar
- `private_profile (optional) - User's profile is private - true or false
 
On password update, user will be forced to change it upon next login.
Note, at the moment this method does only return a `404` error,
Loading
Loading
@@ -382,7 +387,8 @@ GET /user
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
"external": false
"external": false,
"private_profile": false
}
```
 
Loading
Loading
@@ -429,7 +435,8 @@ GET /user
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
"external": false
"external": false,
"private_profile": false
}
```
 
Loading
Loading
Loading
Loading
@@ -68,6 +68,28 @@ Alternatively, you can follow [this detailed procedure from the GitLab Team Hand
which also covers the case where you have projects hosted with
[GitLab Pages](../project/pages/index.md).
 
## Private profile
The following information will be hidden from the user profile page (https://gitlab.example.com/username) if this feature is enabled:
- Atom feed
- Date when account is created
- Activity tab
- Groups tab
- Contributed projects tab
- Personal projects tab
- Snippets tab
To enable private profile:
1. Navigate to your personal [profile settings](#profile-settings).
1. Check the "Private profile" option.
1. Hit **Update profile settings**.
NOTE: **Note:**
You and GitLab admins can see your the abovementioned information on your profile even if it is private.
## Troubleshooting
 
### Why do I keep getting signed out?
Loading
Loading
Loading
Loading
@@ -30,7 +30,7 @@ module API
end
 
class User < UserBasic
expose :created_at
expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) }
expose :bio, :location, :skype, :linkedin, :twitter, :website_url, :organization
end
 
Loading
Loading
@@ -55,6 +55,7 @@ module API
expose :can_create_project?, as: :can_create_project
expose :two_factor_enabled?, as: :two_factor_enabled
expose :external
expose :private_profile
end
 
class UserWithAdmin < UserPublic
Loading
Loading
Loading
Loading
@@ -12,7 +12,7 @@ module API
 
key = Key.find(params[:id])
 
present key, with: Entities::SSHKeyWithUser
present key, with: Entities::SSHKeyWithUser, current_user: current_user
end
end
end
Loading
Loading
Loading
Loading
@@ -42,6 +42,7 @@ module API
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
optional :avatar, type: File, desc: 'Avatar image for user'
optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile'
optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user'
all_or_none_of :extern_uid, :provider
end
Loading
Loading
@@ -97,7 +98,7 @@ module API
 
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
users, options = with_custom_attributes(users, with: entity)
users, options = with_custom_attributes(users, { with: entity, current_user: current_user })
 
present paginate(users), options
end
Loading
Loading
@@ -114,7 +115,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user && can?(current_user, :read_user, user)
 
opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : { with: Entities::User }
opts = { with: current_user&.admin? ? Entities::UserWithAdmin : Entities::User, current_user: current_user }
user, opts = with_custom_attributes(user, opts)
 
present user, opts
Loading
Loading
@@ -140,7 +141,7 @@ module API
user = ::Users::CreateService.new(current_user, params).execute(skip_authorization: true)
 
if user.persisted?
present user, with: Entities::UserPublic
present user, with: Entities::UserPublic, current_user: current_user
else
conflict!('Email has already been taken') if User
.where(email: user.email)
Loading
Loading
@@ -199,7 +200,7 @@ module API
result = ::Users::UpdateService.new(current_user, user_params.except(:extern_uid, :provider).merge(user: user)).execute
 
if result[:status] == :success
present user, with: Entities::UserPublic
present user, with: Entities::UserPublic, current_user: current_user
else
render_validation_error!(user)
end
Loading
Loading
@@ -546,7 +547,7 @@ module API
Entities::UserPublic
end
 
present current_user, with: entity
present current_user, with: entity, current_user: current_user
end
end
 
Loading
Loading
Loading
Loading
@@ -2,6 +2,8 @@ require 'spec_helper'
 
describe UsersController do
let(:user) { create(:user) }
let(:private_user) { create(:user, private_profile: true) }
let(:public_user) { create(:user) }
 
describe 'GET #show' do
context 'with rendered views' do
Loading
Loading
@@ -98,16 +100,47 @@ describe UsersController do
 
expect(assigns(:events)).to be_empty
end
it 'hides events if the user has a private profile' do
Gitlab::DataBuilder::Push.build_sample(project, private_user)
get :show, username: private_user.username, format: :json
expect(assigns(:events)).to be_empty
end
end
end
 
describe 'GET #calendar' do
it 'renders calendar' do
sign_in(user)
context 'for user' do
let(:project) { create(:project) }
before do
sign_in(user)
project.add_developer(user)
end
context 'with public profile' do
it 'renders calendar' do
push_data = Gitlab::DataBuilder::Push.build_sample(project, public_user)
EventCreateService.new.push(project, public_user, push_data)
get :calendar, username: public_user.username, format: :json
 
get :calendar, username: user.username, format: :json
expect(response).to have_gitlab_http_status(200)
end
end
context 'with private profile' do
it 'does not render calendar' do
push_data = Gitlab::DataBuilder::Push.build_sample(project, private_user)
EventCreateService.new.push(project, private_user, push_data)
 
expect(response).to have_gitlab_http_status(200)
get :calendar, username: private_user.username, format: :json
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
 
context 'forked project' do
Loading
Loading
@@ -150,9 +183,26 @@ describe UsersController do
expect(assigns(:calendar_date)).to eq(Date.parse('2014-07-31'))
end
 
it 'renders calendar_activities' do
get :calendar_activities, username: user.username
expect(response).to render_template('calendar_activities')
context 'for user' do
context 'with public profile' do
it 'renders calendar_activities' do
push_data = Gitlab::DataBuilder::Push.build_sample(project, public_user)
EventCreateService.new.push(project, public_user, push_data)
get :calendar_activities, username: public_user.username
expect(assigns[:events]).not_to be_empty
end
end
context 'with private profile' do
it 'does not render calendar_activities' do
push_data = Gitlab::DataBuilder::Push.build_sample(project, private_user)
EventCreateService.new.push(project, private_user, push_data)
get :calendar_activities, username: private_user.username
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
 
Loading
Loading
Loading
Loading
@@ -3,15 +3,53 @@ require 'spec_helper'
describe 'User page' do
let(:user) { create(:user) }
 
it 'shows all the tabs' do
visit(user_path(user))
page.within '.nav-links' do
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')
context 'with public profile' do
it 'shows all the tabs' do
visit(user_path(user))
page.within '.nav-links' do
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
it 'does not show private profile message' do
visit(user_path(user))
expect(page).not_to have_content("This user has a private profile")
end
end
context 'with private profile' do
let(:user) { create(:user, private_profile: true) }
it 'shows no tab' do
visit(user_path(user))
expect(page).to have_css("div.profile-header")
expect(page).not_to have_css("ul.nav-links")
end
it 'shows private profile message' do
visit(user_path(user))
expect(page).to have_content("This user has a private profile")
end
it 'shows own tabs' do
sign_in(user)
visit(user_path(user))
page.within '.nav-links' do
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
 
Loading
Loading
Loading
Loading
@@ -29,11 +29,22 @@ describe UserRecentEventsFinder do
public_project.add_developer(current_user)
end
 
it 'returns all the events' do
expect(finder.execute).to include(private_event, internal_event, public_event)
context 'when profile is public' do
it 'returns all the events' do
expect(finder.execute).to include(private_event, internal_event, public_event)
end
end
context 'when profile is private' do
it 'returns no event' do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, project_owner).and_return(false)
expect(finder.execute).to be_empty
end
end
 
it 'does not include the events if the user cannot read cross project' do
expect(Ability).to receive(:allowed?).and_call_original
expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false }
expect(finder.execute).to be_empty
end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment