GitLab wurde erfolgreich aktualisiert. Durch regelmäßige Updates bleibt das THM GitLab sicher. Danke für Ihre Geduld.

Commit 99011a61 authored by JX Terry's avatar JX Terry Committed by Douwe Maan

Add an option to have a private profile on GitLab

parent adc327d3
......@@ -99,7 +99,8 @@ def user_params
: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 @@ def load_snippets
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 @@ def initialize(user, params = {})
#
# 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 @@ def initialize(current_user, target_user, params = {})
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 @@ def max_project_member_access_cache_key(project)
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 @@ def admin_create_params
: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 @@
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 @@ class UserBasic < UserSafe
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 @@ class UserPublic < User
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 @@ class Keys < Grape::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 @@ def reorder_users(users)
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 @@ def reorder_users(users)
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 @@ def reorder_users(users)
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 @@ def reorder_users(users)
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 @@ def reorder_users(users)
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 @@ def find_impersonation_token
Entities::UserPublic
end
present current_user, with: entity
present current_user, with: entity, current_user: current_user
end
end
......
......@@ -2,6 +2,8 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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) { (