Commit fa17712c56ace6aa6bb83a75b4f11606549870ce

Authored by Dmitriy Zaporozhets
2 parents 9ad5d9a4 fdc23a93

Merge pull request #3525 from karlhungus/api_sudo

API: admin users can sudo commands as other users
app/models/user.rb
@@ -190,6 +190,14 @@ class User < ActiveRecord::Base @@ -190,6 +190,14 @@ class User < ActiveRecord::Base
190 def search query 190 def search query
191 where("name LIKE :query OR email LIKE :query OR username LIKE :query", query: "%#{query}%") 191 where("name LIKE :query OR email LIKE :query OR username LIKE :query", query: "%#{query}%")
192 end 192 end
  193 +
  194 + def by_username_or_id(name_or_id)
  195 + if (name_or_id.is_a?(Integer))
  196 + User.find_by_id(name_or_id)
  197 + else
  198 + User.find_by_username(name_or_id)
  199 + end
  200 + end
193 end 201 end
194 202
195 # 203 #
doc/api/README.md
@@ -58,7 +58,43 @@ Return values: @@ -58,7 +58,43 @@ Return values:
58 * `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists 58 * `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists
59 * `500 Server Error` - While handling the request something went wrong on the server side 59 * `500 Server Error` - While handling the request something went wrong on the server side
60 60
  61 +## Sudo
  62 +All API requests support performing an api call as if you were another user, if your private token is for an administration account. You need to pass `sudo` parameter by url or header with an id or username of the user you want to perform the operation as. If passed as header, the header name must be "SUDO" (capitals).
61 63
  64 +If a non administrative `private_token` is provided then an error message will be returned with status code 403:
  65 +
  66 +```json
  67 +{
  68 + "message": "403 Forbidden: Must be admin to use sudo"
  69 +}
  70 +```
  71 +
  72 +If the sudo user id or username cannot be found then an error message will be returned with status code 404:
  73 +
  74 +```json
  75 +{
  76 + "message": "404 Not Found: No user id or username for: <id/username>"
  77 +}
  78 +```
  79 +
  80 +Example of a valid API with sudo request:
  81 +
  82 +```
  83 +GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U&sudo=username
  84 +```
  85 +```
  86 +GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U&sudo=23
  87 +```
  88 +
  89 +
  90 +Example for a valid API request with sudo using curl and authentication via header:
  91 +
  92 +```
  93 +curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" --header "SUDO: username" "http://example.com/api/v3/projects"
  94 +```
  95 +```
  96 +curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" --header "SUDO: 23" "http://example.com/api/v3/projects"
  97 +```
62 98
63 #### Pagination 99 #### Pagination
64 100
lib/api/helpers.rb
1 module API 1 module API
2 module APIHelpers 2 module APIHelpers
  3 + PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
  4 + PRIVATE_TOKEN_PARAM = :private_token
  5 + SUDO_HEADER ="HTTP_SUDO"
  6 + SUDO_PARAM = :sudo
  7 +
3 def current_user 8 def current_user
4 - @current_user ||= User.find_by_authentication_token(params[:private_token] || env["HTTP_PRIVATE_TOKEN"]) 9 + @current_user ||= User.find_by_authentication_token(params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER])
  10 + identifier = sudo_identifier()
  11 + # If the sudo is the current user do nothing
  12 + if (identifier && !(@current_user.id == identifier || @current_user.username == identifier))
  13 + render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin?
  14 + begin
  15 + @current_user = User.by_username_or_id(identifier)
  16 + rescue => ex
  17 + not_found!("No user id or username for: #{identifier}")
  18 + end
  19 + not_found!("No user id or username for: #{identifier}") if current_user.nil?
  20 + end
  21 + @current_user
  22 + end
  23 +
  24 + def sudo_identifier()
  25 + identifier ||= params[SUDO_PARAM] ||= env[SUDO_HEADER]
  26 + # Regex for integers
  27 + if (!!(identifier =~ /^[0-9]+$/))
  28 + identifier.to_i
  29 + else
  30 + identifier
  31 + end
5 end 32 end
6 33
7 def user_project 34 def user_project
spec/models/user_spec.rb
@@ -208,4 +208,14 @@ describe User do @@ -208,4 +208,14 @@ describe User do
208 user.can_create_group.should == false 208 user.can_create_group.should == false
209 end 209 end
210 end 210 end
  211 +
  212 + describe 'by_username_or_id' do
  213 + let(:user1){create(:user, username: 'foo')}
  214 + it "should get the correct user" do
  215 + User.by_username_or_id(user1.id).should == user1
  216 + User.by_username_or_id('foo').should == user1
  217 + User.by_username_or_id(-1).should be_nil
  218 + User.by_username_or_id('bar').should be_nil
  219 + end
  220 + end
