Commit 3572bfb7 authored by lexicalunit's avatar lexicalunit

Create new deploy tokens via ajax

Resolves the backend half of
https://gitlab.com/gitlab-org/gitlab/-/issues/22794
by changing the POST to an ajax request. This means that a reload of the
page will no longer trigger the creation of a new deploy token.

These changes are behind a feature flag named `:ajax_new_deploy_token`.
The flag exists at the group and project level.

When enabled:

- Makes create_deploy_token actions of ci_cd controllers ajax
- The JSON response matches the existing API schema

API Schema: https://docs.gitlab.com/ee/api/deploy_tokens.html
parent f0f05d2d
...@@ -8,6 +8,7 @@ module Groups ...@@ -8,6 +8,7 @@ module Groups
before_action :authorize_update_max_artifacts_size!, only: [:update] before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action do before_action do
push_frontend_feature_flag(:new_variables_ui, @group) push_frontend_feature_flag(:new_variables_ui, @group)
push_frontend_feature_flag(:ajax_new_deploy_token, @group)
end end
before_action :define_variables, only: [:show, :create_deploy_token] before_action :define_variables, only: [:show, :create_deploy_token]
...@@ -42,13 +43,30 @@ module Groups ...@@ -42,13 +43,30 @@ module Groups
end end
def create_deploy_token def create_deploy_token
@new_deploy_token = Groups::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute result = Projects::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute
@new_deploy_token = result[:deploy_token]
if @new_deploy_token.persisted?
flash.now[:notice] = s_('DeployTokens|Your new group deploy token has been created.') if result[:status] == :success
respond_to do |format|
format.json do
# IMPORTANT: It's a security risk to expose the token value more than just once here!
json = API::Entities::DeployTokenWithToken.represent(@new_deploy_token).as_json
render json: json, status: result[:http_status]
end
format.html do
flash.now[:notice] = s_('DeployTokens|Your new group deploy token has been created.')
render :show
end
end
else
respond_to do |format|
format.json { render json: { message: result[:message] }, status: result[:http_status] }
format.html do
flash.now[:alert] = result[:message]
render :show
end
end
end end
render 'show'
end end
private private
......
...@@ -7,6 +7,7 @@ module Projects ...@@ -7,6 +7,7 @@ module Projects
before_action :define_variables before_action :define_variables
before_action do before_action do
push_frontend_feature_flag(:new_variables_ui, @project) push_frontend_feature_flag(:new_variables_ui, @project)
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
end end
def show def show
...@@ -47,13 +48,30 @@ module Projects ...@@ -47,13 +48,30 @@ module Projects
end end
def create_deploy_token def create_deploy_token
@new_deploy_token = Projects::DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute result = Projects::DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
@new_deploy_token = result[:deploy_token]
if @new_deploy_token.persisted? if result[:status] == :success
flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.') respond_to do |format|
format.json do
# IMPORTANT: It's a security risk to expose the token value more than just once here!
json = API::Entities::DeployTokenWithToken.represent(@new_deploy_token).as_json
render json: json, status: result[:http_status]
end
format.html do
flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
render :show
end
end
else
respond_to do |format|
format.json { render json: { message: result[:message] }, status: result[:http_status] }
format.html do
flash.now[:alert] = result[:message]
render :show
end
end
end end
render 'show'
end end
private private
......
...@@ -6,7 +6,13 @@ module Groups ...@@ -6,7 +6,13 @@ module Groups
include DeployTokenMethods include DeployTokenMethods
def execute def execute
create_deploy_token_for(@group, params) deploy_token = create_deploy_token_for(@group, params)
if deploy_token.persisted?
success(deploy_token: deploy_token, http_status: :ok)
else
error(deploy_token.errors.full_messages.to_sentence, :bad_request)
end
end end
end end
end end
......
...@@ -6,7 +6,13 @@ module Projects ...@@ -6,7 +6,13 @@ module Projects
include DeployTokenMethods include DeployTokenMethods
def execute def execute
create_deploy_token_for(@project, params) deploy_token = create_deploy_token_for(@project, params)
if deploy_token.persisted?
success(deploy_token: deploy_token, http_status: :ok)
else
error(deploy_token.errors.full_messages.to_sentence, :bad_request)
end
end end
end end
end end
......
%p.profile-settings-content %p.profile-settings-content
= s_("DeployTokens|Pick a name for the application, and we'll give you a unique deploy token.") = s_("DeployTokens|Pick a name for the application, and we'll give you a unique deploy token.")
= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post do |f| = form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f|
= form_errors(token) = form_errors(token)
.form-group .form-group
......
...@@ -65,11 +65,15 @@ module API ...@@ -65,11 +65,15 @@ module API
post ':id/deploy_tokens' do post ':id/deploy_tokens' do
authorize!(:create_deploy_token, user_project) authorize!(:create_deploy_token, user_project)
deploy_token = ::Projects::DeployTokens::CreateService.new( result = ::Projects::DeployTokens::CreateService.new(
user_project, current_user, scope_params.merge(declared(params, include_missing: false, include_parent_namespaces: false)) user_project, current_user, scope_params.merge(declared(params, include_missing: false, include_parent_namespaces: false))
).execute ).execute
present deploy_token, with: Entities::DeployTokenWithToken if result[:status] == :success
present result[:deploy_token], with: Entities::DeployTokenWithToken
else
render_api_error!(result[:message], result[:http_status])
end
end end
desc 'Delete a project deploy token' do desc 'Delete a project deploy token' do
...@@ -126,11 +130,15 @@ module API ...@@ -126,11 +130,15 @@ module API
post ':id/deploy_tokens' do post ':id/deploy_tokens' do
authorize!(:create_deploy_token, user_group) authorize!(:create_deploy_token, user_group)
deploy_token = ::Groups::DeployTokens::CreateService.new( result = ::Groups::DeployTokens::CreateService.new(
user_group, current_user, scope_params.merge(declared(params, include_missing: false, include_parent_namespaces: false)) user_group, current_user, scope_params.merge(declared(params, include_missing: false, include_parent_namespaces: false))
).execute ).execute
present deploy_token, with: Entities::DeployTokenWithToken if result[:status] == :success
present result[:deploy_token], with: Entities::DeployTokenWithToken
else
render_api_error!(result[:message], result[:http_status])
end
end end
desc 'Delete a group deploy token' do desc 'Delete a group deploy token' do
......
...@@ -212,14 +212,86 @@ describe Groups::Settings::CiCdController do ...@@ -212,14 +212,86 @@ describe Groups::Settings::CiCdController do
end end
describe 'POST create_deploy_token' do describe 'POST create_deploy_token' do
it_behaves_like 'a created deploy token' do context 'when ajax_new_deploy_token feature flag is disabled for the project' do
let(:entity) { group }
let(:create_entity_params) { { group_id: group } }
let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] }
before do before do
stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: group })
entity.add_owner(user) entity.add_owner(user)
end end
it_behaves_like 'a created deploy token' do
let(:entity) { group }
let(:create_entity_params) { { group_id: group } }
let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] }
end
end
context 'when ajax_new_deploy_token feature flag is enabled for the project' do
let(:good_deploy_token_params) do
{
name: 'name',
expires_at: 1.day.from_now.to_s,
username: 'deployer',
read_repository: '1',
deploy_token_type: DeployToken.deploy_token_types[:group_type]
}
end
let(:request_params) do
{
group_id: group.to_param,
deploy_token: deploy_token_params
}
end
before do
group.add_owner(user)
end
subject { post :create_deploy_token, params: request_params, format: :json }
context('a good request') do
let(:deploy_token_params) { good_deploy_token_params }
let(:expected_response) do
{
'id' => be_a(Integer),
'name' => deploy_token_params[:name],
'username' => deploy_token_params[:username],
'expires_at' => Time.parse(deploy_token_params[:expires_at]),
'token' => be_a(String),
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv
key.to_s.start_with?('read_') && !value.to_i.zero? ? scopes << key.to_s : scopes
end
}
end
it 'creates the deploy token' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/deploy_token')
expect(json_response).to match(expected_response)
end
end
context('a bad request') do
let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) }
let(:expected_response) { { 'message' => "Scopes can't be blank" } }
it 'does not create the deploy token' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to match(expected_response)
end
end
context('an invalid request') do
let(:deploy_token_params) { good_deploy_token_params.except(:name) }
it 'raises a validation error' do
expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
end
end
end end
end end
end end
...@@ -249,10 +249,82 @@ describe Projects::Settings::CiCdController do ...@@ -249,10 +249,82 @@ describe Projects::Settings::CiCdController do
end end
describe 'POST create_deploy_token' do describe 'POST create_deploy_token' do
it_behaves_like 'a created deploy token' do context 'when ajax_new_deploy_token feature flag is disabled for the project' do
let(:entity) { project } before do
let(:create_entity_params) { { namespace_id: project.namespace, project_id: project } } stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] } end
it_behaves_like 'a created deploy token' do
let(:entity) { project }
let(:create_entity_params) { { namespace_id: project.namespace, project_id: project } }
let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] }
end
end
context 'when ajax_new_deploy_token feature flag is enabled for the project' do
let(:good_deploy_token_params) do
{
name: 'name',
expires_at: 1.day.from_now.to_s,
username: 'deployer',
read_repository: '1',
deploy_token_type: DeployToken.deploy_token_types[:project_type]
}
end
let(:request_params) do
{
namespace_id: project.namespace.to_param,
project_id: project.to_param,
deploy_token: deploy_token_params
}
end
subject { post :create_deploy_token, params: request_params, format: :json }
context('a good request') do
let(:deploy_token_params) { good_deploy_token_params }
let(:expected_response) do
{
'id' => be_a(Integer),
'name' => deploy_token_params[:name],
'username' => deploy_token_params[:username],
'expires_at' => Time.parse(deploy_token_params[:expires_at]),
'token' => be_a(String),
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv
key.to_s.start_with?('read_') && !value.to_i.zero? ? scopes << key.to_s : scopes
end
}
end
it 'creates the deploy token' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/deploy_token')
expect(json_response).to match(expected_response)
end
end
context('a bad request') do
let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) }
let(:expected_response) { { 'message' => "Scopes can't be blank" } }
it 'does not create the deploy token' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to match(expected_response)
end
end
context('an invalid request') do
let(:deploy_token_params) { good_deploy_token_params.except(:name) }
it 'raises a validation error' do
expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
end
end
end end
end end
end end
...@@ -14,6 +14,7 @@ describe 'Projects > Settings > CI / CD settings' do ...@@ -14,6 +14,7 @@ describe 'Projects > Settings > CI / CD settings' do
project.add_role(user, role) project.add_role(user, role)
sign_in(user) sign_in(user)
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
visit project_settings_ci_cd_path(project) visit project_settings_ci_cd_path(project)
end end
......
...@@ -11,6 +11,7 @@ describe 'Repository Settings > User sees revoke deploy token modal', :js do ...@@ -11,6 +11,7 @@ describe 'Repository Settings > User sees revoke deploy token modal', :js do
before do before do
project.add_role(user, role) project.add_role(user, role)
sign_in(user) sign_in(user)
stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
visit(project_settings_ci_cd_path(project)) visit(project_settings_ci_cd_path(project))
click_link('Revoke') click_link('Revoke')
end end
......
...@@ -17,7 +17,7 @@ RSpec.shared_examples 'a deploy token creation service' do ...@@ -17,7 +17,7 @@ RSpec.shared_examples 'a deploy token creation service' do
end end
it 'returns a DeployToken' do it 'returns a DeployToken' do
expect(subject).to be_an_instance_of DeployToken expect(subject[:deploy_token]).to be_an_instance_of DeployToken
end end
end end
...@@ -25,7 +25,7 @@ RSpec.shared_examples 'a deploy token creation service' do ...@@ -25,7 +25,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') } let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') }
it 'sets Forever.date' do it 'sets Forever.date' do
expect(subject.read_attribute(:expires_at)).to eq(Forever.date) expect(subject[:deploy_token].read_attribute(:expires_at)).to eq(Forever.date)
end end
end end
...@@ -33,7 +33,7 @@ RSpec.shared_examples 'a deploy token creation service' do ...@@ -33,7 +33,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, username: '') } let(:deploy_token_params) { attributes_for(:deploy_token, username: '') }
it 'converts it to nil' do it 'converts it to nil' do
expect(subject.read_attribute(:username)).to be_nil expect(subject[:deploy_token].read_attribute(:username)).to be_nil
end end
end end
...@@ -41,7 +41,7 @@ RSpec.shared_examples 'a deploy token creation service' do ...@@ -41,7 +41,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, username: 'deployer') } let(:deploy_token_params) { attributes_for(:deploy_token, username: 'deployer') }
it 'keeps the provided username' do it 'keeps the provided username' do
expect(subject.read_attribute(:username)).to eq('deployer') expect(subject[:deploy_token].read_attribute(:username)).to eq('deployer')
end 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