Commit 45ab743a authored by Douwe Maan's avatar Douwe Maan Committed by Rémy Coutable

Merge branch '24537-reenable-private-token-with-sudo' into 'master'

Reenables the API /users to return `private-token` when sudo is either a parameter or passed as a header and the user is admin.

Closes #24537

See merge request !7615
Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent ead32558
---
title: Reenables /user API request to return private-token if user is admin and request
is made with sudo
merge_request: 7615
author:
...@@ -277,7 +277,9 @@ Parameters: ...@@ -277,7 +277,9 @@ Parameters:
- `id` (required) - The ID of the user - `id` (required) - The ID of the user
## Current user ## User
### For normal users
Gets currently authenticated user. Gets currently authenticated user.
...@@ -321,6 +323,53 @@ GET /user ...@@ -321,6 +323,53 @@ GET /user
} }
``` ```
### For admins
Parameters:
- `sudo` (required) - the ID of a user
```
GET /user
```
```json
{
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z",
"is_admin": false,
"bio": null,
"location": null,
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
"identities": [
{"provider": "github", "extern_uid": "2435223452345"},
{"provider": "bitbucket", "extern_uid": "john_smith"},
{"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"}
],
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
"external": false,
"private_token": "dd34asd13as"
}
```
## List SSH keys ## List SSH keys
Get a list of currently authenticated user's SSH keys. Get a list of currently authenticated user's SSH keys.
......
...@@ -22,7 +22,7 @@ module API ...@@ -22,7 +22,7 @@ module API
expose :provider, :extern_uid expose :provider, :extern_uid
end end
class UserFull < User class UserPublic < User
expose :last_sign_in_at expose :last_sign_in_at
expose :confirmed_at expose :confirmed_at
expose :email expose :email
...@@ -34,7 +34,7 @@ module API ...@@ -34,7 +34,7 @@ module API
expose :external expose :external
end end
class UserLogin < UserFull class UserWithPrivateToken < UserPublic
expose :private_token expose :private_token
end end
...@@ -283,7 +283,7 @@ module API ...@@ -283,7 +283,7 @@ module API
end end
class SSHKeyWithUser < SSHKey class SSHKeyWithUser < SSHKey
expose :user, using: Entities::UserFull expose :user, using: Entities::UserPublic
end end
class Note < Grape::Entity class Note < Grape::Entity
......
...@@ -39,11 +39,14 @@ module API ...@@ -39,11 +39,14 @@ module API
return nil return nil
end end
identifier = sudo_identifier() identifier = sudo_identifier
# If the sudo is the current user do nothing if identifier
if identifier && !(@current_user.id == identifier || @current_user.username == identifier) # We check for private_token because we cannot allow PAT to be used
forbidden!('Must be admin to use sudo') unless @current_user.is_admin? forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
forbidden!('Private token must be specified in order to use sudo') unless private_token_used?
@impersonator = @current_user
@current_user = User.by_username_or_id(identifier) @current_user = User.by_username_or_id(identifier)
not_found!("No user id or username for: #{identifier}") if @current_user.nil? not_found!("No user id or username for: #{identifier}") if @current_user.nil?
end end
...@@ -410,6 +413,10 @@ module API ...@@ -410,6 +413,10 @@ module API
links.join(', ') links.join(', ')
end end
def private_token_used?
private_token == @current_user.private_token
end
def secret_token def secret_token
Gitlab::Shell.secret_token Gitlab::Shell.secret_token
end end
......
...@@ -15,7 +15,7 @@ module API ...@@ -15,7 +15,7 @@ module API
return unauthorized! unless user return unauthorized! unless user
return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled? return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
present user, with: Entities::UserLogin present user, with: Entities::UserWithPrivateToken
end end
end end
end end
...@@ -25,7 +25,7 @@ module API ...@@ -25,7 +25,7 @@ module API
end end
if current_user.is_admin? if current_user.is_admin?
present @users, with: Entities::UserFull present @users, with: Entities::UserPublic
else else
present @users, with: Entities::UserBasic present @users, with: Entities::UserBasic
end end
...@@ -41,7 +41,7 @@ module API ...@@ -41,7 +41,7 @@ module API
@user = User.find(params[:id]) @user = User.find(params[:id])
if current_user && current_user.is_admin? if current_user && current_user.is_admin?
present @user, with: Entities::UserFull present @user, with: Entities::UserPublic
elsif can?(current_user, :read_user, @user) elsif can?(current_user, :read_user, @user)
present @user, with: Entities::User present @user, with: Entities::User
else else
...@@ -88,7 +88,7 @@ module API ...@@ -88,7 +88,7 @@ module API
end end
if user.save if user.save
present user, with: Entities::UserFull present user, with: Entities::UserPublic
else else
conflict!('Email has already been taken') if User. conflict!('Email has already been taken') if User.
where(email: user.email). where(email: user.email).
...@@ -151,7 +151,7 @@ module API ...@@ -151,7 +151,7 @@ module API
end end
if user.update_attributes(attrs) if user.update_attributes(attrs)
present user, with: Entities::UserFull present user, with: Entities::UserPublic
else else
render_validation_error!(user) render_validation_error!(user)
end end
...@@ -349,7 +349,7 @@ module API ...@@ -349,7 +349,7 @@ module API
# Example Request: # Example Request:
# GET /user # GET /user
get do get do
present @current_user, with: Entities::UserFull present @current_user, with: @impersonator ? Entities::UserWithPrivateToken : Entities::UserPublic
end end
# Get currently authenticated user's keys # Get currently authenticated user's keys
......
{
"type": "object",
"required": [
"id",
"username",
"email",
"name",
"state",
"avatar_url",
"web_url",
"created_at",
"is_admin",
"bio",
"location",
"skype",
"linkedin",
"twitter",
"website_url",
"organization",
"last_sign_in_at",
"confirmed_at",
"theme_id",
"color_scheme_id",
"projects_limit",
"current_sign_in_at",
"identities",
"can_create_group",
"can_create_project",
"two_factor_enabled",
"external",
"private_token"
],
"properties": {
"$ref": "full.json",
"private_token": { "type": "string" }
}
}
{
"type": "object",
"required": [
"id",
"username",
"email",
"name",
"state",
"avatar_url",
"web_url",
"created_at",
"is_admin",
"bio",
"location",
"skype",
"linkedin",
"twitter",
"website_url",
"organization",
"last_sign_in_at",
"confirmed_at",
"theme_id",
"color_scheme_id",
"projects_limit",
"current_sign_in_at",
"identities",
"can_create_group",
"can_create_project",
"two_factor_enabled",
"external"
],
"properties": {
"id": { "type": "integer" },
"username": { "type": "string" },
"email": {
"type": "string",
"pattern": "^[^@]+@[^@]+$"
},
"name": { "type": "string" },
"state": {
"type": "string",
"enum": ["active", "blocked"]
},
"avatar_url": { "type": "string" },
"web_url": { "type": "string" },
"created_at": { "type": "date" },
"is_admin": { "type": "boolean" },
"bio": { "type": ["string", "null"] },
"location": { "type": ["string", "null"] },
"skype": { "type": "string" },
"linkedin": { "type": "string" },
"twitter": { "type": "string "},
"website_url": { "type": "string" },
"organization": { "type": ["string", "null"] },
"last_sign_in_at": { "type": "date" },
"confirmed_at": { "type": ["date", "null"] },
"theme_id": { "type": "integer" },
"color_scheme_id": { "type": "integer" },
"projects_limit": { "type": "integer" },
"current_sign_in_at": { "type": "date" },
"identities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"provider": {
"type": "string",
"enum": ["github", "bitbucket", "google_oauth2"]
},
"extern_uid": { "type": ["number", "string"] }
}
}
},
"can_create_group": { "type": "boolean" },
"can_create_project": { "type": "boolean" },
"two_factor_enabled": { "type": "boolean" },
"external": { "type": "boolean" }
}
}
...@@ -153,85 +153,144 @@ describe API::Helpers, api: true do ...@@ -153,85 +153,144 @@ describe API::Helpers, api: true do
end end
end end
it "changes current user to sudo when admin" do context 'sudo usage' do
set_env(admin, user.id) context 'with admin' do
expect(current_user).to eq(user) context 'with header' do
set_param(admin, user.id) context 'with id' do
expect(current_user).to eq(user) it 'changes current_user to sudo' do
set_env(admin, user.username) set_env(admin, user.id)
expect(current_user).to eq(user)
set_param(admin, user.username)
expect(current_user).to eq(user)
end
it "throws an error when the current user is not an admin and attempting to sudo" do expect(current_user).to eq(user)
set_env(user, admin.id) end
expect { current_user }.to raise_error(Exception)
set_param(user, admin.id)
expect { current_user }.to raise_error(Exception)
set_env(user, admin.username)
expect { current_user }.to raise_error(Exception)
set_param(user, admin.username)
expect { current_user }.to raise_error(Exception)
end
it "throws an error when the user cannot be found for a given id" do it 'handles sudo to oneself' do
id = user.id + admin.id set_env(admin, admin.id)
expect(user.id).not_to eq(id)
expect(admin.id).not_to eq(id)
set_env(admin, id)
expect { current_user }.to raise_error(Exception)
set_param(admin, id) expect(current_user).to eq(admin)
expect { current_user }.to raise_error(Exception) end
end
it "throws an error when the user cannot be found for a given username" do it 'throws an error when user cannot be found' do
username = "#{user.username}#{admin.username}" id = user.id + admin.id
expect(user.username).not_to eq(username) expect(user.id).not_to eq(id)
expect(admin.username).not_to eq(username) expect(admin.id).not_to eq(id)
set_env(admin, username)
expect { current_user }.to raise_error(Exception)
set_param(admin, username) set_env(admin, id)
expect { current_user }.to raise_error(Exception)
end
it "handles sudo's to oneself" do expect { current_user }.to raise_error(Exception)
set_env(admin, admin.id) end
expect(current_user).to eq(admin) end
set_param(admin, admin.id)
expect(current_user).to eq(admin)
set_env(admin, admin.username)
expect(current_user).to eq(admin)
set_param(admin, admin.username)
expect(current_user).to eq(admin)
end
it "handles multiple sudo's to oneself" do context 'with username' do
set_env(admin, user.id) it 'changes current_user to sudo' do
expect(current_user).to eq(user) set_env(admin, user.username)
expect(current_user).to eq(user)
set_env(admin, user.username) expect(current_user).to eq(user)
expect(current_user).to eq(user) end
expect(current_user).to eq(user)
it 'handles sudo to oneself' do
set_param(admin, user.id) set_env(admin, admin.username)
expect(current_user).to eq(user)
expect(current_user).to eq(user) expect(current_user).to eq(admin)
set_param(admin, user.username) end
expect(current_user).to eq(user)
expect(current_user).to eq(user) it "throws an error when the user cannot be found for a given username" do
end username = "#{user.username}#{admin.username}"
expect(user.username).not_to eq(username)
expect(admin.username).not_to eq(username)
set_env(admin, username)
expect { current_user }.to raise_error(Exception)
end
end
end
context 'with param' do
context 'with id' do
it 'changes current_user to sudo' do
set_param(admin, user.id)
expect(current_user).to eq(user)
end
it 'handles sudo to oneself' do
set_param(admin, admin.id)
expect(current_user).to eq(admin)
end
it 'handles sudo to oneself using string' do
set_env(admin, user.id.to_s)
expect(current_user).to eq(user)
end
it 'throws an error when user cannot be found' do
id = user.id + admin.id
expect(user.id).not_to eq(id)
expect(admin.id).not_to eq(id)
it "handles multiple sudo's to oneself using string ids" do set_param(admin, id)
set_env(admin, user.id.to_s)
expect(current_user).to eq(user)
expect(current_user).to eq(user)
set_param(admin, user.id.to_s) expect { current_user }.to raise_error(Exception)
expect(current_user).to eq(user) end
expect(current_user).to eq(user) end
context 'with username' do
it 'changes current_user to sudo' do
set_param(admin, user.username)
expect(current_user).to eq(user)
end
it 'handles sudo to oneself' do
set_param(admin, admin.username)
expect(current_user).to eq(admin)
end
it "throws an error when the user cannot be found for a given username" do
username = "#{user.username}#{admin.username}"
expect(user.username).not_to eq(username)
expect(admin.username).not_to eq(username)
set_param(admin, username)
expect { current_user }.to raise_error(Exception)
end
end
end
end
context 'with regular user' do
context 'with env' do
it 'changes current_user to sudo when admin and user id' do
set_env(user, admin.id)
expect { current_user }.to raise_error(Exception)
end
it 'changes current_user to sudo when admin and user username' do
set_env(user, admin.username)
expect { current_user }.to raise_error(Exception)
end
end
context 'with params' do
it 'changes current_user to sudo when admin and user id' do
set_param(user, admin.id)
expect { current_user }.to raise_error(Exception)
end
it 'changes current_user to sudo when admin and user username' do
set_param(user, admin.username)
expect { current_user }.to raise_error(Exception)
end
end
end
end end
end end
......
...@@ -615,20 +615,75 @@ describe API::API, api: true do ...@@ -615,20 +615,75 @@ describe API::API, api: true do
end end
describe "GET /user" do describe "GET /user" do
it "returns current user" do let(:personal_access_token) { create(:personal_access_token, user: user) }
get api("/user", user) let(:private_token) { user.private_token }
expect(response).to have_http_status(200)
expect(json_response['email']).to eq(user.email) context 'with regular user' do
expect(json_response['is_admin']).to eq(user.is_admin?) context 'with personal access token' do
expect(json_response['can_create_project']).to eq(user.can_create_project?) it 'returns 403 without private token when sudo is defined' do
expect(json_response['can_create_group']).to eq(user.can_create_group?) get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}")
expect(json_response['projects_limit']).to eq(user.projects_limit)
expect(json_response['private_token']).to be_blank expect(response).to have_http_status(403)
end
end
context 'with private token' do
it 'returns 403 without private token when sudo defined' do
get api("/user?private_token=#{private_token}&sudo=#{user.id}")
expect(response).to have_http_status(403)
end
end
it 'returns current user without private token when sudo not defined' do
get api("/user", user)
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public')
end
end end
it "returns 401 error if user is unauthenticated" do context 'with admin' do
get api("/user") let(:user) { create(:admin) }
expect(response).to have_http_status(401)
context 'with personal access token' do
it 'returns 403 without private token when sudo defined' do
get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}")
expect(response).to have_http_status(403)
end
it 'returns current user without private token when sudo not defined' do
get api("/user?private_token=#{personal_access_token.token}")
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public')
end
end
context 'with private token' do
it 'returns current user with private token when sudo defined' do
get api("/user?private_token=#{private_token}&sudo=#{user.id}")
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/login')
end
it 'returns current user without private token when sudo not defined' do
get api("/user?private_token=#{private_token}")
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public')
end
end
end
context 'with unauthenticated user' do
it "returns 401 error if user is unauthenticated" do
get api("/user")
expect(response).to have_http_status(401)
end
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