diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/repository/components/table/row.vue | 4 | ||||
-rw-r--r-- | app/graphql/types/issue_type.rb | 2 | ||||
-rw-r--r-- | app/graphql/types/merge_request_type.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/time_trackable.rb | 8 | ||||
-rw-r--r-- | app/models/timelog.rb | 12 | ||||
-rw-r--r-- | app/services/ci/pipeline_schedules/variables_base_save_service.rb | 52 | ||||
-rw-r--r-- | app/services/ci/pipeline_schedules/variables_create_service.rb | 24 | ||||
-rw-r--r-- | app/services/ci/pipeline_schedules/variables_update_service.rb | 24 | ||||
-rw-r--r-- | app/services/personal_access_tokens/rotate_service.rb | 40 | ||||
-rw-r--r-- | app/services/project_access_tokens/rotate_service.rb | 58 | ||||
-rw-r--r-- | app/views/projects/merge_requests/_nav_btns.html.haml | 2 |
11 files changed, 210 insertions, 18 deletions
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 6a81f11eb51..bb2b8ae54b6 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -120,13 +120,13 @@ export default { routerLinkTo() { if (this.isBlob) { return buildURLwithRefType({ - path: joinPaths('/-/blob', this.escapedRef, this.path), + path: joinPaths('/-/blob', this.escapedRef, encodeURI(this.path)), refType: this.refType, }); } if (this.isFolder) { return buildURLwithRefType({ - path: joinPaths('/-/tree', this.escapedRef, this.path), + path: joinPaths('/-/tree', this.escapedRef, encodeURI(this.path)), refType: this.refType, }); } diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 1c8a654a841..7c7d559e05d 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -111,7 +111,7 @@ module Types field :time_estimate, GraphQL::Types::Int, null: false, description: 'Time estimate of the issue.' field :total_time_spent, GraphQL::Types::Int, null: false, - description: 'Total time reported as spent on the issue.' + description: 'Total time (in seconds) reported as spent on the issue.' field :closed_at, Types::TimeType, null: true, description: 'Timestamp of when the issue was closed.' diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 9dca82f1750..d7c3b313f84 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -199,7 +199,7 @@ module Types field :time_estimate, GraphQL::Types::Int, null: false, description: 'Time estimate of the merge request.' field :total_time_spent, GraphQL::Types::Int, null: false, - description: 'Total time reported as spent on the merge request.' + description: 'Total time (in seconds) reported as spent on the merge request.' field :approved, GraphQL::Types::Boolean, method: :approved?, diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index 0f361e70a91..70bc45b382a 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -45,7 +45,13 @@ module TimeTrackable # rubocop:enable Gitlab/ModuleWithInstanceVariables def total_time_spent - timelogs.sum(:time_spent) + sum = timelogs.sum(:time_spent) + + # A new restriction has been introduced to limit total time spent to - + # Timelog::MAX_TOTAL_TIME_SPENT or 3.154e+7 seconds (approximately a year, a generous limit) + # Since there could be existing records that breach the limit, check and return the maximum/minimum allowed value. + # (some issuable might have total time spent that's negative because a validation was missing.) + sum.clamp(-Timelog::MAX_TOTAL_TIME_SPENT, Timelog::MAX_TOTAL_TIME_SPENT) end def human_total_time_spent diff --git a/app/models/timelog.rb b/app/models/timelog.rb index b6b4decc64b..eb088b1f582 100644 --- a/app/models/timelog.rb +++ b/app/models/timelog.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Timelog < ApplicationRecord + # Gitlab::TimeTrackingFormatter.parse("1y") == 31557600 seconds + # 31557600 slightly deviates from (365 days * 24 hours/day * 60 minutes/hour * 60 seconds/minute) + MAX_TOTAL_TIME_SPENT = 31557600.seconds.to_i # a year + include Importable include IgnorableColumns include Sortable @@ -12,6 +16,7 @@ class Timelog < ApplicationRecord validates :time_spent, :user, presence: true validates :summary, length: { maximum: 255 } validate :issuable_id_is_present, unless: :importing? + validate :check_total_time_spent_is_within_range, on: :create, unless: :importing?, if: :time_spent belongs_to :issue, touch: true belongs_to :merge_request, touch: true @@ -58,6 +63,13 @@ class Timelog < ApplicationRecord private + def check_total_time_spent_is_within_range + total_time_spent = issuable.timelogs.sum(:time_spent) + time_spent + + errors.add(:base, _("Total time spent cannot be negative.")) if total_time_spent < 0 + errors.add(:base, _("Total time spent cannot exceed a year.")) if total_time_spent > MAX_TOTAL_TIME_SPENT + end + def issuable_id_is_present if issue_id && merge_request_id errors.add(:base, _('Only Issue ID or merge request ID is required')) diff --git a/app/services/ci/pipeline_schedules/variables_base_save_service.rb b/app/services/ci/pipeline_schedules/variables_base_save_service.rb new file mode 100644 index 00000000000..fb3ba52b36e --- /dev/null +++ b/app/services/ci/pipeline_schedules/variables_base_save_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Ci + module PipelineSchedules + class VariablesBaseSaveService + include Gitlab::Utils::StrongMemoize + + def execute + variable.assign_attributes(params) + + return forbidden_to_update_pipeline_schedule unless allowed_to_update_pipeline_schedule? + return forbidden_to_save_variables unless allowed_to_save_variables? + + if variable.save + ServiceResponse.success(payload: variable) + else + ServiceResponse.error(payload: variable, message: variable.errors.full_messages) + end + end + + private + + attr_reader :project, :user, :params, :variable, :pipeline_schedule + + def allowed_to_update_pipeline_schedule? + user.can?(:update_pipeline_schedule, pipeline_schedule) + end + + def forbidden_to_update_pipeline_schedule + # We add the error to the base object too + # because model errors are used in the API responses and the `form_errors` helper. + pipeline_schedule.errors.add(:base, authorize_message) + + ServiceResponse.error(payload: pipeline_schedule, message: [authorize_message], reason: :forbidden) + end + + def allowed_to_save_variables? + user.can?(:set_pipeline_variables, project) + end + + def forbidden_to_save_variables + message = _('The current user is not authorized to set pipeline schedule variables') + + # We add the error to the base object too + # because model errors are used in the API responses and the `form_errors` helper. + pipeline_schedule.errors.add(:base, message) + + ServiceResponse.error(payload: pipeline_schedule, message: [message], reason: :forbidden) + end + end + end +end diff --git a/app/services/ci/pipeline_schedules/variables_create_service.rb b/app/services/ci/pipeline_schedules/variables_create_service.rb new file mode 100644 index 00000000000..7d81b0771dc --- /dev/null +++ b/app/services/ci/pipeline_schedules/variables_create_service.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Ci + module PipelineSchedules + class VariablesCreateService < VariablesBaseSaveService + AUTHORIZE = :update_pipeline_schedule + + def initialize(pipeline_schedule, user, params) + @variable = pipeline_schedule.variables.new + @user = user + @pipeline_schedule = pipeline_schedule + @project = pipeline_schedule.project + @params = params + end + + private + + def authorize_message + _('The current user is not authorized to create the pipeline schedule variables') + end + strong_memoize_attr :authorize_message + end + end +end diff --git a/app/services/ci/pipeline_schedules/variables_update_service.rb b/app/services/ci/pipeline_schedules/variables_update_service.rb new file mode 100644 index 00000000000..f0e3a03c37a --- /dev/null +++ b/app/services/ci/pipeline_schedules/variables_update_service.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Ci + module PipelineSchedules + class VariablesUpdateService < VariablesBaseSaveService + AUTHORIZE = :update_pipeline_schedule + + def initialize(pipeline_schedule_variable, user, params) + @variable = pipeline_schedule_variable + @user = user + @pipeline_schedule = pipeline_schedule_variable.pipeline_schedule + @project = pipeline_schedule.project + @params = params + end + + private + + def authorize_message + _('The current user is not authorized to update the pipeline schedule variables') + end + strong_memoize_attr :authorize_message + end + end +end diff --git a/app/services/personal_access_tokens/rotate_service.rb b/app/services/personal_access_tokens/rotate_service.rb index 32710629caf..13144a04c11 100644 --- a/app/services/personal_access_tokens/rotate_service.rb +++ b/app/services/personal_access_tokens/rotate_service.rb @@ -10,26 +10,18 @@ module PersonalAccessTokens end def execute(params = {}) - return ServiceResponse.error(message: _('token already revoked')) if token.revoked? + return error_response(_('token already revoked')) if token.revoked? response = ServiceResponse.success PersonalAccessToken.transaction do unless token.revoke! - response = ServiceResponse.error(message: _('failed to revoke token')) + response = error_response(_('failed to revoke token')) raise ActiveRecord::Rollback end - target_user = token.user - new_token = target_user.personal_access_tokens.create(create_token_params(token, params)) - - if new_token.persisted? - response = ServiceResponse.success(payload: { personal_access_token: new_token }) - else - response = ServiceResponse.error(message: new_token.errors.full_messages.to_sentence) - - raise ActiveRecord::Rollback - end + response = create_access_token(params) + raise ActiveRecord::Rollback unless response.success? end response @@ -47,5 +39,29 @@ module PersonalAccessTokens scopes: token.scopes, expires_at: expires_at } end + + def create_access_token(params) + target_user = token.user + + new_token = target_user.personal_access_tokens.create(create_token_params(token, params)) + + return success_response(new_token) if new_token.persisted? + + error_response(new_token.errors.full_messages.to_sentence) + end + + def expires_at(params) + return params[:expires_at] if params[:expires_at] + + params[:expires_at] || EXPIRATION_PERIOD.from_now.to_date + end + + def success_response(new_token) + ServiceResponse.success(payload: { personal_access_token: new_token }) + end + + def error_response(message) + ServiceResponse.error(message: message) + end end end diff --git a/app/services/project_access_tokens/rotate_service.rb b/app/services/project_access_tokens/rotate_service.rb new file mode 100644 index 00000000000..63d8d2a82cc --- /dev/null +++ b/app/services/project_access_tokens/rotate_service.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module ProjectAccessTokens + class RotateService < ::PersonalAccessTokens::RotateService + extend ::Gitlab::Utils::Override + + def initialize(current_user, token, resource = nil) + @current_user = current_user + @token = token + @project = resource + end + + def execute(params = {}) + super + end + + attr_reader :project + + private + + override :create_access_token + def create_access_token(params) + target_user = token.user + + unless valid_access_level? + return error_response( + _("Not eligible to rotate token with access level higher than the user") + ) + end + + new_token = target_user.personal_access_tokens.create(create_token_params(token, params)) + + if new_token.persisted? + update_bot_membership(target_user, new_token.expires_at) + + return success_response(new_token) + end + + error_response(new_token.errors.full_messages.to_sentence) + end + + def update_bot_membership(target_user, expires_at) + target_user.members.update(expires_at: expires_at) + end + + def valid_access_level? + return true if current_user.can_admin_all_resources? + return false unless current_user.can?(:manage_resource_access_tokens, project) + + token_access_level = project.team.max_member_access(token.user.id).to_i + current_user_access_level = project.team.max_member_access(current_user.id).to_i + + return true if token_access_level.to_i <= current_user_access_level + + false + end + end +end diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml index 2c0a8d831e4..4607aebf121 100644 --- a/app/views/projects/merge_requests/_nav_btns.html.haml +++ b/app/views/projects/merge_requests/_nav_btns.html.haml @@ -4,7 +4,7 @@ - if @can_bulk_update = render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-mr-3 js-bulk-update-toggle' }) do = _("Bulk edit") -- if merge_project +- if merge_project && can?(@current_user, :create_merge_request_in, @project) = render Pajamas::ButtonComponent.new(href: new_merge_request_path, variant: :confirm) do = _("New merge request") |