adds impersonator variable and makes sudo usage overall more clear
This commit is contained in:
parent
8b379465a5
commit
3ed96afc47
10 changed files with 389 additions and 99 deletions
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Reenables /user API request to return private-token if user is admin and request
|
||||
is made with sudo
|
||||
merge_request: 7615
|
||||
author:
|
|
@ -291,7 +291,9 @@ Parameters:
|
|||
|
||||
- `id` (required) - The ID of the user
|
||||
|
||||
## Current user
|
||||
## User
|
||||
|
||||
### For normal users
|
||||
|
||||
Gets currently authenticated user.
|
||||
|
||||
|
@ -335,6 +337,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
|
||||
|
||||
Get a list of currently authenticated user's SSH keys.
|
||||
|
|
|
@ -22,7 +22,7 @@ module API
|
|||
expose :provider, :extern_uid
|
||||
end
|
||||
|
||||
class UserFull < User
|
||||
class UserPublic < User
|
||||
expose :last_sign_in_at
|
||||
expose :confirmed_at
|
||||
expose :email
|
||||
|
@ -32,10 +32,9 @@ module API
|
|||
expose :can_create_project?, as: :can_create_project
|
||||
expose :two_factor_enabled?, as: :two_factor_enabled
|
||||
expose :external
|
||||
expose :private_token, if: lambda { |user, options| user.is_admin? && options[:sudo_identifier] }
|
||||
end
|
||||
|
||||
class UserLogin < UserFull
|
||||
class UserWithPrivateToken < UserPublic
|
||||
expose :private_token
|
||||
end
|
||||
|
||||
|
@ -290,7 +289,7 @@ module API
|
|||
end
|
||||
|
||||
class SSHKeyWithUser < SSHKey
|
||||
expose :user, using: Entities::UserFull
|
||||
expose :user, using: Entities::UserPublic
|
||||
end
|
||||
|
||||
class Note < Grape::Entity
|
||||
|
|
|
@ -44,11 +44,14 @@ module API
|
|||
return nil
|
||||
end
|
||||
|
||||
identifier = sudo_identifier()
|
||||
identifier = sudo_identifier
|
||||
|
||||
# If the sudo is the current user do nothing
|
||||
if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
|
||||
if 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!('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)
|
||||
not_found!("No user id or username for: #{identifier}") if @current_user.nil?
|
||||
end
|
||||
|
@ -399,6 +402,10 @@ module API
|
|||
links.join(', ')
|
||||
end
|
||||
|
||||
def private_token_used?
|
||||
private_token == @current_user.private_token
|
||||
end
|
||||
|
||||
def secret_token
|
||||
Gitlab::Shell.secret_token
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module API
|
||||
class Session < Grape::API
|
||||
desc 'Login to get token' do
|
||||
success Entities::UserLogin
|
||||
success Entities::UserWithPrivateToken
|
||||
end
|
||||
params do
|
||||
optional :login, type: String, desc: 'The username'
|
||||
|
@ -14,7 +14,7 @@ module API
|
|||
|
||||
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?
|
||||
present user, with: Entities::UserLogin
|
||||
present user, with: Entities::UserWithPrivateToken
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -51,7 +51,7 @@ module API
|
|||
users = users.external if params[:external] && current_user.is_admin?
|
||||
end
|
||||
|
||||
entity = current_user.is_admin? ? Entities::UserFull : Entities::UserBasic
|
||||
entity = current_user.is_admin? ? Entities::UserPublic : Entities::UserBasic
|
||||
present paginate(users), with: entity
|
||||
end
|
||||
|
||||
|
@ -66,7 +66,7 @@ module API
|
|||
not_found!('User') unless user
|
||||
|
||||
if current_user && current_user.is_admin?
|
||||
present user, with: Entities::UserFull
|
||||
present user, with: Entities::UserPublic
|
||||
elsif can?(current_user, :read_user, user)
|
||||
present user, with: Entities::User
|
||||
else
|
||||
|
@ -75,7 +75,7 @@ module API
|
|||
end
|
||||
|
||||
desc 'Create a user. Available only for admins.' do
|
||||
success Entities::UserFull
|
||||
success Entities::UserPublic
|
||||
end
|
||||
params do
|
||||
requires :email, type: String, desc: 'The email of the user'
|
||||
|
@ -99,7 +99,7 @@ module API
|
|||
end
|
||||
|
||||
if user.save
|
||||
present user, with: Entities::UserFull
|
||||
present user, with: Entities::UserPublic
|
||||
else
|
||||
conflict!('Email has already been taken') if User.
|
||||
where(email: user.email).
|
||||
|
@ -114,7 +114,7 @@ module API
|
|||
end
|
||||
|
||||
desc 'Update a user. Available only for admins.' do
|
||||
success Entities::UserFull
|
||||
success Entities::UserPublic
|
||||
end
|
||||
params do
|
||||
requires :id, type: Integer, desc: 'The ID of the user'
|
||||
|
@ -161,7 +161,7 @@ module API
|
|||
user_params.delete(:provider)
|
||||
|
||||
if user.update_attributes(user_params)
|
||||
present user, with: Entities::UserFull
|
||||
present user, with: Entities::UserPublic
|
||||
else
|
||||
render_validation_error!(user)
|
||||
end
|
||||
|
@ -350,10 +350,10 @@ module API
|
|||
|
||||
resource :user do
|
||||
desc 'Get the currently authenticated user' do
|
||||
success Entities::UserFull
|
||||
success Entities::UserPublic
|
||||
end
|
||||
get do
|
||||
present current_user, with: Entities::UserFull, sudo_identifier: sudo_identifier
|
||||
present current_user, with: @impersonator ? Entities::UserWithPrivateToken : Entities::UserPublic
|
||||
end
|
||||
|
||||
desc "Get the currently authenticated user's SSH keys" do
|
||||
|
|
37
spec/fixtures/api/schemas/user/login.json
vendored
Normal file
37
spec/fixtures/api/schemas/user/login.json
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"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" }
|
||||
}
|
||||
}
|
79
spec/fixtures/api/schemas/user/public.json
vendored
Normal file
79
spec/fixtures/api/schemas/user/public.json
vendored
Normal file
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"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
|
|||
end
|
||||
end
|
||||
|
||||
it "changes current user to sudo when admin" do
|
||||
set_env(admin, user.id)
|
||||
expect(current_user).to eq(user)
|
||||
set_param(admin, user.id)
|
||||
expect(current_user).to eq(user)
|
||||
set_env(admin, user.username)
|
||||
expect(current_user).to eq(user)
|
||||
set_param(admin, user.username)
|
||||
expect(current_user).to eq(user)
|
||||
end
|
||||
context 'sudo usage' do
|
||||
context 'with admin' do
|
||||
context 'with header' do
|
||||
context 'with id' do
|
||||
it 'changes current_user to sudo' do
|
||||
set_env(admin, user.id)
|
||||
|
||||
it "throws an error when the current user is not an admin and attempting to sudo" do
|
||||
set_env(user, admin.id)
|
||||
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
|
||||
expect(current_user).to eq(user)
|
||||
end
|
||||
|
||||
it "throws an error when the user cannot be found for a given id" do
|
||||
id = user.id + 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)
|
||||
it 'handles sudo to oneself' do
|
||||
set_env(admin, admin.id)
|
||||
|
||||
set_param(admin, id)
|
||||
expect { current_user }.to raise_error(Exception)
|
||||
end
|
||||
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_env(admin, username)
|
||||
expect { current_user }.to raise_error(Exception)
|
||||
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)
|
||||
|
||||
set_param(admin, username)
|
||||
expect { current_user }.to raise_error(Exception)
|
||||
end
|
||||
set_env(admin, id)
|
||||
|
||||
it "handles sudo's to oneself" do
|
||||
set_env(admin, admin.id)
|
||||
expect(current_user).to eq(admin)
|
||||
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
|
||||
expect { current_user }.to raise_error(Exception)
|
||||
end
|
||||
end
|
||||
|
||||
it "handles multiple sudo's to oneself" do
|
||||
set_env(admin, user.id)
|
||||
expect(current_user).to eq(user)
|
||||
expect(current_user).to eq(user)
|
||||
set_env(admin, user.username)
|
||||
expect(current_user).to eq(user)
|
||||
expect(current_user).to eq(user)
|
||||
context 'with username' do
|
||||
it 'changes current_user to sudo' do
|
||||
set_env(admin, user.username)
|
||||
|
||||
set_param(admin, user.id)
|
||||
expect(current_user).to eq(user)
|
||||
expect(current_user).to eq(user)
|
||||
set_param(admin, user.username)
|
||||
expect(current_user).to eq(user)
|
||||
expect(current_user).to eq(user)
|
||||
end
|
||||
expect(current_user).to eq(user)
|
||||
end
|
||||
|
||||
it "handles multiple sudo's to oneself using string ids" do
|
||||
set_env(admin, user.id.to_s)
|
||||
expect(current_user).to eq(user)
|
||||
expect(current_user).to eq(user)
|
||||
it 'handles sudo to oneself' do
|
||||
set_env(admin, admin.username)
|
||||
|
||||
set_param(admin, user.id.to_s)
|
||||
expect(current_user).to eq(user)
|
||||
expect(current_user).to eq(user)
|
||||
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_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)
|
||||
|
||||
set_param(admin, id)
|
||||
|
||||
expect { current_user }.to raise_error(Exception)
|
||||
end
|
||||
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
|
||||
|
||||
|
|
|
@ -651,20 +651,75 @@ describe API::Users, api: true do
|
|||
end
|
||||
|
||||
describe "GET /user" do
|
||||
it "returns current user" do
|
||||
get api("/user", user)
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response['email']).to eq(user.email)
|
||||
expect(json_response['is_admin']).to eq(user.is_admin?)
|
||||
expect(json_response['can_create_project']).to eq(user.can_create_project?)
|
||||
expect(json_response['can_create_group']).to eq(user.can_create_group?)
|
||||
expect(json_response['projects_limit']).to eq(user.projects_limit)
|
||||
expect(json_response['private_token']).to be_blank
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let(:private_token) { user.private_token }
|
||||
|
||||
context 'with regular user' do
|
||||
context 'with personal access token' do
|
||||
it 'returns 403 without private token when sudo is defined' do
|
||||
get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}")
|
||||
|
||||
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
|
||||
|
||||
it "returns 401 error if user is unauthenticated" do
|
||||
get api("/user")
|
||||
expect(response).to have_http_status(401)
|
||||
context 'with admin' do
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
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
|
||||
|
||||
|
|
Loading…
Reference in a new issue