Commit df51e59c authored by Shinya Maeda's avatar Shinya Maeda

Merge branch '216785-use-ci-job-token-for-terraform-state-auth-2' into 'master'

Allow job token auth to terraform state API # 2

See merge request gitlab-org/gitlab!34618
parents f99270bf 5125dbab
---
title: Allow CI_JOB_TOKEN for authenticating to the Terraform state API
merge_request: 34618
author:
type: added
...@@ -64,7 +64,7 @@ You can add a command to your `.gitlab-ci.yml` file to ...@@ -64,7 +64,7 @@ You can add a command to your `.gitlab-ci.yml` file to
| `CI_JOB_MANUAL` | 8.12 | all | The flag to indicate that job was manually started | | `CI_JOB_MANUAL` | 8.12 | all | The flag to indicate that job was manually started |
| `CI_JOB_NAME` | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` | | `CI_JOB_NAME` | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
| `CI_JOB_STAGE` | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | | `CI_JOB_STAGE` | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
| `CI_JOB_TOKEN` | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry](../../user/packages/container_registry/index.md) and downloading [dependent repositories](../../user/project/new_ci_build_permissions_model.md#dependent-repositories) | | `CI_JOB_TOKEN` | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry](../../user/packages/container_registry/index.md), downloading [dependent repositories](../../user/project/new_ci_build_permissions_model.md#dependent-repositories), and accessing [GitLab-managed Terraform state](../../user/infrastructure/index.md#gitlab-managed-terraform-state). |
| `CI_JOB_JWT` | 12.10 | all | RS256 JSON web token that can be used for authenticating with third party systems that support JWT authentication, for example [HashiCorp's Vault](../examples/authenticating-with-hashicorp-vault). | | `CI_JOB_JWT` | 12.10 | all | RS256 JSON web token that can be used for authenticating with third party systems that support JWT authentication, for example [HashiCorp's Vault](../examples/authenticating-with-hashicorp-vault). |
| `CI_JOB_URL` | 11.1 | 0.5 | Job details URL | | `CI_JOB_URL` | 11.1 | 0.5 | Job details URL |
| `CI_KUBERNETES_ACTIVE` | 13.0 | all | Included with the value `true` only if the pipeline has a Kubernetes cluster available for deployments. Not included if no cluster is available. Can be used as an alternative to [`only:kubernetes`/`except:kubernetes`](../yaml/README.md#onlykubernetesexceptkubernetes) with [`rules:if`](../yaml/README.md#rulesif) | | `CI_KUBERNETES_ACTIVE` | 13.0 | all | Included with the value `true` only if the pipeline has a Kubernetes cluster available for deployments. Not included if no cluster is available. Can be used as an alternative to [`only:kubernetes`/`except:kubernetes`](../yaml/README.md#onlykubernetesexceptkubernetes) with [`rules:if`](../yaml/README.md#rulesif) |
......
...@@ -25,7 +25,7 @@ Amazon S3 or Google Cloud Storage. Its features include: ...@@ -25,7 +25,7 @@ Amazon S3 or Google Cloud Storage. Its features include:
To get started with a GitLab-managed Terraform State, there are two different options: To get started with a GitLab-managed Terraform State, there are two different options:
- [Use a local machine](#get-started-using-local-development). - [Use a local machine](#get-started-using-local-development).
- [Use GitLab CI](#get-started-using-a-gitlab-ci). - [Use GitLab CI](#get-started-using-gitlab-ci).
## Get started using local development ## Get started using local development
...@@ -44,10 +44,15 @@ local machine, this is a simple way to get started: ...@@ -44,10 +44,15 @@ local machine, this is a simple way to get started:
} }
``` ```
1. Create a [Personal Access Token](../profile/personal_access_tokens.md) with
the `api` scope. The Terraform backend is restricted to users with
[Maintainer access](../permissions.md) to the repository.
1. On your local machine, run `terraform init`, passing in the following options, 1. On your local machine, run `terraform init`, passing in the following options,
replacing `<YOUR-PROJECT-NAME>` and `<YOUR-PROJECT-ID>` with the values for replacing `<YOUR-PROJECT-NAME>`, `<YOUR-PROJECT-ID>`, `<YOUR-USERNAME>` and
your project. This command initializes your Terraform state, and stores that `<YOUR-ACCESS-TOKEN>` with the relevant values. This command initializes your
state within your GitLab project. This example uses `gitlab.com`: Terraform state, and stores that state within your GitLab project. This example
uses `gitlab.com`:
```shell ```shell
terraform init \ terraform init \
...@@ -61,30 +66,24 @@ local machine, this is a simple way to get started: ...@@ -61,30 +66,24 @@ local machine, this is a simple way to get started:
-backend-config="retry_wait_min=5" -backend-config="retry_wait_min=5"
``` ```
Next, [configure the backend](#configure-the-variables-and-backend). Next, [configure the backend](#configure-the-backend).
## Get started using a GitLab CI ## Get started using GitLab CI
If you don't want to start with local development, you can also use GitLab CI to If you don't want to start with local development, you can also use GitLab CI to
run your `terraform plan` and `terraform apply` commands. run your `terraform plan` and `terraform apply` commands.
Next, [configure the backend](#configure-the-variables-and-backend). Next, [configure the backend](#configure-the-backend).
## Configure the variables and backend ## Configure the backend
After executing the `terraform init` command, you must configure the needed CI After executing the `terraform init` command, you must configure the Terraform backend
variables, the Terraform backend, and the CI YAML file: and the CI YAML file:
1. Create a [Personal Access Token](../profile/personal_access_tokens.md) with CAUTION: **Important:**
the `api` scope. The Terraform backend is restricted to tokens with The Terraform backend is restricted to users with [Maintainer access](../permissions.md)
[Maintainer access](../permissions.md) to the repository. to the repository.
1. To keep the Personal Access Token secure, add it as a
[CI/CD environment variable](../../ci/variables/README.md). For the examples on
this page, it's set to the environment variable `GITLAB_TF_PASSWORD`.
CAUTION: **Important:**
If you plan to use the environment variable on an unprotected branch, make sure
to set the variable protection settings correctly.
1. In your Terraform project, define the [HTTP backend](https://www.terraform.io/docs/backends/types/http.html) 1. In your Terraform project, define the [HTTP backend](https://www.terraform.io/docs/backends/types/http.html)
by adding the following code block in a `.tf` file (such as `backend.tf`) to by adding the following code block in a `.tf` file (such as `backend.tf`) to
define the remote backend: define the remote backend:
...@@ -129,7 +128,7 @@ variables, the Terraform backend, and the CI YAML file: ...@@ -129,7 +128,7 @@ variables, the Terraform backend, and the CI YAML file:
before_script: before_script:
- cd ${TF_ROOT} - cd ${TF_ROOT}
- terraform --version - terraform --version
- terraform init -backend-config="address=${GITLAB_TF_ADDRESS}" -backend-config="lock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="unlock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="username=${GITLAB_USER_LOGIN}" -backend-config="password=${GITLAB_TF_PASSWORD}" -backend-config="lock_method=POST" -backend-config="unlock_method=DELETE" -backend-config="retry_wait_min=5" - terraform init -backend-config="address=${GITLAB_TF_ADDRESS}" -backend-config="lock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="unlock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="username=gitlab-ci-token" -backend-config="password=${CI_JOB_TOKEN}" -backend-config="lock_method=POST" -backend-config="unlock_method=DELETE" -backend-config="retry_wait_min=5"
stages: stages:
- validate - validate
......
...@@ -32,7 +32,7 @@ module API ...@@ -32,7 +32,7 @@ module API
end end
desc 'Get a terraform state by its name' desc 'Get a terraform state by its name'
route_setting :authentication, basic_auth_personal_access_token: true route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
get do get do
remote_state_handler.find_with_lock do |state| remote_state_handler.find_with_lock do |state|
no_content! unless state.file.exists? no_content! unless state.file.exists?
...@@ -44,7 +44,7 @@ module API ...@@ -44,7 +44,7 @@ module API
end end
desc 'Add a new terraform state or update an existing one' desc 'Add a new terraform state or update an existing one'
route_setting :authentication, basic_auth_personal_access_token: true route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
post do post do
data = request.body.read data = request.body.read
no_content! if data.empty? no_content! if data.empty?
...@@ -57,7 +57,7 @@ module API ...@@ -57,7 +57,7 @@ module API
end end
desc 'Delete a terraform state of a certain name' desc 'Delete a terraform state of a certain name'
route_setting :authentication, basic_auth_personal_access_token: true route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
delete do delete do
remote_state_handler.handle_with_lock do |state| remote_state_handler.handle_with_lock do |state|
state.destroy! state.destroy!
...@@ -66,7 +66,7 @@ module API ...@@ -66,7 +66,7 @@ module API
end end
desc 'Lock a terraform state of a certain name' desc 'Lock a terraform state of a certain name'
route_setting :authentication, basic_auth_personal_access_token: true route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
params do params do
requires :ID, type: String, limit: 255, desc: 'Terraform state lock ID' requires :ID, type: String, limit: 255, desc: 'Terraform state lock ID'
requires :Operation, type: String, desc: 'Terraform operation' requires :Operation, type: String, desc: 'Terraform operation'
...@@ -103,7 +103,7 @@ module API ...@@ -103,7 +103,7 @@ module API
end end
desc 'Unlock a terraform state of a certain name' desc 'Unlock a terraform state of a certain name'
route_setting :authentication, basic_auth_personal_access_token: true route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
params do params do
optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID' optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID'
end end
......
...@@ -56,6 +56,7 @@ module Gitlab ...@@ -56,6 +56,7 @@ module Gitlab
def find_user_from_job_token def find_user_from_job_token
return unless route_authentication_setting[:job_token_allowed] return unless route_authentication_setting[:job_token_allowed]
return find_user_from_basic_auth_job if route_authentication_setting[:job_token_allowed] == :basic_auth
token = current_request.params[JOB_TOKEN_PARAM].presence || token = current_request.params[JOB_TOKEN_PARAM].presence ||
current_request.params[RUNNER_JOB_TOKEN_PARAM].presence || current_request.params[RUNNER_JOB_TOKEN_PARAM].presence ||
......
...@@ -4,6 +4,7 @@ require 'spec_helper' ...@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::Auth::AuthFinders do describe Gitlab::Auth::AuthFinders do
include described_class include described_class
include HttpBasicAuthHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:env) do let(:env) do
...@@ -22,10 +23,7 @@ describe Gitlab::Auth::AuthFinders do ...@@ -22,10 +23,7 @@ describe Gitlab::Auth::AuthFinders do
end end
def set_basic_auth_header(username, password) def set_basic_auth_header(username, password)
set_header( env.merge!(basic_auth_header(username, password))
'HTTP_AUTHORIZATION',
ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
)
end end
describe '#find_user_from_warden' do describe '#find_user_from_warden' do
...@@ -653,6 +651,24 @@ describe Gitlab::Auth::AuthFinders do ...@@ -653,6 +651,24 @@ describe Gitlab::Auth::AuthFinders do
it_behaves_like 'job token params', described_class::JOB_TOKEN_PARAM it_behaves_like 'job token params', described_class::JOB_TOKEN_PARAM
it_behaves_like 'job token params', described_class::RUNNER_JOB_TOKEN_PARAM it_behaves_like 'job token params', described_class::RUNNER_JOB_TOKEN_PARAM
end end
context 'when the job token is provided via basic auth' do
let(:route_authentication_setting) { { job_token_allowed: :basic_auth } }
let(:username) { Ci::Build::CI_REGISTRY_USER }
let(:token) { job.token }
before do
set_basic_auth_header(username, token)
end
it { is_expected.to eq(user) }
context 'credentials are provided but route setting is incorrect' do
let(:route_authentication_setting) { { job_token_allowed: :unknown } }
it { is_expected.to be_nil }
end
end
end end
describe '#find_runner_from_token' do describe '#find_runner_from_token' do
......
...@@ -3,20 +3,9 @@ ...@@ -3,20 +3,9 @@
require 'spec_helper' require 'spec_helper'
describe 'OAuth tokens' do describe 'OAuth tokens' do
context 'Resource Owner Password Credentials' do include HttpBasicAuthHelpers
def basic_auth_header(username, password)
{
'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(
username,
password
)
}
end
def client_basic_auth_header(client)
basic_auth_header(client.uid, client.secret)
end
context 'Resource Owner Password Credentials' do
def request_oauth_token(user, headers = {}) def request_oauth_token(user, headers = {})
post '/oauth/token', post '/oauth/token',
params: { username: user.username, password: user.password, grant_type: 'password' }, params: { username: user.username, password: user.password, grant_type: 'password' },
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe API::Terraform::State do describe API::Terraform::State do
include HttpBasicAuthHelpers
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user, developer_projects: [project]) } let_it_be(:developer) { create(:user, developer_projects: [project]) }
let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) } let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) }
...@@ -10,7 +12,7 @@ describe API::Terraform::State do ...@@ -10,7 +12,7 @@ describe API::Terraform::State do
let!(:state) { create(:terraform_state, :with_file, project: project) } let!(:state) { create(:terraform_state, :with_file, project: project) }
let(:current_user) { maintainer } let(:current_user) { maintainer }
let(:auth_header) { basic_auth_header(current_user) } let(:auth_header) { user_basic_auth_header(current_user) }
let(:project_id) { project.id } let(:project_id) { project.id }
let(:state_name) { state.name } let(:state_name) { state.name }
let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}" } let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}" }
...@@ -23,7 +25,7 @@ describe API::Terraform::State do ...@@ -23,7 +25,7 @@ describe API::Terraform::State do
subject(:request) { get api(state_path), headers: auth_header } subject(:request) { get api(state_path), headers: auth_header }
context 'without authentication' do context 'without authentication' do
let(:auth_header) { basic_auth_header('failing_token') } let(:auth_header) { basic_auth_header('bad', 'token') }
it 'returns 401 if user is not authenticated' do it 'returns 401 if user is not authenticated' do
request request
...@@ -32,34 +34,71 @@ describe API::Terraform::State do ...@@ -32,34 +34,71 @@ describe API::Terraform::State do
end end
end end
context 'with maintainer permissions' do context 'personal acceess token authentication' do
let(:current_user) { maintainer } context 'with maintainer permissions' do
let(:current_user) { maintainer }
it 'returns terraform state belonging to a project of given state name' do it 'returns terraform state belonging to a project of given state name' do
request request
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq(state.file.read) expect(response.body).to eq(state.file.read)
end
context 'for a project that does not exist' do
let(:project_id) { '0000' }
it 'returns not found' do
request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end end
context 'for a project that does not exist' do context 'with developer permissions' do
let(:project_id) { '0000' } let(:current_user) { developer }
it 'returns not found' do it 'returns forbidden if the user cannot access the state' do
request request
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:forbidden)
end end
end end
end end
context 'with developer permissions' do context 'job token authentication' do
let(:current_user) { developer } let(:auth_header) { job_basic_auth_header(job) }
it 'returns forbidden if the user cannot access the state' do context 'with maintainer permissions' do
request let(:job) { create(:ci_build, project: project, user: maintainer) }
expect(response).to have_gitlab_http_status(:forbidden) it 'returns terraform state belonging to a project of given state name' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq(state.file.read)
end
context 'for a project that does not exist' do
let(:project_id) { '0000' }
it 'returns not found' do
request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with developer permissions' do
let(:job) { create(:ci_build, project: project, user: developer) }
it 'returns forbidden if the user cannot access the state' do
request
expect(response).to have_gitlab_http_status(:forbidden)
end
end end
end end
end end
......
...@@ -40,17 +40,6 @@ module ApiHelpers ...@@ -40,17 +40,6 @@ module ApiHelpers
end end
end end
def basic_auth_header(user = nil)
return { 'HTTP_AUTHORIZATION' => user } unless user.respond_to?(:username)
{
'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(
user.username,
create(:personal_access_token, user: user).token
)
}
end
def expect_empty_array_response def expect_empty_array_response
expect_successful_response_with_paginated_array expect_successful_response_with_paginated_array
expect(json_response.length).to eq(0) expect(json_response.length).to eq(0)
......
# frozen_string_literal: true
module HttpBasicAuthHelpers
def user_basic_auth_header(user)
access_token = create(:personal_access_token, user: user)
basic_auth_header(user.username, access_token.token)
end
def job_basic_auth_header(job)
basic_auth_header(Ci::Build::CI_REGISTRY_USER, job.token)
end
def client_basic_auth_header(client)
basic_auth_header(client.uid, client.secret)
end
def basic_auth_header(username, password)
{
'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(
username,
password
)
}
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