Commit 89199938 authored by Magdalena Frankiewicz's avatar Magdalena Frankiewicz

Create API endpoint for Terraform state backend

- Use Grape API
- Endpoints are dummies, but with authentication and authorization
- Use basic auth to get the Personal Access Token
- Add Maintainer ability specific to Terraform state
parent f11fa333
......@@ -318,6 +318,7 @@ class ProjectPolicy < BasePolicy
enable :read_pod_logs
enable :destroy_deploy_token
enable :read_prometheus_alerts
enable :admin_terraform_state
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
......
......@@ -172,6 +172,7 @@ module API
mount ::API::ProjectSnippets
mount ::API::ProjectStatistics
mount ::API::ProjectTemplates
mount ::API::Terraform::State
mount ::API::ProtectedBranches
mount ::API::ProtectedTags
mount ::API::Releases
......
......@@ -46,6 +46,10 @@ module API
prepend_if_ee('EE::API::APIGuard::HelperMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule
include Gitlab::Auth::AuthFinders
def access_token
super || find_personal_access_token_from_http_basic_auth
end
def find_current_user!
user = find_user_from_sources
return unless user
......
# frozen_string_literal: true
module API
module Terraform
class State < Grape::API
before { authenticate! }
before { authorize! :admin_terraform_state, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
requires :name, type: String, desc: 'The name of a terraform state'
end
namespace ':id/terraform/state/:name' do
desc 'Get a terraform state by its name'
route_setting :authentication, basic_auth_personal_access_token: true
get do
status 501
content_type 'text/plain'
body 'not implemented'
end
desc 'Add a new terraform state or update an existing one'
route_setting :authentication, basic_auth_personal_access_token: true
post do
status 501
content_type 'text/plain'
body 'not implemented'
end
desc 'Delete a terraform state of certain name'
route_setting :authentication, basic_auth_personal_access_token: true
delete do
status 501
content_type 'text/plain'
body 'not implemented'
end
end
end
end
end
end
......@@ -167,6 +167,14 @@ module Gitlab
oauth_token
end
def find_personal_access_token_from_http_basic_auth
return unless route_authentication_setting[:basic_auth_personal_access_token]
return unless has_basic_credentials?(current_request)
_username, password = user_name_and_password(current_request)
PersonalAccessToken.find_by_token(password)
end
def parsed_oauth_token
Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
end
......
......@@ -335,6 +335,54 @@ describe Gitlab::Auth::AuthFinders do
end
end
describe '#find_personal_access_token_from_http_basic_auth' do
def auth_header_with(token)
env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('username', token)
end
context 'access token is valid' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:route_authentication_setting) { { basic_auth_personal_access_token: true } }
it 'finds the token from basic auth' do
auth_header_with(personal_access_token.token)
expect(find_personal_access_token_from_http_basic_auth).to eq personal_access_token
end
end
context 'access token is not valid' do
let(:route_authentication_setting) { { basic_auth_personal_access_token: true } }
it 'returns nil' do
auth_header_with('failing_token')
expect(find_personal_access_token_from_http_basic_auth).to be_nil
end
end
context 'route_setting is not set' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
it 'returns nil' do
auth_header_with(personal_access_token.token)
expect(find_personal_access_token_from_http_basic_auth).to be_nil
end
end
context 'route_setting is not correct' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:route_authentication_setting) { { basic_auth_personal_access_token: false } }
it 'returns nil' do
auth_header_with(personal_access_token.token)
expect(find_personal_access_token_from_http_basic_auth).to be_nil
end
end
end
describe '#find_user_from_basic_auth_job' do
def basic_http_auth(username, password)
ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
......
......@@ -53,6 +53,7 @@ describe ProjectPolicy do
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics read_deploy_token create_deploy_token destroy_deploy_token
admin_terraform_state
]
end
......
# frozen_string_literal: true
require 'spec_helper'
describe API::Terraform::State do
def auth_header_for(user)
auth_header = ActionController::HttpAuthentication::Basic.encode_credentials(
user.username,
create(:personal_access_token, user: user).token
)
{ 'HTTP_AUTHORIZATION' => auth_header }
end
let!(:project) { create(:project) }
let(:developer) { create(:user) }
let(:maintainer) { create(:user) }
let(:state_name) { 'state' }
before do
project.add_maintainer(maintainer)
end
describe 'GET /projects/:id/terraform/state/:name' do
it 'returns 401 if user is not authenticated' do
headers = { 'HTTP_AUTHORIZATION' => 'failing_token' }
get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: headers
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'returns terraform state belonging to a project of given state name' do
get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(maintainer)
expect(response).to have_gitlab_http_status(:not_implemented)
expect(response.body).to eq('not implemented')
end
it 'returns not found if the project does not exists' do
get api("/projects/0000/terraform/state/#{state_name}"), headers: auth_header_for(maintainer)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns forbidden if the user cannot access the state' do
project.add_developer(developer)
get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
describe 'POST /projects/:id/terraform/state/:name' do
context 'when terraform state with a given name is already present' do
it 'updates the state' do
post api("/projects/#{project.id}/terraform/state/#{state_name}"),
params: '{ "instance": "example-instance" }',
headers: { 'Content-Type' => 'text/plain' }.merge(auth_header_for(maintainer))
expect(response).to have_gitlab_http_status(:not_implemented)
expect(response.body).to eq('not implemented')
end
it 'returns forbidden if the user cannot access the state' do
project.add_developer(developer)
get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when there is no terraform state of a given name' do
it 'creates a new state' do
post api("/projects/#{project.id}/terraform/state/example2"),
headers: auth_header_for(maintainer),
params: '{ "database": "example-database" }'
expect(response).to have_gitlab_http_status(:not_implemented)
expect(response.body).to eq('not implemented')
end
end
end
describe 'DELETE /projects/:id/terraform/state/:name' do
it 'deletes the state' do
delete api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(maintainer)
expect(response).to have_gitlab_http_status(:not_implemented)
end
it 'returns forbidden if the user cannot access the state' do
project.add_developer(developer)
get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer)
expect(response).to have_gitlab_http_status(:forbidden)
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