211 end 221 end
spec/requests/api/api_helpers_spec.rb 0 → 100644
@@ -0,0 +1,161 @@ @@ -0,0 +1,161 @@
  1 +require 'spec_helper'
  2 +
  3 +describe API do
  4 + include API::APIHelpers
  5 + include ApiHelpers
  6 + let(:user) { create(:user) }
  7 + let(:admin) { create(:admin) }
  8 + let(:key) { create(:key, user: user) }
  9 +
  10 + let(:params) { {} }
  11 + let(:env) { {} }
  12 +
  13 + def set_env(token_usr, identifier)
  14 + clear_env
  15 + clear_param
  16 + env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token
  17 + env[API::APIHelpers::SUDO_HEADER] = identifier
  18 + end
  19 +
  20 + def set_param(token_usr, identifier)
  21 + clear_env
  22 + clear_param
  23 + params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token
  24 + params[API::APIHelpers::SUDO_PARAM] = identifier
  25 + end
  26 +
  27 + def clear_env
  28 + env.delete(API::APIHelpers::PRIVATE_TOKEN_HEADER)
  29 + env.delete(API::APIHelpers::SUDO_HEADER)
  30 + end
  31 +
  32 + def clear_param
  33 + params.delete(API::APIHelpers::PRIVATE_TOKEN_PARAM)
  34 + params.delete(API::APIHelpers::SUDO_PARAM)
  35 + end
  36 +
  37 + def error!(message, status)
  38 + raise Exception
  39 + end
  40 +
  41 + describe ".current_user" do
  42 + it "should leave user as is when sudo not specified" do
  43 + env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token
  44 + current_user.should == user
  45 + clear_env
  46 + params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = user.private_token
  47 + current_user.should == user
  48 + end
  49 +
  50 + it "should change current user to sudo when admin" do
  51 + set_env(admin, user.id)
  52 + current_user.should == user
  53 + set_param(admin, user.id)
  54 + current_user.should == user
  55 + set_env(admin, user.username)
  56 + current_user.should == user
  57 + set_param(admin, user.username)
  58 + current_user.should == user
  59 + end
  60 +
  61 + it "should throw an error when the current user is not an admin and attempting to sudo" do
  62 + set_env(user, admin.id)
  63 + expect { current_user }.to raise_error
  64 + set_param(user, admin.id)
  65 + expect { current_user }.to raise_error
  66 + set_env(user, admin.username)
  67 + expect { current_user }.to raise_error
  68 + set_param(user, admin.username)
  69 + expect { current_user }.to raise_error
  70 + end
  71 +
  72 + it "should throw an error when the user cannot be found for a given id" do
  73 + id = user.id + admin.id
  74 + user.id.should_not == id
  75 + admin.id.should_not == id
  76 + set_env(admin, id)
  77 + expect { current_user }.to raise_error
  78 +
  79 + set_param(admin, id)
  80 + expect { current_user }.to raise_error
  81 + end
  82 +
  83 + it "should throw an error when the user cannot be found for a given username" do
  84 + username = "#{user.username}#{admin.username}"
  85 + user.username.should_not == username
  86 + admin.username.should_not == username
  87 + set_env(admin, username)
  88 + expect { current_user }.to raise_error
  89 +
  90 + set_param(admin, username)
  91 + expect { current_user }.to raise_error
  92 + end
  93 +
  94 + it "should handle sudo's to oneself" do
  95 + set_env(admin, admin.id)
  96 + current_user.should == admin
  97 + set_param(admin, admin.id)
  98 + current_user.should == admin
  99 + set_env(admin, admin.username)
  100 + current_user.should == admin
  101 + set_param(admin, admin.username)
  102 + current_user.should == admin
  103 + end
  104 +
  105 + it "should handle multiple sudo's to oneself" do
  106 + set_env(admin, user.id)
  107 + current_user.should == user
  108 + current_user.should == user
  109 + set_env(admin, user.username)
  110 + current_user.should == user
  111 + current_user.should == user
  112 +
  113 + set_param(admin, user.id)
  114 + current_user.should == user
  115 + current_user.should == user
  116 + set_param(admin, user.username)
  117 + current_user.should == user
  118 + current_user.should == user
  119 + end
  120 +
  121 + it "should handle multiple sudo's to oneself using string ids" do
  122 + set_env(admin, user.id.to_s)
  123 + current_user.should == user
  124 + current_user.should == user
  125 +
  126 + set_param(admin, user.id.to_s)
  127 + current_user.should == user
  128 + current_user.should == user
  129 + end
  130 + end
  131 +
  132 + describe '.sudo_identifier' do
  133 + it "should return integers when input is an int" do
  134 + set_env(admin, '123')
  135 + sudo_identifier.should == 123
  136 + set_env(admin, '0001234567890')
  137 + sudo_identifier.should == 1234567890
  138 +
  139 + set_param(admin, '123')
  140 + sudo_identifier.should == 123
  141 + set_param(admin, '0001234567890')
  142 + sudo_identifier.should == 1234567890
  143 + end
  144 +
  145 + it "should return string when input is an is not an int" do
  146 + set_env(admin, '12.30')
  147 + sudo_identifier.should == "12.30"
  148 + set_env(admin, 'hello')
  149 + sudo_identifier.should == 'hello'
  150 + set_env(admin, ' 123')
  151 + sudo_identifier.should == ' 123'
  152 +
  153 + set_param(admin, '12.30')
  154 + sudo_identifier.should == "12.30"
  155 + set_param(admin, 'hello')
  156 + sudo_identifier.should == 'hello'
  157 + set_param(admin, ' 123')
  158 + sudo_identifier.should == ' 123'
  159 + end
  160 + end
  161 +end
0 \ No newline at end of file 162 \ No newline at end of file