Commit 4de8a3da authored by Douwe Maan's avatar Douwe Maan

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
......@@ -99,7 +99,8 @@ class ProfilesController < Profiles::ApplicationController
:username,
:website_url,
:organization,
:preferred_language
:preferred_language,
:private_profile
)
end
end
......@@ -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|
......@@ -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
......@@ -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
......
......@@ -7,6 +7,7 @@
class UserRecentEventsFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include Gitlab::Allowable
requires_cross_project_access
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......@@ -64,7 +64,8 @@ module Users
:theme_id,
:twitter,
:username,
:website_url
:website_url,
:private_profile
]
end
......
......@@ -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'
......
......@@ -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
......@@ -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?
......@@ -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
......@@ -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
......@@ -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"
......@@ -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
......
......@@ -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,
......@@ -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
}
]
```
......@@ -248,7 +250,8 @@ Parameters:
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
"external": false
"external": false,
"private_profile": false
}
```
......@@ -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
......@@ -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,
......@@ -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
}
```
......@@ -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
}
```
......
......@@ -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?
......
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......@@ -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
......@@ -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)
......@@ -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
......@@ -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
......
......@@ -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
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -25,8 +25,20 @@ describe UsersHelper do
allow(helper).to receive(:can?).and_return(true)
end
it 'includes all the expected tabs' do
expect(tabs).to include(:activity, :groups, :contributed, :projects, :snippets)
context 'with public profile' do
it 'includes all the expected tabs' do
expect(tabs).to include(:activity, :groups, :contributed, :projects, :snippets)
end
end
context 'with private profile' do
before do
allow(helper).to receive(:can?).with(user, :read_user_profile, nil).and_return(false)
end
it 'is empty' do
expect(tabs).to be_empty
end
end
end
......
......@@ -11,6 +11,7 @@ describe API::Users do
let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 }
let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 }
let(:private_user) { create(:user, private_profile: true) }
describe 'GET /users' do
context "when unauthenticated" do
......@@ -254,6 +255,13 @@ describe API::Users do
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response['is_admin']).to be(false)
end
it "includes the `created_at` field for private users" do
get api("/users/#{private_user.id}", admin)
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response.keys).to include 'created_at'
end
end
context 'for an anonymous user' do
......@@ -272,6 +280,20 @@ describe API::Users do
expect(response).to have_gitlab_http_status(404)
end
it "returns the `created_at` field for public users" do
get api("/users/#{user.id}")
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).to include 'created_at'
end
it "does not return the `created_at` field for private users" do
get api("/users/#{private_user.id}")
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).not_to include 'created_at'
end
end
it "returns a 404 error if user id not found" do
......@@ -374,6 +396,18 @@ describe API::Users do
expect(new_user.recently_sent_password_reset?).to eq(true)
end
it "creates user with private profile" do
post api('/users', admin), attributes_for(:user, private_profile: true)
expect(response).to have_gitlab_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
expect(new_user).not_to eq(nil)
expect(new_user.private_profile?).to eq(true)
end
it "does not create user with invalid email" do
post api('/users', admin),
email: 'invalid email',
......@@ -583,6 +617,13 @@ describe API::Users do
expect(user.reload.external?).to be_truthy
end
it "updates private profile" do
put api("/users/#{user.id}", admin), { private_profile: true }
expect(response).to have_gitlab_http_status(200)
expect(user.reload.private_profile).to eq(true)
end
it "does not update admin status" do
put api("/users/#{admin_user.id}", admin), { can_create_group: 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