From 0aeede2d11dbc61af427d2c51a3e6d9cd3aa063f Mon Sep 17 00:00:00 2001 From: Abdul Wadood <awadood@gitlab.com> Date: Mon, 23 Sep 2024 08:37:19 +0000 Subject: [PATCH] Add a REST API to create an organization This change introduces a new POST API for organization creation. The functionality is controlled by a feature flag named 'allow_organization_creation'. The change also modifies workhorse to enable file uploads on the '/api/v4/organizations' endpoint. --- app/helpers/application_settings_helper.rb | 1 + app/models/application_setting.rb | 2 + .../application_setting_implementation.rb | 1 + .../application_setting_rate_limits.json | 5 + .../_organizations_api_limits.html.haml | 20 ++ .../application_settings/network.html.haml | 2 + .../rate_limit_on_organizations_api.md | 36 ++++ doc/api/organizations.md | 63 +++++++ doc/security/rate_limits.md | 1 + lib/api/api.rb | 1 + .../entities/organizations/organization.rb | 23 +++ lib/api/helpers.rb | 2 +- lib/api/organizations.rb | 47 +++++ lib/gitlab/application_rate_limiter.rb | 1 + locale/gitlab.pot | 9 + spec/features/admin/admin_settings_spec.rb | 14 ++ .../application_settings_helper_spec.rb | 1 + .../organizations/organization_spec.rb | 27 +++ spec/models/application_setting_spec.rb | 2 + spec/requests/api/organizations_spec.rb | 175 ++++++++++++++++++ .../network.html.haml_spec.rb | 12 +- .../_support/lint_last_known_acceptable.txt | 4 +- workhorse/cmd/gitlab-workhorse/upload_test.go | 2 + workhorse/internal/upstream/routes.go | 6 + 24 files changed, 452 insertions(+), 5 deletions(-) create mode 100644 app/views/admin/application_settings/_organizations_api_limits.html.haml create mode 100644 doc/administration/settings/rate_limit_on_organizations_api.md create mode 100644 doc/api/organizations.md create mode 100644 lib/api/entities/organizations/organization.rb create mode 100644 lib/api/organizations.rb create mode 100644 spec/lib/api/entities/organizations/organization_spec.rb create mode 100644 spec/requests/api/organizations_spec.rb diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 9c6f757a2ce74..1ebff2a375f64 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -515,6 +515,7 @@ def visible_attributes :project_api_limit, :project_invited_groups_api_limit, :projects_api_limit, + :create_organization_api_limit, :user_contributed_projects_api_limit, :user_projects_api_limit, :user_starred_projects_api_limit, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index afe5ffdc3edc2..d19ea0f41ea70 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -599,6 +599,7 @@ def self.kroki_formats_attributes :max_terraform_state_size_bytes, :members_delete_limit, :notes_create_limit, + :create_organization_api_limit, :package_registry_cleanup_policies_worker_capacity, :packages_cleanup_package_file_worker_capacity, :pages_extra_deployments_default_expiry_seconds, @@ -630,6 +631,7 @@ def self.kroki_formats_attributes group_shared_groups_api_limit: [:integer, { default: 60 }], groups_api_limit: [:integer, { default: 200 }], members_delete_limit: [:integer, { default: 60 }], + create_organization_api_limit: [:integer, { default: 10 }], project_api_limit: [:integer, { default: 400 }], project_invited_groups_api_limit: [:integer, { default: 60 }], projects_api_limit: [:integer, { default: 2000 }], diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index d66e47476d090..342e76f06d44d 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -285,6 +285,7 @@ def defaults # rubocop:disable Metrics/AbcSize group_projects_api_limit: 600, group_shared_groups_api_limit: 60, groups_api_limit: 200, + create_organization_api_limit: 10, project_api_limit: 400, project_invited_groups_api_limit: 60, projects_api_limit: 2000, diff --git a/app/validators/json_schemas/application_setting_rate_limits.json b/app/validators/json_schemas/application_setting_rate_limits.json index 04d8deedb3786..24c27737cd6bc 100644 --- a/app/validators/json_schemas/application_setting_rate_limits.json +++ b/app/validators/json_schemas/application_setting_rate_limits.json @@ -19,6 +19,11 @@ "minimum": 1, "description": "Maximum number of simultaneous import jobs for GitHub importer" }, + "create_organization_api_limit": { + "type": "integer", + "minimum": 0, + "description": "Number of requests allowed to the POST /api/v4/organizations API." + }, "group_api_limit": { "type": "integer", "minimum": 0, diff --git a/app/views/admin/application_settings/_organizations_api_limits.html.haml b/app/views/admin/application_settings/_organizations_api_limits.html.haml new file mode 100644 index 0000000000000..fbdd8999318d0 --- /dev/null +++ b/app/views/admin/application_settings/_organizations_api_limits.html.haml @@ -0,0 +1,20 @@ += render ::Layouts::SettingsBlockComponent.new(_('Organizations API rate limits'), + id: 'js-organizations-api-limits-settings', + testid: 'organizations-api-limits-settings', + expanded: expanded_by_default?) do |c| + - c.with_description do + = _('Set the per-user rate limits for the requests to Organizations API.') + = link_to _('Learn more.'), help_page_path('administration/settings/rate_limit_on_organizations_api'), target: '_blank', rel: 'noopener noreferrer' + - c.with_body do + = gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-organizations-api-limits-settings'), html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + = _("Set to 0 to disable the limits.") + + %fieldset + .form-group + = f.label :create_organization_api_limit, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user'), api_name: 'POST /organizations', timeframe: 'minute'), class: 'label-bold' + = f.number_field :create_organization_api_limit, min: 0, class: 'form-control gl-form-input' + + = f.submit _('Save changes'), pajamas_button: true diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml index dedeef6363ff0..30d4feff27274 100644 --- a/app/views/admin/application_settings/network.html.haml +++ b/app/views/admin/application_settings/network.html.haml @@ -133,6 +133,8 @@ = render 'projects_api_limits' += render 'organizations_api_limits' + = render 'members_api_limits' = render ::Layouts::SettingsBlockComponent.new(_('Import and export rate limits'), diff --git a/doc/administration/settings/rate_limit_on_organizations_api.md b/doc/administration/settings/rate_limit_on_organizations_api.md new file mode 100644 index 0000000000000..5531688378f18 --- /dev/null +++ b/doc/administration/settings/rate_limit_on_organizations_api.md @@ -0,0 +1,36 @@ +--- +stage: Data Stores +group: Tenant Scale +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Rate limit on Organizations API + +DETAILS: +**Tier:** Free, Premium, Ultimate +**Offering:** GitLab.com, Self-managed +**Status:** Experiment + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/470613) in GitLab 17.5 with a [flag](../feature_flags.md) named `allow_organization_creation`. Disabled by default. This feature is an [experiment](../../policy/experiment-beta-support.md). + +FLAG: +The availability of this feature is controlled by a feature flag. +For more information, see the history. + +Requests over the rate limit are logged into the `auth.log` file. + +For example, if you set a limit of 400 for `POST /organizations`, requests to the API endpoint that +exceed a rate of 400 within one minute are blocked. Access to the endpoint is restored after one minute. + +You can configure the per minute rate limit per user for requests to the [POST /organizations API](../../api/organizations.md#create-organization). The default is 10. + +## Change the rate limit + +To change the rate limit: + +1. On the left sidebar, at the bottom, select **Admin**. +1. Select **Settings > Network**. +1. Expand **Organizations API rate limits**. +1. Change the value of any rate limit. The rate limits are per minute per user. + To disable a rate limit, set the value to `0`. +1. Select **Save changes**. diff --git a/doc/api/organizations.md b/doc/api/organizations.md new file mode 100644 index 0000000000000..68629c38c32af --- /dev/null +++ b/doc/api/organizations.md @@ -0,0 +1,63 @@ +--- +stage: Data Stores +group: Tenant Scale +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Organizations API + +DETAILS: +**Tier:** Free, Premium, Ultimate +**Offering:** GitLab.com, Self-managed +**Status:** Experiment + +## Create organization + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/470613) in GitLab 17.5 with a [flag](../administration/feature_flags.md) named `allow_organization_creation`. Disabled by default. This feature is an [experiment](../policy/experiment-beta-support.md). + +FLAG: +The availability of this feature is controlled by a feature flag. +For more information, see the history. + +Creates a new organization. + +This endpoint is an [experiment](../policy/experiment-beta-support.md) and might be changed or removed without notice. + +```plaintext +POST /organizations +``` + +Parameters: + +| Attribute | Type | Required | Description | +|---------------|--------|----------|---------------------------------------| +| `name` | string | yes | The name of the organization | +| `path` | string | yes | The path of the organization | +| `description` | string | no | The description of the organization | +| `avatar` | file | no | The avatar image for the organization | + +Example request: + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ +--form "name=New Organization" \ +--form "path=new-org" \ +--form "description=A new organization" \ +--form "avatar=@/path/to/avatar.png" \ +"https://gitlab.example.com/api/v4/organizations" +``` + +Example response: + +```json +{ + "id": 42, + "name": "New Organization", + "path": "new-org", + "description": "A new organization", + "created_at": "2024-09-18T02:35:15.371Z", + "updated_at": "2024-09-18T02:35:15.371Z", + "web_url": "https://gitlab.example.com/-/organizations/new-org", + "avatar_url": "https://gitlab.example.com/uploads/-/system/organizations/organization_detail/avatar/42/avatar.png" +} +``` diff --git a/doc/security/rate_limits.md b/doc/security/rate_limits.md index 01f28ce93ce08..8c1bffee92dc1 100644 --- a/doc/security/rate_limits.md +++ b/doc/security/rate_limits.md @@ -53,6 +53,7 @@ You can set these rate limits in the **Admin** area of your instance: - [Incident management rate limits](../administration/settings/incident_management_rate_limits.md) - [Projects API rate limits](../administration/settings/rate_limit_on_projects_api.md) - [Groups API rate limits](../administration/settings/rate_limit_on_groups_api.md) +- [Organizations API rate limits](../administration/settings/rate_limit_on_organizations_api.md) You can set these rate limits using the Rails console: diff --git a/lib/api/api.rb b/lib/api/api.rb index 010f93cb143e1..03d9db45a10a7 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -305,6 +305,7 @@ def initialize(location_url) mount ::API::NpmProjectPackages mount ::API::NugetGroupPackages mount ::API::NugetProjectPackages + mount ::API::Organizations mount ::API::PackageFiles mount ::API::Pages mount ::API::PagesDomains diff --git a/lib/api/entities/organizations/organization.rb b/lib/api/entities/organizations/organization.rb new file mode 100644 index 0000000000000..f389bdfc76915 --- /dev/null +++ b/lib/api/entities/organizations/organization.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module API + module Entities + module Organizations + class Organization < Grape::Entity + expose :id, documentation: { type: 'integer', example: 1 } + expose :name, documentation: { type: 'string', example: 'GitLab' } + expose :path, documentation: { type: 'string', example: 'gitlab' } + expose :description, documentation: { type: 'string', example: 'My description' } + expose :created_at, documentation: { type: 'dateTime', example: '2022-02-24T20:22:30.097Z' } + expose :updated_at, documentation: { type: 'dateTime', example: '2022-02-24T20:22:30.097Z' } + expose :web_url, documentation: { type: "string", example: "https://example.com/-/organizations/gitlab" } + expose(:avatar_url, documentation: { + type: 'string', + example: 'https://example.com/uploads/-/system/organizations/organization_detail/avatar/1/avatar.png' + }) do |organization, _options| + organization.avatar_url(only_path: false) + end + end + end + end +end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index f72041dbd0060..709e0aac2e4eb 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -202,7 +202,7 @@ def check_pipeline_access(pipeline) end def find_organization!(id) - organization = Organizations::Organization.find_by_id(id) + organization = ::Organizations::Organization.find_by_id(id) check_organization_access(organization) end diff --git a/lib/api/organizations.rb b/lib/api/organizations.rb new file mode 100644 index 0000000000000..dbc15191e5f79 --- /dev/null +++ b/lib/api/organizations.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module API + class Organizations < ::API::Base + feature_category :cell + + before { authenticate! } + + helpers do + def authorize_organization_creation! + authorize! :create_organization + end + end + + resource :organizations do + desc 'Create an organization' do + detail 'This feature was introduced in GitLab 17.5. \ + This feature is currently in an experimental state. \ + This feature is behind the `allow_organization_creation` feature flag.' + success Entities::Organizations::Organization + tags %w[organizations] + end + params do + requires :name, type: String, desc: 'The name of the organization' + requires :path, type: String, desc: 'The path of the organization' + optional :description, type: String, desc: 'The description of the organization' + optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'The avatar image for the organization', + documentation: { type: 'file' } + end + post do + forbidden! unless Feature.enabled?(:allow_organization_creation, current_user) + check_rate_limit!(:create_organization_api, scope: current_user) + authorize_organization_creation! + + response = ::Organizations::CreateService + .new(current_user: current_user, params: declared_params(include_missing: false)) + .execute + + if response.success? + present response[:organization], with: Entities::Organizations::Organization + else + render_api_error!(response.message, :bad_request) + end + end + end + end +end diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb index 8352f2b52d1d6..20fffe0e3ab99 100644 --- a/lib/gitlab/application_rate_limiter.rb +++ b/lib/gitlab/application_rate_limiter.rb @@ -34,6 +34,7 @@ def rate_limits # rubocop:disable Metrics/AbcSize group_projects_api: { threshold: -> { application_settings.group_projects_api_limit }, interval: 1.minute }, groups_api: { threshold: -> { application_settings.groups_api_limit }, interval: 1.minute }, project_api: { threshold: -> { application_settings.project_api_limit }, interval: 1.minute }, + create_organization_api: { threshold: -> { application_settings.create_organization_api_limit }, interval: 1.minute }, project_invited_groups_api: { threshold: -> { application_settings.project_invited_groups_api_limit }, interval: 1.minute }, projects_api: { threshold: -> { application_settings.projects_api_limit }, interval: 10.minutes }, user_contributed_projects_api: { threshold: -> { application_settings.user_contributed_projects_api_limit }, interval: 1.minute }, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f0af516ad771a..8901b107a1b42 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -33080,6 +33080,9 @@ msgstr "" msgid "Maximum requests to the %{api_name} API per %{timeframe} per IP address for unauthenticated requests" msgstr "" +msgid "Maximum requests to the %{api_name} API per %{timeframe} per user" +msgstr "" + msgid "Maximum requests to the %{api_name} API per %{timeframe} per user for authenticated requests" msgstr "" @@ -37988,6 +37991,9 @@ msgstr "" msgid "Organizations" msgstr "" +msgid "Organizations API rate limits" +msgstr "" + msgid "Organization|%{linkStart}Organizations%{linkEnd} are a top-level container to hold your groups and projects." msgstr "" @@ -51091,6 +51097,9 @@ msgstr "" msgid "Set the per-user rate limit for notes created by web or API requests." msgstr "" +msgid "Set the per-user rate limits for the requests to Organizations API." +msgstr "" + msgid "Set this issue as blocked by %{target}." msgstr "" diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index da013289d1ca3..2ada78e36cccb 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -878,6 +878,20 @@ end end + describe 'organizations API rate limits' do + let_it_be(:network_settings_section) { 'organizations-api-limits-settings' } + + context 'for POST /organizations API requests' do + let(:rate_limit_field) do + format(_('Maximum requests to the %{api_name} API per %{timeframe} per user'), api_name: 'POST /organizations', timeframe: 'minute') + end + + let(:application_setting_key) { :create_organization_api_limit } + + it_behaves_like 'API rate limit setting' + end + end + describe 'groups API rate limits' do let_it_be(:network_settings_section) { 'groups-api-limits-settings' } diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb index e899bf5aaeeb7..d30d2125633e5 100644 --- a/spec/helpers/application_settings_helper_spec.rb +++ b/spec/helpers/application_settings_helper_spec.rb @@ -71,6 +71,7 @@ group_shared_groups_api_limit group_invited_groups_api_limit project_invited_groups_api_limit + create_organization_api_limit ]) end diff --git a/spec/lib/api/entities/organizations/organization_spec.rb b/spec/lib/api/entities/organizations/organization_spec.rb new file mode 100644 index 0000000000000..f0a38b7cd0a7a --- /dev/null +++ b/spec/lib/api/entities/organizations/organization_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Organizations::Organization, feature_category: :cell do + let(:avatar_url) { 'https://example.com/uploads/-/system/organizations/organization_detail/avatar/1/avatar.png' } + let(:organization) { build_stubbed(:organization) } + + subject(:json) { described_class.new(organization).as_json } + + before do + allow(organization).to receive(:avatar_url).with(only_path: false).and_return(avatar_url) + end + + it 'exposes all the correct attributes' do + expect(json).to match_array( + id: organization.id, + name: organization.name, + path: organization.path, + description: organization.description, + created_at: organization.created_at, + updated_at: organization.updated_at, + web_url: organization.web_url, + avatar_url: avatar_url + ) + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 4939021c83bc8..14497ee4a669e 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -41,6 +41,7 @@ it { expect(setting.group_projects_api_limit).to eq(600) } it { expect(setting.group_shared_groups_api_limit).to eq(60) } it { expect(setting.groups_api_limit).to eq(200) } + it { expect(setting.create_organization_api_limit).to eq(10) } it { expect(setting.project_api_limit).to eq(400) } it { expect(setting.project_invited_groups_api_limit).to eq(60) } it { expect(setting.projects_api_limit).to eq(2000) } @@ -237,6 +238,7 @@ def many_usernames(num = 100) package_registry_cleanup_policies_worker_capacity packages_cleanup_package_file_worker_capacity pipeline_limit_per_project_user_sha + create_organization_api_limit project_api_limit projects_api_limit projects_api_rate_limit_unauthenticated diff --git a/spec/requests/api/organizations_spec.rb b/spec/requests/api/organizations_spec.rb new file mode 100644 index 0000000000000..772521855223a --- /dev/null +++ b/spec/requests/api/organizations_spec.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Organizations, feature_category: :cell do + include WorkhorseHelpers + + let(:user) { create(:user) } + + shared_examples 'organization avatar upload' do + context 'when valid' do + let(:file_path) { 'spec/fixtures/banana_sample.gif' } + + it 'returns avatar url in response' do + make_upload_request + + organization_id = json_response['id'] + avatar_url = "http://localhost/uploads/-/system/organizations/organization_detail/avatar/#{organization_id}/banana_sample.gif" + expect(json_response['avatar_url']).to eq(avatar_url) + end + end + + context 'when invalid' do + shared_examples 'invalid file upload request' do + it 'returns 400', :aggregate_failures do + make_upload_request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.message).to eq('Bad Request') + expect(json_response['message'].to_s).to match(/#{message}/) + end + end + + context 'when file format is not supported' do + let(:file_path) { 'spec/fixtures/doc_sample.txt' } + let(:message) { 'file format is not supported. Please try one of the following supported formats: image/png' } + + it_behaves_like 'invalid file upload request' + end + + context 'when file is too large' do + let(:file_path) { 'spec/fixtures/big-image.png' } + let(:message) { 'is too big' } + + it_behaves_like 'invalid file upload request' + end + end + end + + describe 'POST /organizations' do + let(:base_params) do + { + name: 'New Organization', + path: 'new-org', + description: 'A new organization' + } + end + + let(:params) { base_params } + + context 'when user is not authorized' do + it 'returns unauthorized' do + post api("/organizations"), params: params + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(allow_organization_creation: false) + end + + it 'returns forbidden' do + post api("/organizations", user), params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when user is authorized' do + it_behaves_like 'organization avatar upload' do + def make_upload_request + params_with_file_upload = params.merge(avatar: fixture_file_upload(file_path)) + + workhorse_form_with_file( + api('/organizations', user), + method: :post, + file_key: :avatar, + params: params_with_file_upload + ) + end + end + + it_behaves_like 'rate limited endpoint', rate_limit_key: :create_organization_api do + let(:current_user) { user } + + def request + post api("/organizations", user), params: params + end + end + + shared_examples 'returns bad request' do + specify do + post api("/organizations", user), params: params + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + it 'creates a new organization' do + post api("/organizations", user), params: params + + expect(response).to have_gitlab_http_status(:success) + expect(json_response['name']).to eq('New Organization') + expect(json_response['path']).to eq('new-org') + expect(json_response['description']).to eq('A new organization') + end + + context 'when optional params are missing' do + context 'with missing description' do + let(:params) { base_params.except(:description) } + + it 'creates a new organization' do + post api("/organizations", user), params: params + + expect(response).to have_gitlab_http_status(:success) + expect(json_response['name']).to eq('New Organization') + expect(json_response['path']).to eq('new-org') + end + end + end + + context 'when required params are missing' do + context 'with missing name' do + let(:params) { base_params.except(:name) } + + it_behaves_like 'returns bad request' + end + + context 'with missing path' do + let(:params) { base_params.except(:path) } + + it_behaves_like 'returns bad request' + end + end + + context 'when organization creation fails' do + it 'returns an error message' do + message = _('Failed to create organization') + allow_next_instance_of(::Organizations::CreateService) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: Array(message))) + end + + post api("/organizations", user), params: params + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to match_array(message) + end + end + + context 'when organization creation is disable by admin' do + before do + stub_application_setting(can_create_organization: false) + end + + it 'returns forbidden' do + post api("/organizations", user), params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + end +end diff --git a/spec/views/admin/application_settings/network.html.haml_spec.rb b/spec/views/admin/application_settings/network.html.haml_spec.rb index 7ce94a24e15be..25995fb6929cc 100644 --- a/spec/views/admin/application_settings/network.html.haml_spec.rb +++ b/spec/views/admin/application_settings/network.html.haml_spec.rb @@ -22,7 +22,7 @@ end context 'for Projects API rate limits' do - it 'renders the `projects_api_rate_limit_unauthenticated` field' do + it 'renders the project rate limit fields' do render expect(rendered).to have_field('application_setting_projects_api_rate_limit_unauthenticated') @@ -35,7 +35,7 @@ end context 'for Groups API rate limits' do - it 'renders the `projects_api_rate_limit_unauthenticated` field' do + it 'renders the group rate limit fields' do render expect(rendered).to have_field('application_setting_groups_api_limit') @@ -44,6 +44,14 @@ end end + context 'for Organizations API rate limits' do + it 'renders the organization rate limit fields' do + render + + expect(rendered).to have_field('application_setting_create_organization_api_limit') + end + end + context 'for Members API rate limit' do it 'renders the `members_delete_limit` field' do render diff --git a/workhorse/_support/lint_last_known_acceptable.txt b/workhorse/_support/lint_last_known_acceptable.txt index d3f4a8c20921c..0f76c6a833fcd 100644 --- a/workhorse/_support/lint_last_known_acceptable.txt +++ b/workhorse/_support/lint_last_known_acceptable.txt @@ -171,8 +171,8 @@ internal/upload/destination/objectstore/uploader.go:5:2: G501: Blocklisted impor internal/upload/destination/objectstore/uploader.go:95:12: G401: Use of weak cryptographic primitive (gosec) internal/upload/exif/exif.go:103:10: G204: Subprocess launched with variable (gosec) internal/upstream/routes.go:170:74: `(*upstream).wsRoute` - `matchers` always receives `nil` (unparam) -internal/upstream/routes.go:230: Function 'configureRoutes' is too long (333 > 60) (funlen) -internal/upstream/routes.go:479: internal/upstream/routes.go:479: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: We should probably not return a HT..." (godox) +internal/upstream/routes.go:230: Function 'configureRoutes' is too long (339 > 60) (funlen) +internal/upstream/routes.go:485: internal/upstream/routes.go:485: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: We should probably not return a HT..." (godox) internal/upstream/upstream.go:116: internal/upstream/upstream.go:116: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: move to LabKit https://gitlab.com/..." (godox) internal/zipartifacts/metadata.go:118:54: G115: integer overflow conversion int -> uint32 (gosec) internal/zipartifacts/open_archive.go:78:28: response body must be closed (bodyclose) diff --git a/workhorse/cmd/gitlab-workhorse/upload_test.go b/workhorse/cmd/gitlab-workhorse/upload_test.go index 9dee6019136ac..c1fc2f6fb951e 100644 --- a/workhorse/cmd/gitlab-workhorse/upload_test.go +++ b/workhorse/cmd/gitlab-workhorse/upload_test.go @@ -149,6 +149,8 @@ func TestAcceleratedUpload(t *testing.T) { {"POST", `/api/graphql`, false}, {"POST", `/api/v4/topics`, false}, {"PUT", `/api/v4/topics`, false}, + {"POST", `/api/v4/organizations`, false}, + {"PUT", `/api/v4/organizations/1`, false}, {"POST", `/api/v4/groups`, false}, {"PUT", `/api/v4/groups/5`, false}, {"PUT", `/api/v4/groups/group%2Fsubgroup`, false}, diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go index 74c4918e86a8d..d9b0de104b245 100644 --- a/workhorse/internal/upstream/routes.go +++ b/workhorse/internal/upstream/routes.go @@ -430,6 +430,12 @@ func configureRoutes(u *upstream) { u.route("PUT", newRoute(apiPattern+`v4/groups/[^/]+\z`, "api_groups", railsBackend), tempfileMultipartProxy), + // Organization Avatar + u.route("POST", + newRoute(apiPattern+`v4/organizations\z`, "api_organizations", railsBackend), tempfileMultipartProxy), + u.route("PUT", + newRoute(apiPattern+`v4/organizations/[0-9]+\z`, "api_organizations", railsBackend), tempfileMultipartProxy), + // User Avatar u.route("PUT", newRoute(apiPattern+`v4/user/avatar\z`, "api_user_avatar", railsBackend), tempfileMultipartProxy), -- GitLab