# frozen_string_literal: true module API class Users < ::API::Base include PaginationParams include APIGuard include Helpers::CustomAttributes allow_access_with_scope :read_user, if: -> (request) { request.get? || request.head? } feature_category :user_profile, %w[ /users/:id/custom_attributes /users/:id/custom_attributes/:key /users/:id/associations_count ] urgency :medium, %w[ /users/:id/custom_attributes /users/:id/custom_attributes/:key ] resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do include CustomAttributesEndpoints before do authenticate_non_get! end helpers Helpers::UsersHelpers helpers Gitlab::Tracking::Helpers::WeakPasswordErrorEvent helpers do # rubocop: disable CodeReuse/ActiveRecord def reorder_users(users) if params[:order_by] && params[:sort] users.reorder(order_options_with_tie_breaker) else users end end # rubocop: enable CodeReuse/ActiveRecord params :optional_attributes do optional :skype, type: String, desc: 'The Skype username' optional :linkedin, type: String, desc: 'The LinkedIn username' optional :twitter, type: String, desc: 'The Twitter username' optional :discord, type: String, desc: 'The Discord user ID' optional :website_url, type: String, desc: 'The website of the user' optional :organization, type: String, desc: 'The organization of the user' optional :projects_limit, type: Integer, desc: 'The number of projects a user can create' optional :extern_uid, type: String, desc: 'The external authentication provider UID' optional :provider, type: String, desc: 'The external provider' optional :bio, type: String, desc: 'The biography of the user' optional :location, type: String, desc: 'The location of the user' optional :pronouns, type: String, desc: 'The pronouns of the user' optional :public_email, type: String, desc: 'The public email of the user' optional :commit_email, type: String, desc: 'The commit email, _private for private commit email' optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for user', documentation: { type: 'file' } optional :theme_id, type: Integer, desc: 'The GitLab theme for the user' optional :color_scheme_id, type: Integer, desc: 'The color scheme for the file viewer' # TODO: Add `allow_blank: false` in 16.0. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/387005 optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile' optional :note, type: String, desc: 'Admin note for this user' optional :view_diffs_file_by_file, type: Boolean, desc: 'Flag indicating the user sees only one file diff per page' all_or_none_of :extern_uid, :provider use :optional_params_ee end params :sort_params do optional :order_by, type: String, values: %w[id name username created_at updated_at], default: 'id', desc: 'Return users ordered by a field' optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return users sorted in ascending and descending order' end end resources ':id/associations_count' do helpers do def present_entity(result) present result, with: ::API::Entities::UserAssociationsCount end end desc "Returns a list of a specified user's count of projects, groups, issues and merge requests." params do requires :id, type: Integer, desc: 'ID of the user to query.' end get do authenticate! user = find_user_by_id(params) forbidden! unless can?(current_user, :get_user_associations_count, user) not_found!('User') unless user present_entity(user) end end desc 'Get the list of users' do success Entities::UserBasic end params do # CE optional :username, type: String, desc: 'Get a single user with a specific username' optional :extern_uid, type: String, desc: 'Get a single user with a specific external authentication provider UID' optional :provider, type: String, desc: 'The external provider' optional :search, type: String, desc: 'Search for a username' optional :active, type: Boolean, default: false, desc: 'Filters only active users' optional :external, type: Boolean, default: false, desc: 'Filters only external users' optional :exclude_external, as: :non_external, type: Boolean, default: false, desc: 'Filters only non external users' optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users' optional :created_after, type: DateTime, desc: 'Return users created after the specified time' optional :created_before, type: DateTime, desc: 'Return users created before the specified time' optional :without_projects, type: Boolean, default: false, desc: 'Filters only users without projects' optional :exclude_internal, as: :non_internal, type: Boolean, default: false, desc: 'Filters only non internal users' optional :without_project_bots, type: Boolean, default: false, desc: 'Filters users without project bots' optional :admins, type: Boolean, default: false, desc: 'Filters only admin users' all_or_none_of :extern_uid, :provider use :sort_params use :pagination use :with_custom_attributes use :optional_index_params_ee end # rubocop: disable CodeReuse/ActiveRecord get feature_category: :user_profile, urgency: :low do authenticated_as_admin! if params[:extern_uid].present? && params[:provider].present? unless current_user&.can_read_all_resources? params.except!(:created_after, :created_before, :order_by, :sort, :two_factor, :without_projects) end authorized = can?(current_user, :read_users_list) # When `current_user` is not present, require that the `username` # parameter is passed, to prevent an unauthenticated user from accessing # a list of all the users on the GitLab instance. `UsersFinder` performs # an exact match on the `username` parameter, so we are guaranteed to # get either 0 or 1 `users` here. authorized &&= params[:username].present? if current_user.blank? forbidden!("Not authorized to access /api/v4/users") unless authorized users = UsersFinder.new(current_user, params).execute users = reorder_users(users) entity = current_user&.can_read_all_resources? ? Entities::UserWithAdmin : Entities::UserBasic if entity == Entities::UserWithAdmin users = users.preload(:identities, :u2f_registrations, :webauthn_registrations, :namespace, :followers, :followees, :user_preference) end users, options = with_custom_attributes(users, { with: entity, current_user: current_user }) users = users.preload(:user_detail) present paginate(users), options end # rubocop: enable CodeReuse/ActiveRecord desc 'Get a single user' do success Entities::User end params do requires :id, type: Integer, desc: 'The ID of the user' use :with_custom_attributes end # rubocop: disable CodeReuse/ActiveRecord get ":id", feature_category: :user_profile, urgency: :low do forbidden!('Not authorized!') unless current_user unless current_user.can_read_all_resources? check_rate_limit!(:users_get_by_id, scope: current_user, users_allowlist: Gitlab::CurrentSettings.current_application_settings.users_get_by_id_limit_allowlist ) end user = User.find_by(id: params[:id]) not_found!('User') unless user && can?(current_user, :read_user, user) opts = { with: current_user.can_read_all_resources? ? Entities::UserDetailsWithAdmin : Entities::User, current_user: current_user } user, opts = with_custom_attributes(user, opts) present user, opts end # rubocop: enable CodeReuse/ActiveRecord desc "Get the status of a user" params do requires :user_id, type: String, desc: 'The ID or username of the user' end get ":user_id/status", requirements: API::USER_REQUIREMENTS, feature_category: :user_profile, urgency: :default do user = find_user(params[:user_id]) not_found!('User') unless user && can?(current_user, :read_user, user) present user.status || {}, with: Entities::UserStatus end desc 'Follow a user' do success Entities::User end params do requires :id, type: Integer, desc: 'The ID of the user' end post ':id/follow', feature_category: :user_profile do user = find_user(params[:id]) not_found!('User') unless user followee = current_user.follow(user) if followee&.errors&.any? render_api_error!(followee.errors.full_messages.join(', '), 400) elsif followee&.persisted? present user, with: Entities::UserBasic else not_modified! end end desc 'Unfollow a user' do success Entities::User end params do requires :id, type: Integer, desc: 'The ID of the user' end post ':id/unfollow', feature_category: :user_profile do user = find_user(params[:id]) not_found!('User') unless user if current_user.unfollow(user) present user, with: Entities::UserBasic else not_modified! end end desc 'Get the users who follow a user' do success Entities::UserBasic end params do requires :id, type: Integer, desc: 'The ID of the user' use :pagination end get ':id/following', feature_category: :user_profile do forbidden!('Not authorized!') unless current_user user = find_user(params[:id]) not_found!('User') unless user && can?(current_user, :read_user_profile, user) present paginate(user.followees), with: Entities::UserBasic end desc 'Get the followers of a user' do success Entities::UserBasic end params do requires :id, type: Integer, desc: 'The ID of the user' use :pagination end get ':id/followers', feature_category: :user_profile do forbidden!('Not authorized!') unless current_user user = find_user(params[:id]) not_found!('User') unless user && can?(current_user, :read_user_profile, user) present paginate(user.followers), with: Entities::UserBasic end desc 'Create a user. Available only for admins.' do success Entities::UserWithAdmin end params do requires :email, type: String, desc: 'The email of the user' optional :password, type: String, desc: 'The password of the new user' optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token' optional :skip_confirmation, type: Boolean, desc: 'Flag indicating the account is confirmed' at_least_one_of :password, :reset_password, :force_random_password requires :name, type: String, desc: 'The name of the user' requires :username, type: String, desc: 'The username of the user' optional :force_random_password, type: Boolean, desc: 'Flag indicating a random password will be set' use :optional_attributes end post feature_category: :user_profile do authenticated_as_admin! params = declared_params(include_missing: false) # TODO: Remove in 16.0. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/387005 if params.key?(:private_profile) && params[:private_profile].nil? params[:private_profile] = Gitlab::CurrentSettings.user_defaults_to_private_profile end user = ::Users::AuthorizedCreateService.new(current_user, params).execute if user.persisted? present user, with: Entities::UserWithAdmin, current_user: current_user else conflict!('Email has already been taken') if User .by_any_email(user.email.downcase) .any? conflict!('Username has already been taken') if User .by_username(user.username) .any? track_weak_password_error(user, 'API::Users', 'create') render_validation_error!(user) end end desc 'Update a user. Available only for admins.' do success Entities::UserWithAdmin end params do requires :id, type: Integer, desc: 'The ID of the user' optional :email, type: String, desc: 'The email of the user' optional :password, type: String, desc: 'The password of the new user' optional :skip_reconfirmation, type: Boolean, desc: 'Flag indicating the account skips the confirmation by email' optional :name, type: String, desc: 'The name of the user' optional :username, type: String, desc: 'The username of the user' use :optional_attributes end # rubocop: disable CodeReuse/ActiveRecord put ":id", feature_category: :user_profile do authenticated_as_admin! user = User.find_by(id: params.delete(:id)) not_found!('User') unless user conflict!('Email has already been taken') if params[:email] && User.by_any_email(params[:email].downcase) .where.not(id: user.id).exists? conflict!('Username has already been taken') if params[:username] && User.by_username(params[:username]) .where.not(id: user.id).exists? user_params = declared_params(include_missing: false) # TODO: Remove in 16.0. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/387005 if user_params.key?(:private_profile) && user_params[:private_profile].nil? user_params[:private_profile] = Gitlab::CurrentSettings.user_defaults_to_private_profile end admin_making_changes_for_another_user = (current_user != user) if user_params[:password].present? user_params[:password_expires_at] = Time.current if admin_making_changes_for_another_user end result = ::Users::UpdateService.new(current_user, user_params.merge(user: user)).execute do |user| user.send_only_admin_changed_your_password_notification! if admin_making_changes_for_another_user end if result[:status] == :success present user, with: Entities::UserWithAdmin, current_user: current_user else track_weak_password_error(user, 'API::Users', 'update') render_validation_error!(user) end end # rubocop: enable CodeReuse/ActiveRecord desc "Disable two factor authentication for a user. Available only for admins" do detail 'This feature was added in GitLab 15.2' success Entities::UserWithAdmin end params do requires :id, type: Integer, desc: 'The ID of the user' end patch ":id/disable_two_factor", feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by_id(params[:id]) not_found!('User') unless user # We're disabling Cop/UserAdmin because it checks if the given user (not the current user) is an admin. forbidden!('Two-factor authentication for admins cannot be disabled via the API. Use the Rails console') if user.admin? # rubocop:disable Cop/UserAdmin result = TwoFactor::DestroyService.new(current_user, user: user).execute if result[:status] == :success no_content! else bad_request!(result[:message]) end end desc "Delete a user's identity. Available only for admins" do success Entities::UserWithAdmin end params do requires :id, type: Integer, desc: 'The ID of the user' requires :provider, type: String, desc: 'The external provider' end # rubocop: disable CodeReuse/ActiveRecord delete ":id/identities/:provider", feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user identity = user.identities.find_by(provider: params[:provider]) not_found!('Identity') unless identity destroy_conditionally!(identity) end # rubocop: enable CodeReuse/ActiveRecord desc 'Get the project-level Deploy keys that a specified user can access to.' do success Entities::DeployKey end params do requires :user_id, type: String, desc: 'The ID or username of the user' use :pagination end get ':user_id/project_deploy_keys', requirements: API::USER_REQUIREMENTS, feature_category: :continuous_delivery do user = find_user(params[:user_id]) not_found!('User') unless user && can?(current_user, :read_user, user) project_ids = Project.visible_to_user_and_access_level(current_user, Gitlab::Access::MAINTAINER) unless current_user == user # Restrict to only common projects of both current_user and user. project_ids = project_ids.visible_to_user_and_access_level(user, Gitlab::Access::MAINTAINER) end forbidden!('No common authorized project found') unless project_ids.present? keys = DeployKey.in_projects(project_ids) present paginate(keys), with: Entities::DeployKey end desc 'Add an SSH key to a specified user. Available only for admins.' do success Entities::SSHKey end params do requires :user_id, type: Integer, desc: 'The ID of the user' requires :key, type: String, desc: 'The new SSH key' requires :title, type: String, desc: 'The title of the new SSH key' optional :expires_at, type: DateTime, desc: 'The expiration date of the SSH key in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)' optional :usage_type, type: String, values: Key.usage_types.keys, default: 'auth_and_signing', desc: 'Scope of usage for the SSH key' end # rubocop: disable CodeReuse/ActiveRecord post ":user_id/keys", feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params.delete(:user_id)) not_found!('User') unless user key = ::Keys::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute if key.persisted? present key, with: Entities::SSHKey else render_validation_error!(key) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Get the SSH keys of a specified user.' do success Entities::SSHKey end params do requires :user_id, type: String, desc: 'The ID or username of the user' use :pagination end get ':user_id/keys', requirements: API::USER_REQUIREMENTS, feature_category: :authentication_and_authorization do user = find_user(params[:user_id]) not_found!('User') unless user && can?(current_user, :read_user, user) keys = user.keys.preload_users present paginate(keys), with: Entities::SSHKey end desc 'Get a SSH key of a specified user.' do success Entities::SSHKey end params do requires :id, type: Integer, desc: 'The ID of the user' requires :key_id, type: Integer, desc: 'The ID of the SSH key' end get ':id/keys/:key_id', requirements: API::USER_REQUIREMENTS, feature_category: :authentication_and_authorization do user = find_user(params[:id]) not_found!('User') unless user && can?(current_user, :read_user, user) key = user.keys.find_by(id: params[:key_id]) # rubocop: disable CodeReuse/ActiveRecord not_found!('Key') unless key present key, with: Entities::SSHKey end desc 'Delete an existing SSH key from a specified user. Available only for admins.' do success Entities::SSHKey end params do requires :id, type: Integer, desc: 'The ID of the user' requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord delete ':id/keys/:key_id', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user key = user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key destroy_conditionally!(key) do |key| destroy_service = ::Keys::DestroyService.new(current_user) destroy_service.execute(key) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Add a GPG key to a specified user. Available only for admins.' do detail 'This feature was added in GitLab 10.0' success Entities::GpgKey end params do requires :id, type: Integer, desc: 'The ID of the user' requires :key, type: String, desc: 'The new GPG key' end # rubocop: disable CodeReuse/ActiveRecord post ':id/gpg_keys', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params.delete(:id)) not_found!('User') unless user key = ::GpgKeys::CreateService.new(user, declared_params(include_missing: false)).execute if key.persisted? present key, with: Entities::GpgKey else render_validation_error!(key) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Get the GPG keys of a specified user.' do detail 'This feature was added in GitLab 10.0' success Entities::GpgKey end params do requires :id, type: Integer, desc: 'The ID of the user' use :pagination end # rubocop: disable CodeReuse/ActiveRecord get ':id/gpg_keys', feature_category: :authentication_and_authorization do user = User.find_by(id: params[:id]) not_found!('User') unless user present paginate(user.gpg_keys), with: Entities::GpgKey end # rubocop: enable CodeReuse/ActiveRecord desc 'Get a specific GPG key for a given user.' do detail 'This feature was added in GitLab 13.5' success Entities::GpgKey end params do requires :id, type: Integer, desc: 'The ID of the user' requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord get ':id/gpg_keys/:key_id', feature_category: :authentication_and_authorization do user = User.find_by(id: params[:id]) not_found!('User') unless user key = user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key present key, with: Entities::GpgKey end # rubocop: enable CodeReuse/ActiveRecord desc 'Delete an existing GPG key from a specified user. Available only for admins.' do detail 'This feature was added in GitLab 10.0' end params do requires :id, type: Integer, desc: 'The ID of the user' requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord delete ':id/gpg_keys/:key_id', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user key = user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key destroy_conditionally!(key) do |key| destroy_service = ::GpgKeys::DestroyService.new(current_user) destroy_service.execute(key) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Revokes an existing GPG key from a specified user. Available only for admins.' do detail 'This feature was added in GitLab 10.0' end params do requires :id, type: Integer, desc: 'The ID of the user' requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord post ':id/gpg_keys/:key_id/revoke', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user key = user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key key.revoke status :accepted end # rubocop: enable CodeReuse/ActiveRecord desc 'Add an email address to a specified user. Available only for admins.' do success Entities::Email end params do requires :id, type: Integer, desc: 'The ID of the user' requires :email, type: String, desc: 'The email of the user' optional :skip_confirmation, type: Boolean, desc: 'Skip confirmation of email and assume it is verified' end # rubocop: disable CodeReuse/ActiveRecord post ":id/emails", feature_category: :user_profile do authenticated_as_admin! user = User.find_by(id: params.delete(:id)) not_found!('User') unless user email = Emails::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute if email.errors.blank? present email, with: Entities::Email else render_validation_error!(email) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Get the emails addresses of a specified user. Available only for admins.' do success Entities::Email end params do requires :id, type: Integer, desc: 'The ID of the user' use :pagination end # rubocop: disable CodeReuse/ActiveRecord get ':id/emails', feature_category: :user_profile do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user present paginate(user.emails), with: Entities::Email end # rubocop: enable CodeReuse/ActiveRecord desc 'Delete an email address of a specified user. Available only for admins.' do success Entities::Email end params do requires :id, type: Integer, desc: 'The ID of the user' requires :email_id, type: Integer, desc: 'The ID of the email' end # rubocop: disable CodeReuse/ActiveRecord delete ':id/emails/:email_id', feature_category: :user_profile do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user email = user.emails.find_by(id: params[:email_id]) not_found!('Email') unless email destroy_conditionally!(email) do |email| Emails::DestroyService.new(current_user, user: user).execute(email) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Delete a user. Available only for admins.' do success Entities::Email end params do requires :id, type: Integer, desc: 'The ID of the user' optional :hard_delete, type: Boolean, desc: "Whether to remove a user's contributions" end # rubocop: disable CodeReuse/ActiveRecord delete ":id", feature_category: :user_profile do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user conflict!('User cannot be removed while is the sole-owner of a group') unless user.can_be_removed? || params[:hard_delete] destroy_conditionally!(user) do user.delete_async(deleted_by: current_user, params: params) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Activate a deactivated user. Available only for admins.' params do requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord post ':id/activate', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user forbidden!('A blocked user must be unblocked to be activated') if user.blocked? user.activate end desc 'Approve a pending user. Available only for admins.' params do requires :id, type: Integer, desc: 'The ID of the user' end post ':id/approve', feature_category: :authentication_and_authorization do user = User.find_by(id: params[:id]) not_found!('User') unless can?(current_user, :read_user, user) result = ::Users::ApproveService.new(current_user).execute(user) if result[:success] result else render_api_error!(result[:message], result[:http_status]) end end desc 'Reject a pending user. Available only for admins.' params do requires :id, type: Integer, desc: 'The ID of the user' end post ':id/reject', feature_category: :authentication_and_authorization do user = find_user_by_id(params) result = ::Users::RejectService.new(current_user).execute(user) if result[:success] present user else render_api_error!(result[:message], result[:http_status]) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Deactivate an active user. Available only for admins.' params do requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord post ':id/deactivate', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user break if user.deactivated? unless user.can_be_deactivated? forbidden!('A blocked user cannot be deactivated by the API') if user.blocked? forbidden!('An internal user cannot be deactivated by the API') if user.internal? forbidden!("The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated") end if user.deactivate true else render_api_error!(user.errors.full_messages, 400) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Block a user. Available only for admins.' params do requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord post ':id/block', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user if user.ldap_blocked? forbidden!('LDAP blocked users cannot be modified by the API') elsif current_user == user forbidden!('The API initiating user cannot be blocked by the API') end break if user.blocked? result = ::Users::BlockService.new(current_user).execute(user) if result[:status] == :success true else render_api_error!(result[:message], result[:http_status]) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Unblock a user. Available only for admins.' params do requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord post ':id/unblock', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user if user.ldap_blocked? forbidden!('LDAP blocked users cannot be unblocked by the API') elsif user.deactivated? forbidden!('Deactivated users cannot be unblocked by the API') else result = ::Users::UnblockService.new(current_user).execute(user) result.success? end end # rubocop: enable CodeReuse/ActiveRecord desc 'Ban a user. Available only for admins.' params do requires :id, type: Integer, desc: 'The ID of the user' end post ':id/ban', feature_category: :authentication_and_authorization do authenticated_as_admin! user = find_user_by_id(params) result = ::Users::BanService.new(current_user).execute(user) if result[:status] == :success true else render_api_error!(result[:message], result[:http_status]) end end desc 'Unban a user. Available only for admins.' params do requires :id, type: Integer, desc: 'The ID of the user' end post ':id/unban', feature_category: :authentication_and_authorization do authenticated_as_admin! user = find_user_by_id(params) result = ::Users::UnbanService.new(current_user).execute(user) if result[:status] == :success true else render_api_error!(result[:message], result[:http_status]) end end desc 'Get memberships' do success Entities::Membership end params do requires :user_id, type: Integer, desc: 'The ID of the user' optional :type, type: String, values: %w[Project Namespace] use :pagination end get ":user_id/memberships", feature_category: :user_profile, urgency: :high do authenticated_as_admin! user = find_user_by_id(params) members = case params[:type] when 'Project' user.project_members when 'Namespace' user.group_members else user.members end members = members.including_source present paginate(members), with: Entities::Membership end params do requires :user_id, type: Integer, desc: 'The ID of the user' end segment ':user_id' do resource :impersonation_tokens do helpers do def finder(options = {}) user = find_user_by_id(params) PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options)) end def find_impersonation_token finder.find_by_id(declared_params[:impersonation_token_id]) || not_found!('Impersonation Token') end end before { authenticated_as_admin! } desc 'Retrieve impersonation tokens. Available only for admins.' do detail 'This feature was introduced in GitLab 9.0' success Entities::ImpersonationToken end params do use :pagination optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) impersonation_tokens' end get feature_category: :authentication_and_authorization do present paginate(finder(declared_params(include_missing: false)).execute), with: Entities::ImpersonationToken end desc 'Create a impersonation token. Available only for admins.' do detail 'This feature was introduced in GitLab 9.0' success Entities::ImpersonationTokenWithToken end params do requires :name, type: String, desc: 'The name of the impersonation token' optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the impersonation token' optional :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The array of scopes of the impersonation token' end post feature_category: :authentication_and_authorization do impersonation_token = finder.build(declared_params(include_missing: false)) if impersonation_token.save present impersonation_token, with: Entities::ImpersonationTokenWithToken else render_validation_error!(impersonation_token) end end desc 'Retrieve impersonation token. Available only for admins.' do detail 'This feature was introduced in GitLab 9.0' success Entities::ImpersonationToken end params do requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token' end get ':impersonation_token_id', feature_category: :authentication_and_authorization do present find_impersonation_token, with: Entities::ImpersonationToken end desc 'Revoke a impersonation token. Available only for admins.' do detail 'This feature was introduced in GitLab 9.0' end params do requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token' end delete ':impersonation_token_id', feature_category: :authentication_and_authorization do token = find_impersonation_token destroy_conditionally!(token) do token.revoke! end end end resource :personal_access_tokens do helpers do def target_user find_user_by_id(params) end end before { authenticated_as_admin! } desc 'Create a personal access token. Available only for admins.' do detail 'This feature was introduced in GitLab 13.6' success Entities::PersonalAccessTokenWithToken end params do requires :name, type: String, desc: 'The name of the personal access token' requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::Gitlab::Auth.all_available_scopes.map(&:to_s), desc: 'The array of scopes of the personal access token' optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token' end post feature_category: :authentication_and_authorization do response = ::PersonalAccessTokens::CreateService.new( current_user: current_user, target_user: target_user, params: declared_params(include_missing: false) ).execute if response.success? present response.payload[:personal_access_token], with: Entities::PersonalAccessTokenWithToken else render_api_error!(response.message, response.http_status || :unprocessable_entity) end end end end end resource :user do before do authenticate! end # Enabling /user endpoint for the v3 version to allow oauth # authentication through this endpoint. version %w(v3 v4), using: :path do desc 'Get the currently authenticated user' do success Entities::UserPublic end get feature_category: :user_profile, urgency: :low do entity = # We're disabling Cop/UserAdmin because it checks if the given user is an admin. if current_user.admin? # rubocop:disable Cop/UserAdmin Entities::UserWithAdmin else Entities::UserPublic end present current_user, with: entity, current_user: current_user end end helpers do def set_user_status(include_missing_params:) forbidden! unless can?(current_user, :update_user_status, current_user) if ::Users::SetStatusService.new(current_user, declared_params(include_missing: include_missing_params)).execute present current_user.status, with: Entities::UserStatus else render_validation_error!(current_user.status) end end params :set_user_status_params do optional :emoji, type: String, desc: "The emoji to set on the status" optional :message, type: String, desc: "The status message to set" optional :availability, type: String, desc: "The availability of user to set" optional :clear_status_after, type: String, desc: "Automatically clear emoji, message and availability fields after a certain time", values: UserStatus::CLEAR_STATUS_QUICK_OPTIONS.keys end end desc "Get the currently authenticated user's SSH keys" do success Entities::SSHKey end params do use :pagination end get "keys", feature_category: :authentication_and_authorization do keys = current_user.keys.preload_users present paginate(keys), with: Entities::SSHKey end desc 'Get a single key owned by currently authenticated user' do success Entities::SSHKey end params do requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord get "keys/:key_id", feature_category: :authentication_and_authorization do key = current_user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key present key, with: Entities::SSHKey end # rubocop: enable CodeReuse/ActiveRecord desc 'Add a new SSH key to the currently authenticated user' do success Entities::SSHKey end params do requires :key, type: String, desc: 'The new SSH key' requires :title, type: String, desc: 'The title of the new SSH key' optional :expires_at, type: DateTime, desc: 'The expiration date of the SSH key in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)' optional :usage_type, type: String, values: Key.usage_types.keys, default: 'auth_and_signing', desc: 'Scope of usage for the SSH key' end post "keys", feature_category: :authentication_and_authorization do key = ::Keys::CreateService.new(current_user, declared_params(include_missing: false)).execute if key.persisted? present key, with: Entities::SSHKey else render_validation_error!(key) end end desc 'Delete an SSH key from the currently authenticated user' do success Entities::SSHKey end params do requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord delete "keys/:key_id", feature_category: :authentication_and_authorization do key = current_user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key destroy_conditionally!(key) do |key| destroy_service = ::Keys::DestroyService.new(current_user) destroy_service.execute(key) end end # rubocop: enable CodeReuse/ActiveRecord desc "Get the currently authenticated user's GPG keys" do detail 'This feature was added in GitLab 10.0' success Entities::GpgKey end params do use :pagination end get 'gpg_keys', feature_category: :authentication_and_authorization do present paginate(current_user.gpg_keys), with: Entities::GpgKey end desc 'Get a single GPG key owned by currently authenticated user' do detail 'This feature was added in GitLab 10.0' success Entities::GpgKey end params do requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord get 'gpg_keys/:key_id', feature_category: :authentication_and_authorization do key = current_user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key present key, with: Entities::GpgKey end # rubocop: enable CodeReuse/ActiveRecord desc 'Add a new GPG key to the currently authenticated user' do detail 'This feature was added in GitLab 10.0' success Entities::GpgKey end params do requires :key, type: String, desc: 'The new GPG key' end post 'gpg_keys', feature_category: :authentication_and_authorization do key = ::GpgKeys::CreateService.new(current_user, declared_params(include_missing: false)).execute if key.persisted? present key, with: Entities::GpgKey else render_validation_error!(key) end end desc 'Revoke a GPG key owned by currently authenticated user' do detail 'This feature was added in GitLab 10.0' end params do requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord post 'gpg_keys/:key_id/revoke', feature_category: :authentication_and_authorization do key = current_user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key key.revoke status :accepted end # rubocop: enable CodeReuse/ActiveRecord desc 'Delete a GPG key from the currently authenticated user' do detail 'This feature was added in GitLab 10.0' end params do requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord delete 'gpg_keys/:key_id', feature_category: :authentication_and_authorization do key = current_user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key destroy_conditionally!(key) do |key| destroy_service = ::GpgKeys::DestroyService.new(current_user) destroy_service.execute(key) end end # rubocop: enable CodeReuse/ActiveRecord desc "Get the currently authenticated user's email addresses" do success Entities::Email end params do use :pagination end get "emails", feature_category: :user_profile, urgency: :high do present paginate(current_user.emails), with: Entities::Email end desc "Update a user's credit_card_validation" do success Entities::UserCreditCardValidations end params do requires :user_id, type: String, desc: 'The ID or username of the user' requires :credit_card_validated_at, type: DateTime, desc: 'The time when the user\'s credit card was validated' requires :credit_card_expiration_month, type: Integer, desc: 'The month the credit card expires' requires :credit_card_expiration_year, type: Integer, desc: 'The year the credit card expires' requires :credit_card_holder_name, type: String, desc: 'The credit card holder name' requires :credit_card_mask_number, type: String, desc: 'The last 4 digits of credit card number' requires :credit_card_type, type: String, desc: 'The credit card network name' end put ":user_id/credit_card_validation", urgency: :low, feature_category: :purchase do authenticated_as_admin! user = find_user(params[:user_id]) not_found!('User') unless user attrs = declared_params(include_missing: false) service = ::Users::UpsertCreditCardValidationService.new(attrs, user).execute if service.success? present user.credit_card_validation, with: Entities::UserCreditCardValidations else render_api_error!('400 Bad Request', 400) end end desc "Update the current user's preferences" do success Entities::UserPreferences detail 'This feature was introduced in GitLab 13.10.' end params do optional :view_diffs_file_by_file, type: Boolean, desc: 'Flag indicating the user sees only one file diff per page' optional :show_whitespace_in_diffs, type: Boolean, desc: 'Flag indicating the user sees whitespace changes in diffs' at_least_one_of :view_diffs_file_by_file, :show_whitespace_in_diffs end put "preferences", feature_category: :user_profile, urgency: :high do authenticate! preferences = current_user.user_preference attrs = declared_params(include_missing: false) service = ::UserPreferences::UpdateService.new(current_user, attrs).execute if service.success? present preferences, with: Entities::UserPreferences else render_api_error!('400 Bad Request', 400) end end desc "Get the current user's preferences" do success Entities::UserPreferences detail 'This feature was introduced in GitLab 14.0.' end get "preferences", feature_category: :user_profile do present current_user.user_preference, with: Entities::UserPreferences end desc 'Get a single email address owned by the currently authenticated user' do success Entities::Email end params do requires :email_id, type: Integer, desc: 'The ID of the email' end # rubocop: disable CodeReuse/ActiveRecord get "emails/:email_id", feature_category: :user_profile do email = current_user.emails.find_by(id: params[:email_id]) not_found!('Email') unless email present email, with: Entities::Email end # rubocop: enable CodeReuse/ActiveRecord desc 'Add new email address to the currently authenticated user' do success Entities::Email end params do requires :email, type: String, desc: 'The new email' end post "emails", feature_category: :user_profile do email = Emails::CreateService.new(current_user, declared_params.merge(user: current_user)).execute if email.errors.blank? present email, with: Entities::Email else render_validation_error!(email) end end desc 'Delete an email address from the currently authenticated user' params do requires :email_id, type: Integer, desc: 'The ID of the email' end # rubocop: disable CodeReuse/ActiveRecord delete "emails/:email_id", feature_category: :user_profile do email = current_user.emails.find_by(id: params[:email_id]) not_found!('Email') unless email destroy_conditionally!(email) do |email| Emails::DestroyService.new(current_user, user: current_user).execute(email) end end # rubocop: enable CodeReuse/ActiveRecord desc 'Get a list of user activities' params do optional :from, type: DateTime, default: 6.months.ago, desc: 'Date string in the format YEAR-MONTH-DAY' use :pagination end # rubocop: disable CodeReuse/ActiveRecord get "activities", feature_category: :user_profile do authenticated_as_admin! activities = User .where(User.arel_table[:last_activity_on].gteq(params[:from])) .reorder(last_activity_on: :asc) present paginate(activities), with: Entities::UserActivity end # rubocop: enable CodeReuse/ActiveRecord desc 'Set the status of the current user' do success Entities::UserStatus detail 'Any parameters that are not passed will be nullified.' end params do use :set_user_status_params end put "status", feature_category: :user_profile do set_user_status(include_missing_params: true) end desc 'Set the status of the current user' do success Entities::UserStatus detail 'Any parameters that are not passed will be ignored.' end params do use :set_user_status_params end patch "status", feature_category: :user_profile do if declared_params(include_missing: false).empty? status :ok break end set_user_status(include_missing_params: false) end desc 'get the status of the current user' do success Entities::UserStatus end get 'status', feature_category: :user_profile do present current_user.status || {}, with: Entities::UserStatus end end end end API::Users.prepend_mod