diff --git a/doc/api/README.md b/doc/api/README.md index 7668df07af1..6971e08f010 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -58,7 +58,43 @@ Return values: * `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists * `500 Server Error` - While handling the request something went wrong on the server side +## Sudo +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). +If a non administrative `private_token` is provided then an error message will be returned with status code 403: + +```json +{ + "message": "403 Forbidden: Must be admin to use sudo" +} +``` + +If the sudo user id or username cannot be found then an error message will be returned with status code 404: + +```json +{ + "message": "404 Not Found: No user id or username for: " +} +``` + +Example of a valid API with sudo request: + +``` +GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U&sudo=username +``` +``` +GET http://example.com/api/v3/projects?private_token=QVy1PB7sTxfy4pqfZM1U&sudo=23 +``` + + +Example for a valid API request with sudo using curl and authentication via header: + +``` +curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" --header "SUDO: username" "http://example.com/api/v3/projects" +``` +``` +curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" --header "SUDO: 23" "http://example.com/api/v3/projects" +``` #### Pagination diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index f857d4133b2..996d3adb174 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -1,7 +1,41 @@ module API module APIHelpers + PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" + PRIVATE_TOKEN_PARAM = :private_token + SUDO_HEADER ="HTTP_SUDO" + SUDO_PARAM = :sudo + def current_user - @current_user ||= User.find_by_authentication_token(params[:private_token] || env["HTTP_PRIVATE_TOKEN"]) + @current_user ||= User.find_by_authentication_token(params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]) + identifier = sudo_identifier() + # If the sudo is the current user do nothing + if (identifier && !(@current_user.id == identifier || @current_user.username == identifier)) + render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin? + begin + + if (identifier.is_a?(Integer)) + user = User.find_by_id(identifier) + else + user = User.find_by_username(identifier) + end + if user.nil? + not_found!("No user id or username for: #{identifier}") + end + @current_user = user + rescue => ex + not_found!("No user id or username for: #{identifier}") + end + end + @current_user + end + + def sudo_identifier() + identifier = params[SUDO_PARAM] == nil ? env[SUDO_HEADER] : params[SUDO_PARAM] + if (!!(identifier =~ /^[0-9]+$/)) + identifier.to_i + else + identifier + end end def user_project @@ -95,10 +129,10 @@ module API def abilities @abilities ||= begin - abilities = Six.new - abilities << Ability - abilities - end + abilities = Six.new + abilities << Ability + abilities + end end end end diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb new file mode 100644 index 00000000000..de71deabb8e --- /dev/null +++ b/spec/requests/api/api_helpers_spec.rb @@ -0,0 +1,159 @@ +require 'spec_helper' + +describe Gitlab::API do + include Gitlab::APIHelpers + include ApiHelpers + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:key) { create(:key, user: user) } + + let(:params) { {} } + let(:env) { {} } + + def set_env(token_usr, identifier) + clear_env + clear_param + env[Gitlab::APIHelpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token + env[Gitlab::APIHelpers::SUDO_HEADER] = identifier + end + + + def set_param(token_usr, identifier) + clear_env + clear_param + params[Gitlab::APIHelpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token + params[Gitlab::APIHelpers::SUDO_PARAM] = identifier + end + + + def clear_env + env.delete(Gitlab::APIHelpers::PRIVATE_TOKEN_HEADER) + env.delete(Gitlab::APIHelpers::SUDO_HEADER) + end + + def clear_param + params.delete(Gitlab::APIHelpers::PRIVATE_TOKEN_PARAM) + params.delete(Gitlab::APIHelpers::SUDO_PARAM) + end + + def error!(message, status) + raise Exception + end + + describe ".current_user" do + it "should leave user as is when sudo not specified" do + env[Gitlab::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token + current_user.should == user + clear_env + params[Gitlab::APIHelpers::PRIVATE_TOKEN_PARAM] = user.private_token + current_user.should == user + end + + it "should change current user to sudo when admin" do + set_env(admin, user.id) + current_user.should == user + set_param(admin, user.id) + current_user.should == user + set_env(admin, user.username) + current_user.should == user + set_param(admin, user.username) + current_user.should == user + end + + it "should throw 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 + set_param(user, admin.id) + expect { current_user }.to raise_error + set_env(user, admin.username) + expect { current_user }.to raise_error + set_param(user, admin.username) + expect { current_user }.to raise_error + end + it "should throw an error when the user cannot be found for a given id" do + id = user.id + admin.id + user.id.should_not == id + admin.id.should_not == id + set_env(admin, id) + expect { current_user }.to raise_error + + set_param(admin, id) + expect { current_user }.to raise_error + end + it "should throw an error when the user cannot be found for a given username" do + username = "#{user.username}#{admin.username}" + user.username.should_not == username + admin.username.should_not == username + set_env(admin, username) + expect { current_user }.to raise_error + + set_param(admin, username) + expect { current_user }.to raise_error + end + it "should handle sudo's to oneself" do + set_env(admin, admin.id) + current_user.should == admin + set_param(admin, admin.id) + current_user.should == admin + set_env(admin, admin.username) + current_user.should == admin + set_param(admin, admin.username) + current_user.should == admin + end + + it "should handle multiple sudo's to oneself" do + set_env(admin, user.id) + current_user.should == user + current_user.should == user + set_env(admin, user.username) + current_user.should == user + current_user.should == user + + set_param(admin, user.id) + current_user.should == user + current_user.should == user + set_param(admin, user.username) + current_user.should == user + current_user.should == user + end + it "should handle multiple sudo's to oneself using string ids" do + set_env(admin, user.id.to_s) + current_user.should == user + current_user.should == user + + set_param(admin, user.id.to_s) + current_user.should == user + current_user.should == user + end + end + + describe '.sudo_identifier' do + it "should return integers when input is an int" do + set_env(admin, '123') + sudo_identifier.should == 123 + set_env(admin, '0001234567890') + sudo_identifier.should == 1234567890 + + set_param(admin, '123') + sudo_identifier.should == 123 + set_param(admin, '0001234567890') + sudo_identifier.should == 1234567890 + end + + it "should return string when input is an is not an int" do + set_env(admin, '12.30') + sudo_identifier.should == "12.30" + set_env(admin, 'hello') + sudo_identifier.should == 'hello' + set_env(admin, ' 123') + sudo_identifier.should == ' 123' + + set_param(admin, '12.30') + sudo_identifier.should == "12.30" + set_param(admin, 'hello') + sudo_identifier.should == 'hello' + set_param(admin, ' 123') + sudo_identifier.should == ' 123' + end + end +end \ No newline at end of file