From d0b8bc41f1669417203748650fc4ff1a059bd5fd Mon Sep 17 00:00:00 2001 From: Paulo Barros Date: Fri, 1 Aug 2025 21:00:05 +0200 Subject: [PATCH] Resolve Gitlab/EeOnlyClass violations for instance SCIM code --- .../gitlab/avoid_gitlab_instance_checks.yml | 1 - .rubocop_todo/gitlab/ee_only_class.yml | 17 -- .../layout/empty_line_after_magic_comment.yml | 3 - .../lint/no_return_in_begin_end_blocks.yml | 1 - .rubocop_todo/rspec/be_eq.yml | 2 - .../rspec/example_without_description.yml | 2 - .rubocop_todo/rspec/feature_category.yml | 9 - .rubocop_todo/rspec/named_subject.yml | 7 - .rubocop_todo/style/redundant_parentheses.yml | 1 - ee/app/finders/authn/group_scim_finder.rb | 2 +- ee/app/finders/scim_finder.rb | 2 +- .../authn/sync_scim_group_members_worker.rb | 2 +- ee/lib/api/entities/scim/conflict.rb | 11 ++ ee/lib/api/entities/scim/emails.rb | 25 +++ ee/lib/api/entities/scim/error.rb | 27 +++ ee/lib/api/entities/scim/group.rb | 37 ++++ ee/lib/api/entities/scim/groups.rb | 40 ++++ ee/lib/api/entities/scim/not_found.rb | 11 ++ ee/lib/api/entities/scim/user.rb | 57 ++++++ ee/lib/api/entities/scim/user_name.rb | 13 ++ ee/lib/api/entities/scim/users.rb | 40 ++++ ee/lib/api/helpers/scim_helpers.rb | 8 +- ee/lib/api/scim/group_scim.rb | 8 +- ee/lib/api/scim/instance_scim.rb | 42 ++--- ee/lib/ee/api/entities/scim/conflict.rb | 13 -- ee/lib/ee/api/entities/scim/emails.rb | 27 --- ee/lib/ee/api/entities/scim/error.rb | 29 --- ee/lib/ee/api/entities/scim/group.rb | 43 ----- ee/lib/ee/api/entities/scim/groups.rb | 46 ----- ee/lib/ee/api/entities/scim/not_found.rb | 13 -- ee/lib/ee/api/entities/scim/user.rb | 59 ------ ee/lib/ee/api/entities/scim/user_name.rb | 15 -- ee/lib/ee/api/entities/scim/users.rb | 42 ----- ee/lib/ee/gitlab/scim/attribute_transform.rb | 37 ---- .../scim/base_deprovisioning_service.rb | 24 --- .../gitlab/scim/base_provisioning_service.rb | 53 ------ .../ee/gitlab/scim/deprovisioning_service.rb | 24 --- ee/lib/ee/gitlab/scim/filter_parser.rb | 35 ---- .../scim/group/deprovisioning_service.rb | 2 +- .../gitlab/scim/group/provisioning_service.rb | 4 +- .../scim/group_membership_cache_service.rb | 57 ------ .../scim/group_sync_deletion_service.rb | 39 ---- .../gitlab/scim/group_sync_patch_service.rb | 83 -------- .../scim/group_sync_provisioning_service.rb | 63 ------- .../ee/gitlab/scim/group_sync_put_service.rb | 46 ----- ee/lib/ee/gitlab/scim/params_parser.rb | 95 ---------- .../ee/gitlab/scim/provisioning_response.rb | 18 -- ee/lib/ee/gitlab/scim/provisioning_service.rb | 67 ------- .../ee/gitlab/scim/reprovisioning_service.rb | 32 ---- ee/lib/ee/gitlab/scim/value_parser.rb | 37 ---- ee/lib/gitlab/scim/attribute_transform.rb | 35 ++++ .../scim/base_deprovisioning_service.rb | 23 +++ .../gitlab/scim/base_provisioning_service.rb | 52 +++++ ee/lib/gitlab/scim/deprovisioning_service.rb | 22 +++ ee/lib/gitlab/scim/filter_parser.rb | 33 ++++ .../scim/group_membership_cache_service.rb | 51 +++++ .../scim/group_sync_deletion_service.rb | 33 ++++ .../gitlab/scim/group_sync_patch_service.rb | 77 ++++++++ .../scim/group_sync_provisioning_service.rb | 57 ++++++ ee/lib/gitlab/scim/group_sync_put_service.rb | 40 ++++ ee/lib/gitlab/scim/params_parser.rb | 93 +++++++++ ee/lib/gitlab/scim/provisioning_response.rb | 16 ++ ee/lib/gitlab/scim/provisioning_service.rb | 66 +++++++ ee/lib/gitlab/scim/reprovisioning_service.rb | 30 +++ ee/lib/gitlab/scim/value_parser.rb | 35 ++++ .../lib/ee/api/entities/scim/conflict_spec.rb | 24 --- .../lib/ee/api/entities/scim/emails_spec.rb | 26 --- .../lib/ee/api/entities/scim/error_spec.rb | 24 --- .../lib/ee/api/entities/scim/group_spec.rb | 38 ---- .../lib/ee/api/entities/scim/groups_spec.rb | 73 -------- .../ee/api/entities/scim/not_found_spec.rb | 23 --- .../ee/api/entities/scim/user_name_spec.rb | 21 --- ee/spec/lib/ee/api/entities/scim/user_spec.rb | 60 ------ .../lib/ee/api/entities/scim/users_spec.rb | 56 ------ .../gitlab/scim/attribute_transform_spec.rb | 49 ----- .../scim/deprovisioning_service_spec.rb | 30 --- .../lib/ee/gitlab/scim/filter_parser_spec.rb | 35 ---- .../scim/group/provisioning_service_spec.rb | 4 +- .../group_membership_cache_service_spec.rb | 64 ------- .../scim/group_sync_deletion_service_spec.rb | 56 ------ .../scim/group_sync_patch_service_spec.rb | 177 ------------------ .../group_sync_provisioning_service_spec.rb | 130 ------------- .../scim/group_sync_put_service_spec.rb | 118 ------------ .../lib/ee/gitlab/scim/params_parser_spec.rb | 155 --------------- .../scim/reprovisioning_service_spec.rb | 37 ---- .../lib/ee/gitlab/scim/value_parser_spec.rb | 26 --- .../gitlab/scim/provisioning_service_spec.rb | 6 +- ee/spec/requests/api/scim/group_scim_spec.rb | 2 +- .../requests/api/scim/instance_scim_spec.rb | 24 +-- spec/support/rspec_order_todo.yml | 11 -- 90 files changed, 977 insertions(+), 2326 deletions(-) create mode 100644 ee/lib/api/entities/scim/conflict.rb create mode 100644 ee/lib/api/entities/scim/emails.rb create mode 100644 ee/lib/api/entities/scim/error.rb create mode 100644 ee/lib/api/entities/scim/group.rb create mode 100644 ee/lib/api/entities/scim/groups.rb create mode 100644 ee/lib/api/entities/scim/not_found.rb create mode 100644 ee/lib/api/entities/scim/user.rb create mode 100644 ee/lib/api/entities/scim/user_name.rb create mode 100644 ee/lib/api/entities/scim/users.rb delete mode 100644 ee/lib/ee/api/entities/scim/conflict.rb delete mode 100644 ee/lib/ee/api/entities/scim/emails.rb delete mode 100644 ee/lib/ee/api/entities/scim/error.rb delete mode 100644 ee/lib/ee/api/entities/scim/group.rb delete mode 100644 ee/lib/ee/api/entities/scim/groups.rb delete mode 100644 ee/lib/ee/api/entities/scim/not_found.rb delete mode 100644 ee/lib/ee/api/entities/scim/user.rb delete mode 100644 ee/lib/ee/api/entities/scim/user_name.rb delete mode 100644 ee/lib/ee/api/entities/scim/users.rb delete mode 100644 ee/lib/ee/gitlab/scim/attribute_transform.rb delete mode 100644 ee/lib/ee/gitlab/scim/base_deprovisioning_service.rb delete mode 100644 ee/lib/ee/gitlab/scim/base_provisioning_service.rb delete mode 100644 ee/lib/ee/gitlab/scim/deprovisioning_service.rb delete mode 100644 ee/lib/ee/gitlab/scim/filter_parser.rb delete mode 100644 ee/lib/ee/gitlab/scim/group_membership_cache_service.rb delete mode 100644 ee/lib/ee/gitlab/scim/group_sync_deletion_service.rb delete mode 100644 ee/lib/ee/gitlab/scim/group_sync_patch_service.rb delete mode 100644 ee/lib/ee/gitlab/scim/group_sync_provisioning_service.rb delete mode 100644 ee/lib/ee/gitlab/scim/group_sync_put_service.rb delete mode 100644 ee/lib/ee/gitlab/scim/params_parser.rb delete mode 100644 ee/lib/ee/gitlab/scim/provisioning_response.rb delete mode 100644 ee/lib/ee/gitlab/scim/provisioning_service.rb delete mode 100644 ee/lib/ee/gitlab/scim/reprovisioning_service.rb delete mode 100644 ee/lib/ee/gitlab/scim/value_parser.rb create mode 100644 ee/lib/gitlab/scim/attribute_transform.rb create mode 100644 ee/lib/gitlab/scim/base_deprovisioning_service.rb create mode 100644 ee/lib/gitlab/scim/base_provisioning_service.rb create mode 100644 ee/lib/gitlab/scim/deprovisioning_service.rb create mode 100644 ee/lib/gitlab/scim/filter_parser.rb create mode 100644 ee/lib/gitlab/scim/group_membership_cache_service.rb create mode 100644 ee/lib/gitlab/scim/group_sync_deletion_service.rb create mode 100644 ee/lib/gitlab/scim/group_sync_patch_service.rb create mode 100644 ee/lib/gitlab/scim/group_sync_provisioning_service.rb create mode 100644 ee/lib/gitlab/scim/group_sync_put_service.rb create mode 100644 ee/lib/gitlab/scim/params_parser.rb create mode 100644 ee/lib/gitlab/scim/provisioning_response.rb create mode 100644 ee/lib/gitlab/scim/provisioning_service.rb create mode 100644 ee/lib/gitlab/scim/reprovisioning_service.rb create mode 100644 ee/lib/gitlab/scim/value_parser.rb delete mode 100644 ee/spec/lib/ee/api/entities/scim/conflict_spec.rb delete mode 100644 ee/spec/lib/ee/api/entities/scim/emails_spec.rb delete mode 100644 ee/spec/lib/ee/api/entities/scim/error_spec.rb delete mode 100644 ee/spec/lib/ee/api/entities/scim/group_spec.rb delete mode 100644 ee/spec/lib/ee/api/entities/scim/groups_spec.rb delete mode 100644 ee/spec/lib/ee/api/entities/scim/not_found_spec.rb delete mode 100644 ee/spec/lib/ee/api/entities/scim/user_name_spec.rb delete mode 100644 ee/spec/lib/ee/api/entities/scim/user_spec.rb delete mode 100644 ee/spec/lib/ee/api/entities/scim/users_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/attribute_transform_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/deprovisioning_service_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/filter_parser_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/group_membership_cache_service_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/group_sync_deletion_service_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/group_sync_patch_service_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/group_sync_provisioning_service_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/group_sync_put_service_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/params_parser_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/reprovisioning_service_spec.rb delete mode 100644 ee/spec/lib/ee/gitlab/scim/value_parser_spec.rb rename ee/spec/lib/{ee => }/gitlab/scim/provisioning_service_spec.rb (96%) diff --git a/.rubocop_todo/gitlab/avoid_gitlab_instance_checks.yml b/.rubocop_todo/gitlab/avoid_gitlab_instance_checks.yml index f84e287fc50dd1..9d91d42296d48d 100644 --- a/.rubocop_todo/gitlab/avoid_gitlab_instance_checks.yml +++ b/.rubocop_todo/gitlab/avoid_gitlab_instance_checks.yml @@ -75,7 +75,6 @@ Gitlab/AvoidGitlabInstanceChecks: - 'ee/lib/ee/gitlab/background_migration/create_compliance_standards_adherence.rb' - 'ee/lib/ee/gitlab/gon_helper.rb' - 'ee/lib/ee/gitlab/personal_access_tokens/service_account_token_validator.rb' - - 'ee/lib/ee/gitlab/scim/base_provisioning_service.rb' - 'ee/lib/ee/gitlab/snippet_search_results.rb' - 'ee/lib/ee/gitlab/tracking/standard_context.rb' - 'ee/lib/ee/sidebars/groups/menus/settings_menu.rb' diff --git a/.rubocop_todo/gitlab/ee_only_class.yml b/.rubocop_todo/gitlab/ee_only_class.yml index cc62c5ec7101ca..2dd4dcc4a336b6 100644 --- a/.rubocop_todo/gitlab/ee_only_class.yml +++ b/.rubocop_todo/gitlab/ee_only_class.yml @@ -77,13 +77,6 @@ Gitlab/EeOnlyClass: - 'ee/lib/ee/api/entities/resource_iteration_event.rb' - 'ee/lib/ee/api/entities/resource_weight_event.rb' - 'ee/lib/ee/api/entities/saml_group_link.rb' - - 'ee/lib/ee/api/entities/scim/conflict.rb' - - 'ee/lib/ee/api/entities/scim/emails.rb' - - 'ee/lib/ee/api/entities/scim/error.rb' - - 'ee/lib/ee/api/entities/scim/not_found.rb' - - 'ee/lib/ee/api/entities/scim/user.rb' - - 'ee/lib/ee/api/entities/scim/user_name.rb' - - 'ee/lib/ee/api/entities/scim/users.rb' - 'ee/lib/ee/api/entities/security_policy_configuration.rb' - 'ee/lib/ee/api/entities/special_board_filter.rb' - 'ee/lib/ee/api/entities/ssh_certificate.rb' @@ -108,18 +101,8 @@ Gitlab/EeOnlyClass: - 'ee/lib/ee/gitlab/namespace_storage_size_error_message.rb' - 'ee/lib/ee/gitlab/personal_access_tokens/expiry_date_calculator.rb' - 'ee/lib/ee/gitlab/personal_access_tokens/service_account_token_validator.rb' - - 'ee/lib/ee/gitlab/scim/attribute_transform.rb' - - 'ee/lib/ee/gitlab/scim/base_deprovisioning_service.rb' - - 'ee/lib/ee/gitlab/scim/base_provisioning_service.rb' - - 'ee/lib/ee/gitlab/scim/deprovisioning_service.rb' - - 'ee/lib/ee/gitlab/scim/filter_parser.rb' - 'ee/lib/ee/gitlab/scim/group/deprovisioning_service.rb' - 'ee/lib/ee/gitlab/scim/group/provisioning_service.rb' - 'ee/lib/ee/gitlab/scim/group/reprovisioning_service.rb' - - 'ee/lib/ee/gitlab/scim/params_parser.rb' - - 'ee/lib/ee/gitlab/scim/provisioning_response.rb' - - 'ee/lib/ee/gitlab/scim/provisioning_service.rb' - - 'ee/lib/ee/gitlab/scim/reprovisioning_service.rb' - - 'ee/lib/ee/gitlab/scim/value_parser.rb' - 'ee/lib/ee/sidebars/explore/menus/dependencies_menu.rb' - 'ee/spec/support/helpers/ee/markdown_feature.rb' diff --git a/.rubocop_todo/layout/empty_line_after_magic_comment.yml b/.rubocop_todo/layout/empty_line_after_magic_comment.yml index 73e4f63f67b7ae..57573b94d03fc0 100644 --- a/.rubocop_todo/layout/empty_line_after_magic_comment.yml +++ b/.rubocop_todo/layout/empty_line_after_magic_comment.yml @@ -137,9 +137,6 @@ Layout/EmptyLineAfterMagicComment: - 'ee/lib/compliance_management/merge_request_approval_settings/resolver.rb' - 'ee/lib/ee/gitlab/hook_data/group_member_builder.rb' - 'ee/lib/ee/gitlab/hook_data/issue_builder.rb' - - 'ee/lib/ee/gitlab/scim/base_deprovisioning_service.rb' - - 'ee/lib/ee/gitlab/scim/base_provisioning_service.rb' - - 'ee/lib/ee/gitlab/scim/provisioning_service.rb' - 'ee/lib/elastic/as_json.rb' - 'ee/lib/gem_extensions/elasticsearch/model/adapter/active_record/importing.rb' - 'ee/lib/gem_extensions/elasticsearch/model/adapter/multiple/records.rb' diff --git a/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml b/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml index 8d030a54e585bc..3f500d560d9f2b 100644 --- a/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml +++ b/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml @@ -6,6 +6,5 @@ Lint/NoReturnInBeginEndBlocks: - 'ee/app/services/gitlab_subscriptions/preview_billable_user_change_service.rb' - 'ee/app/services/security/token_revocation_service.rb' - 'ee/lib/api/vulnerability_findings.rb' - - 'ee/lib/ee/gitlab/scim/filter_parser.rb' - 'lib/object_storage/config.rb' - 'spec/support/database/prevent_cross_joins.rb' diff --git a/.rubocop_todo/rspec/be_eq.yml b/.rubocop_todo/rspec/be_eq.yml index 8da33e64d554ea..996d07971044a6 100644 --- a/.rubocop_todo/rspec/be_eq.yml +++ b/.rubocop_todo/rspec/be_eq.yml @@ -100,8 +100,6 @@ RSpec/BeEq: - 'ee/spec/lib/ee/gitlab/personal_access_tokens/expiry_date_calculator_spec.rb' - 'ee/spec/lib/ee/gitlab/repository_size_checker_spec.rb' - 'ee/spec/lib/ee/gitlab/saas_spec.rb' - - 'ee/spec/lib/ee/gitlab/scim/deprovisioning_service_spec.rb' - - 'ee/spec/lib/ee/gitlab/scim/params_parser_spec.rb' - 'ee/spec/lib/ee/gitlab/session_spec.rb' - 'ee/spec/lib/ee/gitlab/web_hooks/rate_limiter_spec.rb' - 'ee/spec/lib/ee/import/placeholder_user_limit_spec.rb' diff --git a/.rubocop_todo/rspec/example_without_description.yml b/.rubocop_todo/rspec/example_without_description.yml index 23bfc6fb0f79aa..76ab125114474b 100644 --- a/.rubocop_todo/rspec/example_without_description.yml +++ b/.rubocop_todo/rspec/example_without_description.yml @@ -47,8 +47,6 @@ RSpec/ExampleWithoutDescription: - 'ee/spec/lib/ee/gitlab/ci/variables/builder/scan_execution_policies_spec.rb' - 'ee/spec/lib/ee/gitlab/database_spec.rb' - 'ee/spec/lib/ee/gitlab/observability_spec.rb' - - 'ee/spec/lib/ee/gitlab/scim/attribute_transform_spec.rb' - - 'ee/spec/lib/ee/gitlab/scim/value_parser_spec.rb' - 'ee/spec/lib/gitlab/auth/smartcard/certificate_spec.rb' - 'ee/spec/lib/gitlab/auth/smartcard/ldap_certificate_spec.rb' - 'ee/spec/lib/gitlab/auth/smartcard/san_extension_spec.rb' diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml index 9f52ff1bb71e15..4be2980dcfe2ee 100644 --- a/.rubocop_todo/rspec/feature_category.yml +++ b/.rubocop_todo/rspec/feature_category.yml @@ -405,13 +405,6 @@ RSpec/FeatureCategory: - 'ee/spec/lib/ee/api/entities/experiment_spec.rb' - 'ee/spec/lib/ee/api/entities/group_detail_spec.rb' - 'ee/spec/lib/ee/api/entities/identity_detail_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/conflict_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/emails_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/error_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/not_found_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/user_name_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/user_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/users_spec.rb' - 'ee/spec/lib/ee/api/entities/user_with_admin_spec.rb' - 'ee/spec/lib/ee/api/entities/vulnerability_export_spec.rb' - 'ee/spec/lib/ee/api/entities/vulnerability_spec.rb' @@ -455,8 +448,6 @@ RSpec/FeatureCategory: - 'ee/spec/lib/ee/gitlab/omniauth_initializer_spec.rb' - 'ee/spec/lib/ee/gitlab/repo_path_spec.rb' - 'ee/spec/lib/ee/gitlab/repository_size_checker_spec.rb' - - 'ee/spec/lib/ee/gitlab/scim/attribute_transform_spec.rb' - - 'ee/spec/lib/ee/gitlab/scim/value_parser_spec.rb' - 'ee/spec/lib/ee/gitlab/search_results_spec.rb' - 'ee/spec/lib/ee/gitlab/snippet_search_results_spec.rb' - 'ee/spec/lib/ee/gitlab/template/gitlab_ci_yml_template_spec.rb' diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index c5a116eb09456a..06fec3a2fd11e3 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -226,13 +226,6 @@ RSpec/NamedSubject: - 'ee/spec/lib/ee/api/entities/geo_site_status_spec.rb' - 'ee/spec/lib/ee/api/entities/group_detail_spec.rb' - 'ee/spec/lib/ee/api/entities/project_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/conflict_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/emails_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/error_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/not_found_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/user_name_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/user_spec.rb' - - 'ee/spec/lib/ee/api/entities/scim/users_spec.rb' - 'ee/spec/lib/ee/api/entities/user_with_admin_spec.rb' - 'ee/spec/lib/ee/api/entities/vulnerability_export_spec.rb' - 'ee/spec/lib/ee/api/entities/vulnerability_spec.rb' diff --git a/.rubocop_todo/style/redundant_parentheses.yml b/.rubocop_todo/style/redundant_parentheses.yml index bad3e58eb1f94d..4c77624e731703 100644 --- a/.rubocop_todo/style/redundant_parentheses.yml +++ b/.rubocop_todo/style/redundant_parentheses.yml @@ -14,7 +14,6 @@ Style/RedundantParentheses: - 'ee/app/services/concerns/approval_rules/updater.rb' - 'ee/app/services/merge_trains/refresh_service.rb' - 'ee/lib/ee/gitlab/git_access.rb' - - 'ee/lib/ee/gitlab/scim/params_parser.rb' - 'ee/spec/requests/lfs_locks_api_spec.rb' - 'lib/api/appearance.rb' - 'lib/file_size_validator.rb' diff --git a/ee/app/finders/authn/group_scim_finder.rb b/ee/app/finders/authn/group_scim_finder.rb index 123f2c3ba4f677..32f222d59d22f4 100644 --- a/ee/app/finders/authn/group_scim_finder.rb +++ b/ee/app/finders/authn/group_scim_finder.rb @@ -30,7 +30,7 @@ def unfiltered?(params) end def filter_identities(params) - parser = EE::Gitlab::Scim::ParamsParser.new(params) + parser = Gitlab::Scim::ParamsParser.new(params) if eq_filter_on_extern_uid?(parser) by_extern_uid(parser.filter_params[:extern_uid]) diff --git a/ee/app/finders/scim_finder.rb b/ee/app/finders/scim_finder.rb index 9d7d55efffed68..8bc88c0c6d4ae9 100644 --- a/ee/app/finders/scim_finder.rb +++ b/ee/app/finders/scim_finder.rb @@ -44,7 +44,7 @@ def unfiltered?(params) end def filter_identities(params) - parser = EE::Gitlab::Scim::ParamsParser.new(params) + parser = Gitlab::Scim::ParamsParser.new(params) if eq_filter_on_extern_uid?(parser) by_extern_uid(parser.filter_params[:extern_uid]) diff --git a/ee/app/workers/authn/sync_scim_group_members_worker.rb b/ee/app/workers/authn/sync_scim_group_members_worker.rb index 9651db9061d698..97c4219fe1aaea 100644 --- a/ee/app/workers/authn/sync_scim_group_members_worker.rb +++ b/ee/app/workers/authn/sync_scim_group_members_worker.rb @@ -28,7 +28,7 @@ def perform(scim_group_uid, user_ids, operation_type) @scim_group_uid = scim_group_uid @user_ids = user_ids - @cache_service = ::EE::Gitlab::Scim::GroupMembershipCacheService.new(scim_group_uid: scim_group_uid) + @cache_service = ::Gitlab::Scim::GroupMembershipCacheService.new(scim_group_uid: scim_group_uid) return if group_links.empty? return if user_ids.empty? && operation_type != 'replace' diff --git a/ee/lib/api/entities/scim/conflict.rb b/ee/lib/api/entities/scim/conflict.rb new file mode 100644 index 00000000000000..f46e5f18d58c24 --- /dev/null +++ b/ee/lib/api/entities/scim/conflict.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module Scim + class Conflict < Error + STATUS = 409 + end + end + end +end diff --git a/ee/lib/api/entities/scim/emails.rb b/ee/lib/api/entities/scim/emails.rb new file mode 100644 index 00000000000000..15f0c08906465f --- /dev/null +++ b/ee/lib/api/entities/scim/emails.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module API + module Entities + module Scim + class Emails < Grape::Entity + expose :type + expose :value do |user, _options| + user.email + end + expose :primary + + private + + def type + 'work' + end + + def primary + true + end + end + end + end +end diff --git a/ee/lib/api/entities/scim/error.rb b/ee/lib/api/entities/scim/error.rb new file mode 100644 index 00000000000000..ff4af2089e789d --- /dev/null +++ b/ee/lib/api/entities/scim/error.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module API + module Entities + module Scim + class Error < Grape::Entity + STATUS = 412 + + expose :schemas + expose :detail, safe: true + expose :status + + private + + DEFAULT_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:Error' + + def schemas + [DEFAULT_SCHEMA] + end + + def status + self.class::STATUS + end + end + end + end +end diff --git a/ee/lib/api/entities/scim/group.rb b/ee/lib/api/entities/scim/group.rb new file mode 100644 index 00000000000000..3a9cc307906e5c --- /dev/null +++ b/ee/lib/api/entities/scim/group.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module API + module Entities + module Scim + class Group < Grape::Entity + expose :schemas + expose :id do |group, _options| + group.scim_group_uid + end + expose :display_name, as: :displayName do |group, _options| + group.saml_group_name + end + expose :members, unless: ->(_, opts) { opts[:excluded_attributes]&.include?('members') } + expose :meta do + expose :resource_type, as: :resourceType + end + + private + + DEFAULT_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:Group' + + def schemas + [DEFAULT_SCHEMA] + end + + def members + [] # We'll need to implement this if we want to show actual members + end + + def resource_type + 'Group' + end + end + end + end +end diff --git a/ee/lib/api/entities/scim/groups.rb b/ee/lib/api/entities/scim/groups.rb new file mode 100644 index 00000000000000..71b9357de0bbf6 --- /dev/null +++ b/ee/lib/api/entities/scim/groups.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module API + module Entities + module Scim + class Groups < Grape::Entity + expose :schemas + expose :total_results, as: :totalResults + expose :items_per_page, as: :itemsPerPage + expose :start_index, as: :startIndex + + expose :resources, as: :Resources, using: ::API::Entities::Scim::Group + + private + + DEFAULT_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:ListResponse' + + def schemas + [DEFAULT_SCHEMA] + end + + def total_results + object[:total_results] || resources.count + end + + def items_per_page + object[:items_per_page] || Kaminari.config.default_per_page + end + + def start_index + object[:start_index].presence || 1 + end + + def resources + object[:resources] || [] + end + end + end + end +end diff --git a/ee/lib/api/entities/scim/not_found.rb b/ee/lib/api/entities/scim/not_found.rb new file mode 100644 index 00000000000000..3be2574057ceef --- /dev/null +++ b/ee/lib/api/entities/scim/not_found.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module Scim + class NotFound < Error + STATUS = 404 + end + end + end +end diff --git a/ee/lib/api/entities/scim/user.rb b/ee/lib/api/entities/scim/user.rb new file mode 100644 index 00000000000000..b3b18f8ca1a976 --- /dev/null +++ b/ee/lib/api/entities/scim/user.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module API + module Entities + module Scim + class User < Grape::Entity + expose :schemas + expose :extern_uid, as: :id + expose :active + expose :email_user, as: :emails, using: ::API::Entities::Scim::Emails + + expose :name, using: ::API::Entities::Scim::UserName do |identity, _options| + identity.user + end + + expose :meta do + expose :resource_type, as: :resourceType + end + expose :username, as: :userName do |identity, _options| + identity.user.username + end + + private + + DEFAULT_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User' + + def schemas + [DEFAULT_SCHEMA] + end + + def active + object_active = object.try(:active) + + return true if object_active.nil? + + object_active + end + + def email_type + 'work' + end + + def email_primary + true + end + + def email_user + [object.user] + end + + def resource_type + 'User' + end + end + end + end +end diff --git a/ee/lib/api/entities/scim/user_name.rb b/ee/lib/api/entities/scim/user_name.rb new file mode 100644 index 00000000000000..bac86af7db128c --- /dev/null +++ b/ee/lib/api/entities/scim/user_name.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + module Scim + class UserName < Grape::Entity + expose :name, as: :formatted + expose :first_name, as: :givenName + expose :last_name, as: :familyName + end + end + end +end diff --git a/ee/lib/api/entities/scim/users.rb b/ee/lib/api/entities/scim/users.rb new file mode 100644 index 00000000000000..1abd1758db24f5 --- /dev/null +++ b/ee/lib/api/entities/scim/users.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module API + module Entities + module Scim + class Users < Grape::Entity + expose :schemas + expose :total_results, as: :totalResults + expose :items_per_page, as: :itemsPerPage + expose :start_index, as: :startIndex + + expose :resources, as: :Resources, using: ::API::Entities::Scim::User + + private + + DEFAULT_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:ListResponse' + + def schemas + [DEFAULT_SCHEMA] + end + + def total_results + object[:total_results] || resources.count + end + + def items_per_page + object[:items_per_page] || Kaminari.config.default_per_page + end + + def start_index + object[:start_index].presence || 1 + end + + def resources + object[:resources] || [] + end + end + end + end +end diff --git a/ee/lib/api/helpers/scim_helpers.rb b/ee/lib/api/helpers/scim_helpers.rb index 11356987ba5f69..12f8e6e0739072 100644 --- a/ee/lib/api/helpers/scim_helpers.rb +++ b/ee/lib/api/helpers/scim_helpers.rb @@ -4,15 +4,15 @@ module API module Helpers module ScimHelpers def scim_not_found!(message:) - render_scim_error(::EE::API::Entities::Scim::NotFound, message) + render_scim_error(::API::Entities::Scim::NotFound, message) end def scim_error!(message:) - render_scim_error(::EE::API::Entities::Scim::Error, message) + render_scim_error(::API::Entities::Scim::Error, message) end def scim_conflict!(message:) - render_scim_error(::EE::API::Entities::Scim::Conflict, message) + render_scim_error(::API::Entities::Scim::Conflict, message) end def render_scim_error(error_class, message) @@ -25,7 +25,7 @@ def sanitize_request_parameters(parameters) end def update_scim_user(identity) - parser = ::EE::Gitlab::Scim::ParamsParser.new(params) + parser = ::Gitlab::Scim::ParamsParser.new(params) parsed_hash = parser.update_params if parser.deprovision_user? diff --git a/ee/lib/api/scim/group_scim.rb b/ee/lib/api/scim/group_scim.rb index 5cd2420d72bbd0..61a50a63554941 100644 --- a/ee/lib/api/scim/group_scim.rb +++ b/ee/lib/api/scim/group_scim.rb @@ -123,7 +123,7 @@ def reprovision(identity) items_per_page: per_page(params[:count]), start_index: params[:startIndex] } - present result_set, with: ::EE::API::Entities::Scim::Users + present result_set, with: ::API::Entities::Scim::Users rescue Authn::GroupScimFinder::UnsupportedFilter, ScimFinder::UnsupportedFilter scim_error!(message: 'Unsupported Filter') end @@ -140,7 +140,7 @@ def reprovision(identity) status 200 - present identity, with: ::EE::API::Entities::Scim::User + present identity, with: ::API::Entities::Scim::User end desc 'Create a SCIM user' do @@ -148,14 +148,14 @@ def reprovision(identity) end post do group = find_and_authenticate_group!(params[:group]) - parser = ::EE::Gitlab::Scim::ParamsParser.new(params) + parser = ::Gitlab::Scim::ParamsParser.new(params) result = ::EE::Gitlab::Scim::Group::ProvisioningService.new(parser.post_params, group).execute case result.status when :success status 201 - present result.identity, with: ::EE::API::Entities::Scim::User + present result.identity, with: ::API::Entities::Scim::User when :conflict scim_conflict!( message: "Error saving user with #{sanitize_request_parameters(params).inspect}: #{result.message}" diff --git a/ee/lib/api/scim/instance_scim.rb b/ee/lib/api/scim/instance_scim.rb index fdec784b13076c..493ae374e7f33b 100644 --- a/ee/lib/api/scim/instance_scim.rb +++ b/ee/lib/api/scim/instance_scim.rb @@ -36,7 +36,7 @@ def find_user_identity(extern_uid) end def patch_deprovision(identity) - ::EE::Gitlab::Scim::DeprovisioningService.new(identity).execute + ::Gitlab::Scim::DeprovisioningService.new(identity).execute true rescue StandardError => e @@ -50,7 +50,7 @@ def patch_deprovision(identity) end def reprovision(identity) - ::EE::Gitlab::Scim::ReprovisioningService.new(identity).execute + ::Gitlab::Scim::ReprovisioningService.new(identity).execute true rescue StandardError => e @@ -71,7 +71,7 @@ def reprovision(identity) before { check_instance_requirements! } desc 'Get SCIM users' do - success ::EE::API::Entities::Scim::Users + success ::API::Entities::Scim::Users end get do @@ -85,13 +85,13 @@ def reprovision(identity) items_per_page: per_page(params[:count]), start_index: params[:startIndex] } - present result_set, with: ::EE::API::Entities::Scim::Users + present result_set, with: ::API::Entities::Scim::Users rescue ScimFinder::UnsupportedFilter scim_error!(message: 'Unsupported Filter') end desc 'Get a SCIM user' do - success ::EE::API::Entities::Scim::Users + success ::API::Entities::Scim::Users end get ':id', requirements: USER_ID_REQUIREMENTS do @@ -100,16 +100,16 @@ def reprovision(identity) status 200 - present identity, with: ::EE::API::Entities::Scim::User + present identity, with: ::API::Entities::Scim::User end desc 'Create a SCIM user' do - success ::EE::API::Entities::Scim::Users + success ::API::Entities::Scim::Users end post do - parser = ::EE::Gitlab::Scim::ParamsParser.new(params) - result = ::EE::Gitlab::Scim::ProvisioningService.new( + parser = ::Gitlab::Scim::ParamsParser.new(params) + result = ::Gitlab::Scim::ProvisioningService.new( parser.post_params.merge(organization_id: ::Current.organization.id) ).execute @@ -117,7 +117,7 @@ def reprovision(identity) when :success status 201 - present result.identity, with: ::EE::API::Entities::Scim::User + present result.identity, with: ::API::Entities::Scim::User when :conflict scim_conflict!( message: "Error saving user with #{sanitize_request_parameters(params).inspect}: #{result.message}" @@ -181,14 +181,14 @@ def find_group_link(scim_group_uid) desc 'Create a SCIM group' do detail 'Associates SCIM group ID with existing SAML group link' - success ::EE::API::Entities::Scim::Group + success ::API::Entities::Scim::Group end params do requires :displayName, type: String, desc: 'Name of the group as configured in GitLab' optional :externalId, type: String, desc: 'SCIM group ID' end post do - result = ::EE::Gitlab::Scim::GroupSyncProvisioningService.new( + result = ::Gitlab::Scim::GroupSyncProvisioningService.new( saml_group_name: params[:displayName], scim_group_uid: params[:externalId] || SecureRandom.uuid ).execute @@ -196,7 +196,7 @@ def find_group_link(scim_group_uid) case result.status when :success status 201 - present result.group_link, with: ::EE::API::Entities::Scim::Group + present result.group_link, with: ::API::Entities::Scim::Group when :error scim_error!(message: result.message) end @@ -204,18 +204,18 @@ def find_group_link(scim_group_uid) desc 'Get a SCIM group' do detail 'Retrieves a SCIM group by its ID' - success ::EE::API::Entities::Scim::Group + success ::API::Entities::Scim::Group end params do requires :id, type: String, desc: 'The SCIM group ID' end get ':id' do group_link = find_group_link(params[:id]) - present group_link, with: ::EE::API::Entities::Scim::Group + present group_link, with: ::API::Entities::Scim::Group end desc 'Get SCIM groups' do - success ::EE::API::Entities::Scim::Groups + success ::API::Entities::Scim::Groups end params do optional :filter, type: String, desc: 'Filter string (e.g. displayName eq "Engineering")' @@ -237,7 +237,7 @@ def find_group_link(scim_group_uid) } status :ok - present result_set, with: ::EE::API::Entities::Scim::Groups, excluded_attributes: excluded_attributes + present result_set, with: ::API::Entities::Scim::Groups, excluded_attributes: excluded_attributes rescue Authn::ScimGroupFinder::UnsupportedFilter scim_error!(message: 'Unsupported Filter') end @@ -258,7 +258,7 @@ def find_group_link(scim_group_uid) saml_group_links = SamlGroupLink.by_scim_group_uid(params[:id]) scim_not_found!(message: "Group #{params[:id]} not found") unless saml_group_links.exists? - ::EE::Gitlab::Scim::GroupSyncPatchService.new( + ::Gitlab::Scim::GroupSyncPatchService.new( scim_group_uid: params[:id], operations: params[:Operations] ).execute @@ -277,13 +277,13 @@ def find_group_link(scim_group_uid) saml_group_links = SamlGroupLink.by_scim_group_uid(params[:id]) scim_not_found!(message: "Group #{params[:id]} not found") unless saml_group_links.exists? - ::EE::Gitlab::Scim::GroupSyncPutService.new( + ::Gitlab::Scim::GroupSyncPutService.new( scim_group_uid: params[:id], members: params[:members] || [], display_name: params[:displayName] ).execute - present saml_group_links.first, with: ::EE::API::Entities::Scim::Group, excluded_attributes: ['members'] + present saml_group_links.first, with: ::API::Entities::Scim::Group, excluded_attributes: ['members'] end desc 'Delete a SCIM group' @@ -294,7 +294,7 @@ def find_group_link(scim_group_uid) saml_group_links = SamlGroupLink.by_scim_group_uid(params[:id]) scim_not_found!(message: "Group #{params[:id]} not found") unless saml_group_links.exists? - result = ::EE::Gitlab::Scim::GroupSyncDeletionService.new(scim_group_uid: params[:id]).execute + result = ::Gitlab::Scim::GroupSyncDeletionService.new(scim_group_uid: params[:id]).execute scim_error!(message: result.message) if result.error? no_content! diff --git a/ee/lib/ee/api/entities/scim/conflict.rb b/ee/lib/ee/api/entities/scim/conflict.rb deleted file mode 100644 index 72b0a8c704ec06..00000000000000 --- a/ee/lib/ee/api/entities/scim/conflict.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module EE - module API - module Entities - module Scim - class Conflict < Error - STATUS = 409 - end - end - end - end -end diff --git a/ee/lib/ee/api/entities/scim/emails.rb b/ee/lib/ee/api/entities/scim/emails.rb deleted file mode 100644 index 703bef56a05a75..00000000000000 --- a/ee/lib/ee/api/entities/scim/emails.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module EE - module API - module Entities - module Scim - class Emails < Grape::Entity - expose :type - expose :value do |user, _options| - user.email - end - expose :primary - - private - - def type - 'work' - end - - def primary - true - end - end - end - end - end -end diff --git a/ee/lib/ee/api/entities/scim/error.rb b/ee/lib/ee/api/entities/scim/error.rb deleted file mode 100644 index bed95b49340933..00000000000000 --- a/ee/lib/ee/api/entities/scim/error.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module EE - module API - module Entities - module Scim - class Error < Grape::Entity - STATUS = 412 - - expose :schemas - expose :detail, safe: true - expose :status - - private - - DEFAULT_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:Error' - - def schemas - [DEFAULT_SCHEMA] - end - - def status - self.class::STATUS - end - end - end - end - end -end diff --git a/ee/lib/ee/api/entities/scim/group.rb b/ee/lib/ee/api/entities/scim/group.rb deleted file mode 100644 index 447f3ea0af67b1..00000000000000 --- a/ee/lib/ee/api/entities/scim/group.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module EE - module API - module Entities - module Scim - # rubocop:disable Gitlab/EeOnlyClass -- All existing instance SCIM code - # currently lives under ee/ and making it compliant requires a larger - # refactor to be addressed by https://gitlab.com/gitlab-org/gitlab/-/issues/520129. - class Group < Grape::Entity - expose :schemas - expose :id do |group, _options| - group.scim_group_uid - end - expose :display_name, as: :displayName do |group, _options| - group.saml_group_name - end - expose :members, unless: ->(_, opts) { opts[:excluded_attributes]&.include?('members') } - expose :meta do - expose :resource_type, as: :resourceType - end - - private - - DEFAULT_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:Group' - - def schemas - [DEFAULT_SCHEMA] - end - - def members - [] # We'll need to implement this if we want to show actual members - end - - def resource_type - 'Group' - end - end - # rubocop:enable Gitlab/EeOnlyClass - end - end - end -end diff --git a/ee/lib/ee/api/entities/scim/groups.rb b/ee/lib/ee/api/entities/scim/groups.rb deleted file mode 100644 index 5d84e3da7444ae..00000000000000 --- a/ee/lib/ee/api/entities/scim/groups.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module EE - module API - module Entities - module Scim - # rubocop:disable Gitlab/EeOnlyClass -- All existing instance SCIM code - # currently lives under ee/ and making it compliant requires a larger - # refactor to be addressed by https://gitlab.com/gitlab-org/gitlab/-/issues/520129. - class Groups < Grape::Entity - expose :schemas - expose :total_results, as: :totalResults - expose :items_per_page, as: :itemsPerPage - expose :start_index, as: :startIndex - - expose :resources, as: :Resources, using: ::EE::API::Entities::Scim::Group - - private - - DEFAULT_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:ListResponse' - - def schemas - [DEFAULT_SCHEMA] - end - - def total_results - object[:total_results] || resources.count - end - - def items_per_page - object[:items_per_page] || Kaminari.config.default_per_page - end - - def start_index - object[:start_index].presence || 1 - end - - def resources - object[:resources] || [] - end - end - # rubocop:enable Gitlab/EeOnlyClass - end - end - end -end diff --git a/ee/lib/ee/api/entities/scim/not_found.rb b/ee/lib/ee/api/entities/scim/not_found.rb deleted file mode 100644 index ae08adfa27a328..00000000000000 --- a/ee/lib/ee/api/entities/scim/not_found.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module EE - module API - module Entities - module Scim - class NotFound < Error - STATUS = 404 - end - end - end - end -end diff --git a/ee/lib/ee/api/entities/scim/user.rb b/ee/lib/ee/api/entities/scim/user.rb deleted file mode 100644 index 4c6e13c928fa41..00000000000000 --- a/ee/lib/ee/api/entities/scim/user.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -module EE - module API - module Entities - module Scim - class User < Grape::Entity - expose :schemas - expose :extern_uid, as: :id - expose :active - expose :email_user, as: :emails, using: ::EE::API::Entities::Scim::Emails - - expose :name, using: ::EE::API::Entities::Scim::UserName do |identity, _options| - identity.user - end - - expose :meta do - expose :resource_type, as: :resourceType - end - expose :username, as: :userName do |identity, _options| - identity.user.username - end - - private - - DEFAULT_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User' - - def schemas - [DEFAULT_SCHEMA] - end - - def active - object_active = object.try(:active) - - return true if object_active.nil? - - object_active - end - - def email_type - 'work' - end - - def email_primary - true - end - - def email_user - [object.user] - end - - def resource_type - 'User' - end - end - end - end - end -end diff --git a/ee/lib/ee/api/entities/scim/user_name.rb b/ee/lib/ee/api/entities/scim/user_name.rb deleted file mode 100644 index 2021124efc6360..00000000000000 --- a/ee/lib/ee/api/entities/scim/user_name.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module EE - module API - module Entities - module Scim - class UserName < Grape::Entity - expose :name, as: :formatted - expose :first_name, as: :givenName - expose :last_name, as: :familyName - end - end - end - end -end diff --git a/ee/lib/ee/api/entities/scim/users.rb b/ee/lib/ee/api/entities/scim/users.rb deleted file mode 100644 index 32c048a43e0bb6..00000000000000 --- a/ee/lib/ee/api/entities/scim/users.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module EE - module API - module Entities - module Scim - class Users < Grape::Entity - expose :schemas - expose :total_results, as: :totalResults - expose :items_per_page, as: :itemsPerPage - expose :start_index, as: :startIndex - - expose :resources, as: :Resources, using: ::EE::API::Entities::Scim::User - - private - - DEFAULT_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:ListResponse' - - def schemas - [DEFAULT_SCHEMA] - end - - def total_results - object[:total_results] || resources.count - end - - def items_per_page - object[:items_per_page] || Kaminari.config.default_per_page - end - - def start_index - object[:start_index].presence || 1 - end - - def resources - object[:resources] || [] - end - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/scim/attribute_transform.rb b/ee/lib/ee/gitlab/scim/attribute_transform.rb deleted file mode 100644 index a206ad6f99a575..00000000000000 --- a/ee/lib/ee/gitlab/scim/attribute_transform.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - class AttributeTransform - MAPPINGS = { - id: :extern_uid, - displayName: :name, - 'name.formatted': :name, - 'emails[type eq "work"].value': :email, - active: :active, - externalId: :extern_uid, - userName: :username - }.with_indifferent_access.freeze - - def initialize(key) - @key = key - end - - def valid? - MAPPINGS.key?(@key) - end - - def gitlab_key - MAPPINGS[@key] - end - - def map_to(input) - return {} unless valid? - - { gitlab_key => ValueParser.new(input).type_cast } - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/scim/base_deprovisioning_service.rb b/ee/lib/ee/gitlab/scim/base_deprovisioning_service.rb deleted file mode 100644 index e4c41f4257ae93..00000000000000 --- a/ee/lib/ee/gitlab/scim/base_deprovisioning_service.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true -module EE - module Gitlab - module Scim - class BaseDeprovisioningService - include ::Gitlab::Utils::StrongMemoize - - attr_reader :identity - - delegate :user, :group, to: :identity - - def initialize(identity) - @identity = identity - end - - private - - def error(message) - ServiceResponse.error(message: message) - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/scim/base_provisioning_service.rb b/ee/lib/ee/gitlab/scim/base_provisioning_service.rb deleted file mode 100644 index 3956d18560ada9..00000000000000 --- a/ee/lib/ee/gitlab/scim/base_provisioning_service.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true -module EE - module Gitlab - module Scim - class BaseProvisioningService - include ::Gitlab::Utils::StrongMemoize - - PASSWORD_AUTOMATICALLY_SET = true - SKIP_EMAIL_CONFIRMATION = false - - def initialize(parsed_hash, group = nil) - raise ArgumentError, 'Group cannot be nil' if group.nil? && ::Gitlab.com? - - @group = group - @parsed_hash = parsed_hash.dup - end - - private - - def error_response(errors: nil, objects: []) - errors ||= objects.compact.flat_map { |obj| obj.errors.full_messages } - conflict = errors.any? { |error| error.include?('has already been taken') } - - ProvisioningResponse.new(status: conflict ? :conflict : :error, message: errors.to_sentence) - end - - def logger - ::API::API.logger - end - - def random_password - ::User.random_password - end - - def valid_username - ::Gitlab::Auth::ExternalUsernameSanitizer.new(@parsed_hash[:username]).sanitize - end - - def missing_params - @missing_params ||= ([:extern_uid, :email, :username] - @parsed_hash.keys) - end - - def user_params - @parsed_hash.tap do |hash| - hash[:username] = valid_username - hash[:password] = hash[:password_confirmation] = random_password - hash[:password_automatically_set] = PASSWORD_AUTOMATICALLY_SET - end - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/scim/deprovisioning_service.rb b/ee/lib/ee/gitlab/scim/deprovisioning_service.rb deleted file mode 100644 index 462eee7839c4ad..00000000000000 --- a/ee/lib/ee/gitlab/scim/deprovisioning_service.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - class DeprovisioningService < BaseDeprovisioningService - def execute - ScimIdentity.transaction do - identity.update!(active: false) - block_user(user) - end - - ServiceResponse.success(message: format(_("User %{user} SCIM identity is deactivated"), user: user.name)) - end - - private - - def block_user(user) - user.system_block - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/scim/filter_parser.rb b/ee/lib/ee/gitlab/scim/filter_parser.rb deleted file mode 100644 index 169fdbe72554a3..00000000000000 --- a/ee/lib/ee/gitlab/scim/filter_parser.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - class FilterParser - FILTER_OPERATORS = %w[eq].freeze - - attr_reader :attribute, :operator, :value - - def initialize(filter) - @attribute, @operator, @value = filter&.split(' ') - end - - def valid? - FILTER_OPERATORS.include?(operator) && attribute_transform.valid? - end - - def params - @params ||= begin - return {} unless valid? - - attribute_transform.map_to(value) - end - end - - private - - def attribute_transform - @attribute_transform ||= AttributeTransform.new(attribute) - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/scim/group/deprovisioning_service.rb b/ee/lib/ee/gitlab/scim/group/deprovisioning_service.rb index fbbd7d6b87591e..91369015e6a5b3 100644 --- a/ee/lib/ee/gitlab/scim/group/deprovisioning_service.rb +++ b/ee/lib/ee/gitlab/scim/group/deprovisioning_service.rb @@ -4,7 +4,7 @@ module EE module Gitlab module Scim module Group - class DeprovisioningService < BaseDeprovisioningService + class DeprovisioningService < ::Gitlab::Scim::BaseDeprovisioningService def execute if group.last_owner?(user) return error(format( diff --git a/ee/lib/ee/gitlab/scim/group/provisioning_service.rb b/ee/lib/ee/gitlab/scim/group/provisioning_service.rb index cdeb07e0800b3e..59234eaeaca651 100644 --- a/ee/lib/ee/gitlab/scim/group/provisioning_service.rb +++ b/ee/lib/ee/gitlab/scim/group/provisioning_service.rb @@ -4,7 +4,7 @@ module EE module Gitlab module Scim module Group - class ProvisioningService < BaseProvisioningService + class ProvisioningService < ::Gitlab::Scim::BaseProvisioningService def execute return error_response(errors: ["Missing params: #{missing_params}"]) unless missing_params.empty? return success_response if existing_identity_and_member? @@ -108,7 +108,7 @@ def existing_user? end def success_response - ProvisioningResponse.new(status: :success, identity: identity) + ::Gitlab::Scim::ProvisioningResponse.new(status: :success, identity: identity) end def log_audit_event diff --git a/ee/lib/ee/gitlab/scim/group_membership_cache_service.rb b/ee/lib/ee/gitlab/scim/group_membership_cache_service.rb deleted file mode 100644 index b0d1aa8627f277..00000000000000 --- a/ee/lib/ee/gitlab/scim/group_membership_cache_service.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - # rubocop:disable Gitlab/EeOnlyClass -- All existing instance SCIM code - # currently lives under ee/ and making it compliant requires a larger - # refactor to be addressed by https://gitlab.com/gitlab-org/gitlab/-/issues/520129. - class GroupMembershipCacheService - BATCH_SIZE = 1000 - - attr_reader :scim_group_uid - - def initialize(scim_group_uid:) - @scim_group_uid = scim_group_uid - end - - def add_users(user_ids) - return if user_ids.empty? - - now = Time.zone.now - - memberships = user_ids.map do |user_id| - Authn::ScimGroupMembership.new( - user_id: user_id, scim_group_uid: scim_group_uid, created_at: now, updated_at: now - ) - end - - Authn::ScimGroupMembership.bulk_upsert!( - memberships, - unique_by: [:user_id, :scim_group_uid], - batch_size: BATCH_SIZE, - validate: false - ) - end - - def remove_users(user_ids) - return if user_ids.empty? - - Authn::ScimGroupMembership.by_scim_group_uid(scim_group_uid).by_user_id(user_ids) - .each_batch(of: BATCH_SIZE) do |batch| - batch.delete_all - end - end - - def replace_users(user_ids) - Authn::ScimGroupMembership.by_scim_group_uid(scim_group_uid).each_batch(of: BATCH_SIZE) do |batch| - batch.delete_all - end - - add_users(user_ids) - end - end - # rubocop:enable Gitlab/EeOnlyClass - end - end -end diff --git a/ee/lib/ee/gitlab/scim/group_sync_deletion_service.rb b/ee/lib/ee/gitlab/scim/group_sync_deletion_service.rb deleted file mode 100644 index f1c26a6cfcbe51..00000000000000 --- a/ee/lib/ee/gitlab/scim/group_sync_deletion_service.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - # rubocop:disable Gitlab/EeOnlyClass -- All existing instance SCIM code - # currently lives under ee/ and making it compliant requires a larger - # refactor to be addressed by https://gitlab.com/gitlab-org/gitlab/-/issues/520129. - class GroupSyncDeletionService - attr_reader :scim_group_uid - - def initialize(scim_group_uid:) - @scim_group_uid = scim_group_uid - end - - def execute - clear_scim_group_uid_from_links - - schedule_membership_cleanup - - ServiceResponse.success - rescue ActiveRecord::ActiveRecordError => e - ServiceResponse.error(message: e.message) - end - - private - - def clear_scim_group_uid_from_links - SamlGroupLink.by_scim_group_uid(scim_group_uid).update_all(scim_group_uid: nil) - end - - def schedule_membership_cleanup - ::Authn::CleanupScimGroupMembershipsWorker.perform_async(scim_group_uid) - end - end - # rubocop:enable Gitlab/EeOnlyClass - end - end -end diff --git a/ee/lib/ee/gitlab/scim/group_sync_patch_service.rb b/ee/lib/ee/gitlab/scim/group_sync_patch_service.rb deleted file mode 100644 index 30beb46ffdc641..00000000000000 --- a/ee/lib/ee/gitlab/scim/group_sync_patch_service.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - # rubocop:disable Gitlab/EeOnlyClass -- All existing instance SCIM code - # currently lives under ee/ and making it compliant requires a larger - # refactor to be addressed by https://gitlab.com/gitlab-org/gitlab/-/issues/520129. - class GroupSyncPatchService - attr_reader :scim_group_uid, :operations - - def initialize(scim_group_uid:, operations:) - @scim_group_uid = scim_group_uid - @operations = operations - end - - def execute - operations.each do |operation| - operation_type = operation[:op].to_s.downcase - - case operation_type - when 'add' - process_add_operation(operation) - when 'remove' - process_remove_operation(operation) - end - end - - ServiceResponse.success - end - - private - - def process_add_operation(operation) - case operation[:path].to_s.downcase - when 'externalid' - # NO-OP - # - # For now we just accept the externalId update but don't store it. - # In some IdPs (e.g. Microsoft Entra), this is part of the group - # sync provisioning cycle. - when 'members' - process_add_members(operation[:value]) - end - end - - def process_remove_operation(operation) - case operation[:path].to_s.downcase - when 'members' - process_remove_members(operation[:value]) - end - end - - def process_add_members(members) - return unless members.is_a?(Array) - - user_ids = collect_user_ids_from_members(members) - return if user_ids.empty? - - ::Authn::SyncScimGroupMembersWorker.perform_async(scim_group_uid, user_ids, 'add') - end - - def process_remove_members(members) - return unless members.is_a?(Array) - - user_ids = collect_user_ids_from_members(members) - return if user_ids.empty? - - ::Authn::SyncScimGroupMembersWorker.perform_async(scim_group_uid, user_ids, 'remove') - end - - def collect_user_ids_from_members(members) - extern_uids = members.filter_map { |member| member[:value] } - return [] if extern_uids.empty? - - identities = ScimIdentity.for_instance.with_extern_uid(extern_uids) - identities.filter_map(&:user_id) - end - end - # rubocop:enable Gitlab/EeOnlyClass - end - end -end diff --git a/ee/lib/ee/gitlab/scim/group_sync_provisioning_service.rb b/ee/lib/ee/gitlab/scim/group_sync_provisioning_service.rb deleted file mode 100644 index c47253fbc114d7..00000000000000 --- a/ee/lib/ee/gitlab/scim/group_sync_provisioning_service.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - # rubocop:disable Gitlab/EeOnlyClass -- All existing instance SCIM code - # currently lives under ee/ and making it compliant requires a larger - # refactor to be addressed by https://gitlab.com/gitlab-org/gitlab/-/issues/520129. - class GroupSyncProvisioningService < BaseProvisioningService - def initialize(parsed_hash) - @parsed_hash = parsed_hash - end - - def execute - return error_response(errors: ["Missing params: #{missing_params}"]) unless missing_params.empty? - return error_response(errors: ["Invalid UUID for scim_group_uid"]) unless valid_scim_group_uid? - - if matching_group_links.exists? - update_group_links - success_response - else - error_response(errors: ["No matching SAML group found with name: #{@parsed_hash[:saml_group_name]}"]) - end - end - - private - - def update_group_links - matching_group_links.update_all(scim_group_uid: @parsed_hash[:scim_group_uid]) - end - - def matching_group_links - @matching_group_links ||= SamlGroupLink.by_saml_group_name(@parsed_hash[:saml_group_name]) - end - - def group_link - matching_group_links.first - end - strong_memoize_attr :group_link - - def success_response - ProvisioningResponse.new(status: :success, group_link: group_link) - end - - def missing_params - required_params = [:saml_group_name, :scim_group_uid] - required_params.select { |param| @parsed_hash[param].blank? } - end - - def valid_scim_group_uid? - uuid_param = @parsed_hash[:scim_group_uid] - - return false unless uuid_param.is_a?(String) - - # UUID format: 8-4-4-4-12 hex digits - uuid_regex = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i - uuid_regex.match?(uuid_param) - end - end - # rubocop:enable Gitlab/EeOnlyClass - end - end -end diff --git a/ee/lib/ee/gitlab/scim/group_sync_put_service.rb b/ee/lib/ee/gitlab/scim/group_sync_put_service.rb deleted file mode 100644 index 75d3a834a59dd9..00000000000000 --- a/ee/lib/ee/gitlab/scim/group_sync_put_service.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - # rubocop:disable Gitlab/EeOnlyClass -- All existing instance SCIM code - # currently lives under ee/ and making it compliant requires a larger - # refactor to be addressed by https://gitlab.com/gitlab-org/gitlab/-/issues/520129. - class GroupSyncPutService - attr_reader :scim_group_uid, :members, :display_name - - def initialize(scim_group_uid:, members:, display_name:) - @scim_group_uid = scim_group_uid - @members = members - @display_name = display_name - end - - def execute - sync_group_membership - - ServiceResponse.success - end - - private - - def sync_group_membership - return unless members.is_a?(Array) - - normalized_members = members.reject(&:blank?) - target_user_ids = fetch_target_user_ids(normalized_members) - - ::Authn::SyncScimGroupMembersWorker.perform_async(scim_group_uid, target_user_ids, 'replace') - end - - def fetch_target_user_ids(normalized_members) - extern_uids = normalized_members.filter_map { |member| member[:value] } - return [] if extern_uids.empty? - - scim_identities = ScimIdentity.for_instance.with_extern_uid(extern_uids) - scim_identities.map(&:user_id) - end - end - # rubocop:enable Gitlab/EeOnlyClass - end - end -end diff --git a/ee/lib/ee/gitlab/scim/params_parser.rb b/ee/lib/ee/gitlab/scim/params_parser.rb deleted file mode 100644 index 201aa88da1fcdb..00000000000000 --- a/ee/lib/ee/gitlab/scim/params_parser.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - class ParamsParser - OPERATIONS_OPERATORS = %w[replace add].freeze - - def initialize(params) - @params = params.with_indifferent_access - @hash = {} - end - - def deprovision_user? - update_params[:active] == false - end - - def reprovision_user? - !deprovision_user? - end - - def post_params - @post_params ||= process_post_params - end - - def update_params - @update_params ||= (process_operations || {}) - end - - def filter_params - @filter_params ||= filter_parser.params - end - - def filter_operator - filter_parser.operator.to_sym if filter_parser.valid? - end - - private - - def filter_parser - @filter_parser ||= FilterParser.new(@params[:filter]) - end - - def process_operations - @params[:Operations]&.each_with_object({}) do |operation, hash| - next unless OPERATIONS_OPERATORS.include?(operation[:op].downcase) - - hash.merge!(transformed_operation(operation)) - end - end - - def process_post_params - overwrites = { email: parse_emails, name: parse_name }.compact - - # compact can remove :active if the value for that is nil - @params.except(overwrites.keys).compact.each_with_object({}) do |(param, value), hash| - hash.merge!(AttributeTransform.new(param).map_to(value)) - end.merge(overwrites) - end - - def parse_emails - emails = @params[:emails] - - return unless emails - - email = emails.find { |email| email[:type] == 'work' || email[:primary] } - email[:value] if email - end - - def parse_name - name = @params.delete(:name) - - return unless name - - formatted_name = name[:formatted]&.presence - formatted_name ||= [name[:givenName], name[:familyName]].compact.join(' ') - @hash[:name] = formatted_name - end - - # The `path` key is optional per the SCIM spec. - # Ex. Azure uses `path` while Okta does not. - def transformed_operation(operation) - key, value = - if operation[:path] - operation.values_at(:path, :value) - else - [operation[:value].each_key.first, operation[:value].each_value.first] - end - - AttributeTransform.new(key).map_to(value) - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/scim/provisioning_response.rb b/ee/lib/ee/gitlab/scim/provisioning_response.rb deleted file mode 100644 index a578697d707bda..00000000000000 --- a/ee/lib/ee/gitlab/scim/provisioning_response.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - class ProvisioningResponse - attr_reader :status, :message, :identity, :group_link - - def initialize(status:, message: nil, identity: nil, group_link: nil) - @status = status - @message = message - @identity = identity - @group_link = group_link - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/scim/provisioning_service.rb b/ee/lib/ee/gitlab/scim/provisioning_service.rb deleted file mode 100644 index 6bdb3e102c6c57..00000000000000 --- a/ee/lib/ee/gitlab/scim/provisioning_service.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true -module EE - module Gitlab - module Scim - class ProvisioningService < BaseProvisioningService - def execute - return error_response(errors: ["Missing params: #{missing_params}"]) unless missing_params.empty? - return success_response if existing_identity? - - clear_memoization(:identity) - - return create_identity if existing_user? - - create_identity_and_user - end - - private - - def create_identity - return success_response if identity.save - - error_response(objects: [identity]) - end - - def identity - ScimIdentity.with_extern_uid(@parsed_hash[:extern_uid]).first || build_scim_identity - end - strong_memoize_attr :identity - - def user - ::User.find_by_any_email(@parsed_hash[:email]) || build_user - end - strong_memoize_attr :user - - def build_user - ::Users::AuthorizedBuildService.new(nil, user_params.except(:extern_uid)).execute - end - - def build_scim_identity - ScimIdentity.new( - user: user, - extern_uid: @parsed_hash[:extern_uid], - active: true - ) - end - - def existing_identity? - identity&.persisted? - end - - def existing_user? - user&.persisted? - end - - def create_identity_and_user - return success_response if user.save && identity.save - - error_response(objects: [identity, user]) - end - - def success_response - ProvisioningResponse.new(status: :success, identity: identity) - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/scim/reprovisioning_service.rb b/ee/lib/ee/gitlab/scim/reprovisioning_service.rb deleted file mode 100644 index ed827d3589cab6..00000000000000 --- a/ee/lib/ee/gitlab/scim/reprovisioning_service.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Scim - class ReprovisioningService - attr_reader :identity - - delegate :user, to: :identity - - def initialize(identity) - @identity = identity - end - - def execute - ScimIdentity.transaction do - identity.update!(active: true) - unblock_user(user) - end - - ServiceResponse.success(message: format(_("User %{user} SCIM identity is reactivated"), user: user.name)) - end - - private - - def unblock_user(user) - user.activate - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/scim/value_parser.rb b/ee/lib/ee/gitlab/scim/value_parser.rb deleted file mode 100644 index da450b5af84852..00000000000000 --- a/ee/lib/ee/gitlab/scim/value_parser.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -# Casts input from a SCIM compValue to a ruby object -# This should be updated to accept the following JSON style inputs: -# false / null / true / number / string -# -# It also needs to accept capitalized True/False from Azure -# -# See https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2 -module EE - module Gitlab - module Scim - class ValueParser - COERCED_VALUES = { - 'true' => true, - 'false' => false - }.freeze - - def initialize(input) - @input = input - end - - def type_cast - return @input unless @input.is_a?(String) - - COERCED_VALUES.fetch(unquoted.downcase, unquoted) - end - - private - - def unquoted - @unquoted ||= @input.delete('\"') - end - end - end - end -end diff --git a/ee/lib/gitlab/scim/attribute_transform.rb b/ee/lib/gitlab/scim/attribute_transform.rb new file mode 100644 index 00000000000000..bf0242155de3ae --- /dev/null +++ b/ee/lib/gitlab/scim/attribute_transform.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class AttributeTransform + MAPPINGS = { + id: :extern_uid, + displayName: :name, + 'name.formatted': :name, + 'emails[type eq "work"].value': :email, + active: :active, + externalId: :extern_uid, + userName: :username + }.with_indifferent_access.freeze + + def initialize(key) + @key = key + end + + def valid? + MAPPINGS.key?(@key) + end + + def gitlab_key + MAPPINGS[@key] + end + + def map_to(input) + return {} unless valid? + + { gitlab_key => ValueParser.new(input).type_cast } + end + end + end +end diff --git a/ee/lib/gitlab/scim/base_deprovisioning_service.rb b/ee/lib/gitlab/scim/base_deprovisioning_service.rb new file mode 100644 index 00000000000000..dceee4190d69c2 --- /dev/null +++ b/ee/lib/gitlab/scim/base_deprovisioning_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class BaseDeprovisioningService + include ::Gitlab::Utils::StrongMemoize + + attr_reader :identity + + delegate :user, :group, to: :identity + + def initialize(identity) + @identity = identity + end + + private + + def error(message) + ServiceResponse.error(message: message) + end + end + end +end diff --git a/ee/lib/gitlab/scim/base_provisioning_service.rb b/ee/lib/gitlab/scim/base_provisioning_service.rb new file mode 100644 index 00000000000000..cbefc3b7aae99c --- /dev/null +++ b/ee/lib/gitlab/scim/base_provisioning_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class BaseProvisioningService + include ::Gitlab::Utils::StrongMemoize + + PASSWORD_AUTOMATICALLY_SET = true + SKIP_EMAIL_CONFIRMATION = false + + def initialize(parsed_hash, group = nil) + raise ArgumentError, 'Group cannot be nil' if group.nil? && ::Gitlab.com? # rubocop:disable Gitlab/AvoidGitlabInstanceChecks -- Existing code being moved. + + @group = group + @parsed_hash = parsed_hash.dup + end + + private + + def error_response(errors: nil, objects: []) + errors ||= objects.compact.flat_map { |obj| obj.errors.full_messages } + conflict = errors.any? { |error| error.include?('has already been taken') } + + ProvisioningResponse.new(status: conflict ? :conflict : :error, message: errors.to_sentence) + end + + def logger + ::API::API.logger + end + + def random_password + ::User.random_password + end + + def valid_username + ::Gitlab::Auth::ExternalUsernameSanitizer.new(@parsed_hash[:username]).sanitize + end + + def missing_params + @missing_params ||= ([:extern_uid, :email, :username] - @parsed_hash.keys) + end + + def user_params + @parsed_hash.tap do |hash| + hash[:username] = valid_username + hash[:password] = hash[:password_confirmation] = random_password + hash[:password_automatically_set] = PASSWORD_AUTOMATICALLY_SET + end + end + end + end +end diff --git a/ee/lib/gitlab/scim/deprovisioning_service.rb b/ee/lib/gitlab/scim/deprovisioning_service.rb new file mode 100644 index 00000000000000..7ad43e1f81e439 --- /dev/null +++ b/ee/lib/gitlab/scim/deprovisioning_service.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class DeprovisioningService < BaseDeprovisioningService + def execute + ScimIdentity.transaction do + identity.update!(active: false) + block_user(user) + end + + ServiceResponse.success(message: format(_("User %{user} SCIM identity is deactivated"), user: user.name)) + end + + private + + def block_user(user) + user.system_block + end + end + end +end diff --git a/ee/lib/gitlab/scim/filter_parser.rb b/ee/lib/gitlab/scim/filter_parser.rb new file mode 100644 index 00000000000000..a50f8a5f819169 --- /dev/null +++ b/ee/lib/gitlab/scim/filter_parser.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class FilterParser + FILTER_OPERATORS = %w[eq].freeze + + attr_reader :attribute, :operator, :value + + def initialize(filter) + @attribute, @operator, @value = filter&.split(' ') + end + + def valid? + FILTER_OPERATORS.include?(operator) && attribute_transform.valid? + end + + def params + @params ||= if valid? + attribute_transform.map_to(value) + else + {} + end + end + + private + + def attribute_transform + @attribute_transform ||= AttributeTransform.new(attribute) + end + end + end +end diff --git a/ee/lib/gitlab/scim/group_membership_cache_service.rb b/ee/lib/gitlab/scim/group_membership_cache_service.rb new file mode 100644 index 00000000000000..7bcbfffe5bd66f --- /dev/null +++ b/ee/lib/gitlab/scim/group_membership_cache_service.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class GroupMembershipCacheService + BATCH_SIZE = 1000 + + attr_reader :scim_group_uid + + def initialize(scim_group_uid:) + @scim_group_uid = scim_group_uid + end + + def add_users(user_ids) + return if user_ids.empty? + + now = Time.zone.now + + memberships = user_ids.map do |user_id| + Authn::ScimGroupMembership.new( + user_id: user_id, scim_group_uid: scim_group_uid, created_at: now, updated_at: now + ) + end + + Authn::ScimGroupMembership.bulk_upsert!( + memberships, + unique_by: [:user_id, :scim_group_uid], + batch_size: BATCH_SIZE, + validate: false + ) + end + + def remove_users(user_ids) + return if user_ids.empty? + + Authn::ScimGroupMembership.by_scim_group_uid(scim_group_uid).by_user_id(user_ids) + .each_batch(of: BATCH_SIZE) do |batch| + batch.delete_all + end + end + + def replace_users(user_ids) + Authn::ScimGroupMembership.by_scim_group_uid(scim_group_uid).each_batch(of: BATCH_SIZE) do |batch| + batch.delete_all + end + + add_users(user_ids) + end + end + end +end diff --git a/ee/lib/gitlab/scim/group_sync_deletion_service.rb b/ee/lib/gitlab/scim/group_sync_deletion_service.rb new file mode 100644 index 00000000000000..f37211c1d5d2ff --- /dev/null +++ b/ee/lib/gitlab/scim/group_sync_deletion_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class GroupSyncDeletionService + attr_reader :scim_group_uid + + def initialize(scim_group_uid:) + @scim_group_uid = scim_group_uid + end + + def execute + clear_scim_group_uid_from_links + + schedule_membership_cleanup + + ServiceResponse.success + rescue ActiveRecord::ActiveRecordError => e + ServiceResponse.error(message: e.message) + end + + private + + def clear_scim_group_uid_from_links + SamlGroupLink.by_scim_group_uid(scim_group_uid).update_all(scim_group_uid: nil) + end + + def schedule_membership_cleanup + ::Authn::CleanupScimGroupMembershipsWorker.perform_async(scim_group_uid) + end + end + end +end diff --git a/ee/lib/gitlab/scim/group_sync_patch_service.rb b/ee/lib/gitlab/scim/group_sync_patch_service.rb new file mode 100644 index 00000000000000..e4a00510b57da6 --- /dev/null +++ b/ee/lib/gitlab/scim/group_sync_patch_service.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class GroupSyncPatchService + attr_reader :scim_group_uid, :operations + + def initialize(scim_group_uid:, operations:) + @scim_group_uid = scim_group_uid + @operations = operations + end + + def execute + operations.each do |operation| + operation_type = operation[:op].to_s.downcase + + case operation_type + when 'add' + process_add_operation(operation) + when 'remove' + process_remove_operation(operation) + end + end + + ServiceResponse.success + end + + private + + def process_add_operation(operation) + case operation[:path].to_s.downcase + when 'externalid' + # NO-OP + # + # For now we just accept the externalId update but don't store it. + # In some IdPs (e.g. Microsoft Entra), this is part of the group + # sync provisioning cycle. + when 'members' + process_add_members(operation[:value]) + end + end + + def process_remove_operation(operation) + case operation[:path].to_s.downcase + when 'members' + process_remove_members(operation[:value]) + end + end + + def process_add_members(members) + return unless members.is_a?(Array) + + user_ids = collect_user_ids_from_members(members) + return if user_ids.empty? + + ::Authn::SyncScimGroupMembersWorker.perform_async(scim_group_uid, user_ids, 'add') + end + + def process_remove_members(members) + return unless members.is_a?(Array) + + user_ids = collect_user_ids_from_members(members) + return if user_ids.empty? + + ::Authn::SyncScimGroupMembersWorker.perform_async(scim_group_uid, user_ids, 'remove') + end + + def collect_user_ids_from_members(members) + extern_uids = members.filter_map { |member| member[:value] } + return [] if extern_uids.empty? + + identities = ScimIdentity.for_instance.with_extern_uid(extern_uids) + identities.filter_map(&:user_id) + end + end + end +end diff --git a/ee/lib/gitlab/scim/group_sync_provisioning_service.rb b/ee/lib/gitlab/scim/group_sync_provisioning_service.rb new file mode 100644 index 00000000000000..5740e495f731fc --- /dev/null +++ b/ee/lib/gitlab/scim/group_sync_provisioning_service.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class GroupSyncProvisioningService < BaseProvisioningService + def initialize(parsed_hash) + @parsed_hash = parsed_hash + end + + def execute + return error_response(errors: ["Missing params: #{missing_params}"]) unless missing_params.empty? + return error_response(errors: ["Invalid UUID for scim_group_uid"]) unless valid_scim_group_uid? + + if matching_group_links.exists? + update_group_links + success_response + else + error_response(errors: ["No matching SAML group found with name: #{@parsed_hash[:saml_group_name]}"]) + end + end + + private + + def update_group_links + matching_group_links.update_all(scim_group_uid: @parsed_hash[:scim_group_uid]) + end + + def matching_group_links + @matching_group_links ||= SamlGroupLink.by_saml_group_name(@parsed_hash[:saml_group_name]) + end + + def group_link + matching_group_links.first + end + strong_memoize_attr :group_link + + def success_response + ProvisioningResponse.new(status: :success, group_link: group_link) + end + + def missing_params + required_params = [:saml_group_name, :scim_group_uid] + required_params.select { |param| @parsed_hash[param].blank? } + end + + def valid_scim_group_uid? + uuid_param = @parsed_hash[:scim_group_uid] + + return false unless uuid_param.is_a?(String) + + # UUID format: 8-4-4-4-12 hex digits + uuid_regex = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i + uuid_regex.match?(uuid_param) + end + end + end +end diff --git a/ee/lib/gitlab/scim/group_sync_put_service.rb b/ee/lib/gitlab/scim/group_sync_put_service.rb new file mode 100644 index 00000000000000..3a7fe73200f77c --- /dev/null +++ b/ee/lib/gitlab/scim/group_sync_put_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class GroupSyncPutService + attr_reader :scim_group_uid, :members, :display_name + + def initialize(scim_group_uid:, members:, display_name:) + @scim_group_uid = scim_group_uid + @members = members + @display_name = display_name + end + + def execute + sync_group_membership + + ServiceResponse.success + end + + private + + def sync_group_membership + return unless members.is_a?(Array) + + normalized_members = members.reject(&:blank?) + target_user_ids = fetch_target_user_ids(normalized_members) + + ::Authn::SyncScimGroupMembersWorker.perform_async(scim_group_uid, target_user_ids, 'replace') + end + + def fetch_target_user_ids(normalized_members) + extern_uids = normalized_members.filter_map { |member| member[:value] } + return [] if extern_uids.empty? + + scim_identities = ScimIdentity.for_instance.with_extern_uid(extern_uids) + scim_identities.map(&:user_id) + end + end + end +end diff --git a/ee/lib/gitlab/scim/params_parser.rb b/ee/lib/gitlab/scim/params_parser.rb new file mode 100644 index 00000000000000..aef3ac11648bd8 --- /dev/null +++ b/ee/lib/gitlab/scim/params_parser.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class ParamsParser + OPERATIONS_OPERATORS = %w[replace add].freeze + + def initialize(params) + @params = params.with_indifferent_access + @hash = {} + end + + def deprovision_user? + update_params[:active] == false + end + + def reprovision_user? + !deprovision_user? + end + + def post_params + @post_params ||= process_post_params + end + + def update_params + @update_params ||= process_operations || {} + end + + def filter_params + @filter_params ||= filter_parser.params + end + + def filter_operator + filter_parser.operator.to_sym if filter_parser.valid? + end + + private + + def filter_parser + @filter_parser ||= FilterParser.new(@params[:filter]) + end + + def process_operations + @params[:Operations]&.each_with_object({}) do |operation, hash| + next unless OPERATIONS_OPERATORS.include?(operation[:op].downcase) + + hash.merge!(transformed_operation(operation)) + end + end + + def process_post_params + overwrites = { email: parse_emails, name: parse_name }.compact + + # compact can remove :active if the value for that is nil + @params.except(overwrites.keys).compact.each_with_object({}) do |(param, value), hash| + hash.merge!(AttributeTransform.new(param).map_to(value)) + end.merge(overwrites) + end + + def parse_emails + emails = @params[:emails] + + return unless emails + + email = emails.find { |email| email[:type] == 'work' || email[:primary] } + email[:value] if email + end + + def parse_name + name = @params.delete(:name) + + return unless name + + formatted_name = name[:formatted]&.presence + formatted_name ||= [name[:givenName], name[:familyName]].compact.join(' ') + @hash[:name] = formatted_name + end + + # The `path` key is optional per the SCIM spec. + # Ex. Azure uses `path` while Okta does not. + def transformed_operation(operation) + key, value = + if operation[:path] + operation.values_at(:path, :value) + else + [operation[:value].each_key.first, operation[:value].each_value.first] + end + + AttributeTransform.new(key).map_to(value) + end + end + end +end diff --git a/ee/lib/gitlab/scim/provisioning_response.rb b/ee/lib/gitlab/scim/provisioning_response.rb new file mode 100644 index 00000000000000..fa171b2320d8b4 --- /dev/null +++ b/ee/lib/gitlab/scim/provisioning_response.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class ProvisioningResponse + attr_reader :status, :message, :identity, :group_link + + def initialize(status:, message: nil, identity: nil, group_link: nil) + @status = status + @message = message + @identity = identity + @group_link = group_link + end + end + end +end diff --git a/ee/lib/gitlab/scim/provisioning_service.rb b/ee/lib/gitlab/scim/provisioning_service.rb new file mode 100644 index 00000000000000..405acdf3fdf29c --- /dev/null +++ b/ee/lib/gitlab/scim/provisioning_service.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class ProvisioningService < BaseProvisioningService + def execute + return error_response(errors: ["Missing params: #{missing_params}"]) unless missing_params.empty? + return success_response if existing_identity? + + clear_memoization(:identity) + + return create_identity if existing_user? + + create_identity_and_user + end + + private + + def create_identity + return success_response if identity.save + + error_response(objects: [identity]) + end + + def identity + ScimIdentity.with_extern_uid(@parsed_hash[:extern_uid]).first || build_scim_identity + end + strong_memoize_attr :identity + + def user + ::User.find_by_any_email(@parsed_hash[:email]) || build_user + end + strong_memoize_attr :user + + def build_user + ::Users::AuthorizedBuildService.new(nil, user_params.except(:extern_uid)).execute + end + + def build_scim_identity + ScimIdentity.new( + user: user, + extern_uid: @parsed_hash[:extern_uid], + active: true + ) + end + + def existing_identity? + identity&.persisted? + end + + def existing_user? + user&.persisted? + end + + def create_identity_and_user + return success_response if user.save && identity.save + + error_response(objects: [identity, user]) + end + + def success_response + ProvisioningResponse.new(status: :success, identity: identity) + end + end + end +end diff --git a/ee/lib/gitlab/scim/reprovisioning_service.rb b/ee/lib/gitlab/scim/reprovisioning_service.rb new file mode 100644 index 00000000000000..f8f77c202c8e3a --- /dev/null +++ b/ee/lib/gitlab/scim/reprovisioning_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Scim + class ReprovisioningService + attr_reader :identity + + delegate :user, to: :identity + + def initialize(identity) + @identity = identity + end + + def execute + ScimIdentity.transaction do + identity.update!(active: true) + unblock_user(user) + end + + ServiceResponse.success(message: format(_("User %{user} SCIM identity is reactivated"), user: user.name)) + end + + private + + def unblock_user(user) + user.activate + end + end + end +end diff --git a/ee/lib/gitlab/scim/value_parser.rb b/ee/lib/gitlab/scim/value_parser.rb new file mode 100644 index 00000000000000..2a16ade1d27de2 --- /dev/null +++ b/ee/lib/gitlab/scim/value_parser.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Casts input from a SCIM compValue to a ruby object +# This should be updated to accept the following JSON style inputs: +# false / null / true / number / string +# +# It also needs to accept capitalized True/False from Azure +# +# See https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2 +module Gitlab + module Scim + class ValueParser + COERCED_VALUES = { + 'true' => true, + 'false' => false + }.freeze + + def initialize(input) + @input = input + end + + def type_cast + return @input unless @input.is_a?(String) + + COERCED_VALUES.fetch(unquoted.downcase, unquoted) + end + + private + + def unquoted + @unquoted ||= @input.delete('\"') + end + end + end +end diff --git a/ee/spec/lib/ee/api/entities/scim/conflict_spec.rb b/ee/spec/lib/ee/api/entities/scim/conflict_spec.rb deleted file mode 100644 index b6c1debdde5d4a..00000000000000 --- a/ee/spec/lib/ee/api/entities/scim/conflict_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::API::Entities::Scim::Conflict do - let(:params) { { detail: 'error' } } - let(:entity) do - described_class.new(params) - end - - subject { entity.as_json } - - it 'contains the schemas' do - expect(subject[:schemas]).not_to be_empty - end - - it 'contains the detail' do - expect(subject[:detail]).to eq(params[:detail]) - end - - it 'contains the status' do - expect(subject[:status]).to eq(409) - end -end diff --git a/ee/spec/lib/ee/api/entities/scim/emails_spec.rb b/ee/spec/lib/ee/api/entities/scim/emails_spec.rb deleted file mode 100644 index 9de5f7857380c5..00000000000000 --- a/ee/spec/lib/ee/api/entities/scim/emails_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::API::Entities::Scim::Emails do - let(:user) { build(:user) } - let(:identity) { build(:group_saml_identity, user: user) } - - let(:entity) do - described_class.new(user) - end - - subject { entity.as_json } - - it 'contains the email' do - expect(subject[:value]).to eq(user.email) - end - - it 'contains the type' do - expect(subject[:type]).to eq('work') - end - - it 'contains the email' do - expect(subject[:primary]).to be true - end -end diff --git a/ee/spec/lib/ee/api/entities/scim/error_spec.rb b/ee/spec/lib/ee/api/entities/scim/error_spec.rb deleted file mode 100644 index 4896226f10d934..00000000000000 --- a/ee/spec/lib/ee/api/entities/scim/error_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::API::Entities::Scim::Error do - let(:params) { { detail: 'error' } } - let(:entity) do - described_class.new(params) - end - - subject { entity.as_json } - - it 'contains the schemas' do - expect(subject[:schemas]).not_to be_empty - end - - it 'contains the detail' do - expect(subject[:detail]).to eq(params[:detail]) - end - - it 'contains the status' do - expect(subject[:status]).to eq(412) - end -end diff --git a/ee/spec/lib/ee/api/entities/scim/group_spec.rb b/ee/spec/lib/ee/api/entities/scim/group_spec.rb deleted file mode 100644 index 578b5bd68dac75..00000000000000 --- a/ee/spec/lib/ee/api/entities/scim/group_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::API::Entities::Scim::Group, feature_category: :system_access do - let(:group_link) { build(:saml_group_link, saml_group_name: 'engineering', scim_group_uid: 'group-123') } - let(:entity) { described_class.new(group_link) } - - subject(:json_response) { entity.as_json } - - it 'contains the schemas' do - expect(json_response[:schemas]).to eq(['urn:ietf:params:scim:schemas:core:2.0:Group']) - end - - it 'contains the SCIM group uid' do - expect(json_response[:id]).to eq(group_link.scim_group_uid) - end - - it 'contains the display name' do - expect(json_response[:displayName]).to eq(group_link.saml_group_name) - end - - it 'contains an empty members array' do - expect(json_response[:members]).to eq([]) - end - - it 'contains the resource type' do - expect(json_response[:meta][:resourceType]).to eq('Group') - end - - context 'when members are excluded' do - subject(:json_response) { described_class.new(group_link, excluded_attributes: ['members']).as_json } - - it 'does not include members in the response' do - expect(json_response).not_to include(:members) - end - end -end diff --git a/ee/spec/lib/ee/api/entities/scim/groups_spec.rb b/ee/spec/lib/ee/api/entities/scim/groups_spec.rb deleted file mode 100644 index 69c2c7af5d66cd..00000000000000 --- a/ee/spec/lib/ee/api/entities/scim/groups_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe EE::API::Entities::Scim::Groups, feature_category: :system_access do - let_it_be(:group1) { build(:saml_group_link, saml_group_name: 'Engineering', scim_group_uid: SecureRandom.uuid) } - let_it_be(:group2) { build(:saml_group_link, saml_group_name: 'Marketing', scim_group_uid: SecureRandom.uuid) } - - let(:resources) { [group1, group2] } - let(:options) { {} } - - let(:result_set) do - { - resources: resources, - total_results: 2, - items_per_page: 20, - start_index: 1 - } - end - - subject(:json_response) { described_class.represent(result_set, options).as_json } - - it 'exposes the correct SCIM schema' do - expect(json_response[:schemas]).to eq(['urn:ietf:params:scim:api:messages:2.0:ListResponse']) - end - - it 'exposes pagination metadata' do - expect(json_response[:totalResults]).to eq(2) - expect(json_response[:itemsPerPage]).to eq(20) - expect(json_response[:startIndex]).to eq(1) - end - - it 'exposes resources as an array' do - expect(json_response[:Resources]).to be_an_instance_of(Array) - expect(json_response[:Resources].length).to eq(2) - end - - it 'represents each resource using the Group entity' do - expect(json_response[:Resources][0][:displayName]).to eq('Engineering') - expect(json_response[:Resources][1][:displayName]).to eq('Marketing') - end - - context 'with excluded attributes' do - let(:options) { { excluded_attributes: ['members'] } } - - it 'passes excluded_attributes to the Group entity' do - expect(EE::API::Entities::Scim::Group).to receive(:represent) - .with(anything, hash_including(excluded_attributes: ['members'])) - .and_call_original - - json_response - end - end - - context 'with default values' do - let(:result_set) { { resources: resources } } - - it 'uses default values for pagination metadata' do - expect(json_response[:totalResults]).to eq(2) - expect(json_response[:itemsPerPage]).to eq(Kaminari.config.default_per_page) - expect(json_response[:startIndex]).to eq(1) - end - end - - context 'with empty resources' do - let(:result_set) { {} } - - it 'handles empty resources gracefully' do - expect(json_response[:Resources]).to eq([]) - expect(json_response[:totalResults]).to eq(0) - end - end -end diff --git a/ee/spec/lib/ee/api/entities/scim/not_found_spec.rb b/ee/spec/lib/ee/api/entities/scim/not_found_spec.rb deleted file mode 100644 index 83441c45870638..00000000000000 --- a/ee/spec/lib/ee/api/entities/scim/not_found_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::API::Entities::Scim::NotFound do - let(:entity) do - described_class.new({}) - end - - subject { entity.as_json } - - it 'contains the schemas' do - expect(subject[:schemas]).not_to be_empty - end - - it 'contains the detail' do - expect(subject[:detail]).to be_nil - end - - it 'contains the status' do - expect(subject[:status]).to eq(404) - end -end diff --git a/ee/spec/lib/ee/api/entities/scim/user_name_spec.rb b/ee/spec/lib/ee/api/entities/scim/user_name_spec.rb deleted file mode 100644 index fcdca65cc5cd2c..00000000000000 --- a/ee/spec/lib/ee/api/entities/scim/user_name_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::API::Entities::Scim::UserName do - let(:user) { build(:user) } - - subject { described_class.new(user).as_json } - - it 'contains the name' do - expect(subject[:formatted]).to eq(user.name) - end - - it 'contains the first name' do - expect(subject[:givenName]).to eq(user.first_name) - end - - it 'contains the last name' do - expect(subject[:familyName]).to eq(user.last_name) - end -end diff --git a/ee/spec/lib/ee/api/entities/scim/user_spec.rb b/ee/spec/lib/ee/api/entities/scim/user_spec.rb deleted file mode 100644 index 0e04acdea4a886..00000000000000 --- a/ee/spec/lib/ee/api/entities/scim/user_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::API::Entities::Scim::User do - let(:user) { build(:user) } - let(:identity) { build(:group_saml_identity, user: user) } - - let(:entity) do - described_class.new(identity) - end - - subject { entity.as_json } - - it 'contains the schemas' do - expect(subject[:schemas]).to eq(["urn:ietf:params:scim:schemas:core:2.0:User"]) - end - - it 'contains the extern UID' do - expect(subject[:id]).to eq(identity.extern_uid) - end - - it 'contains the active flag' do - expect(subject[:active]).to be true - end - - it 'contains the name' do - expect(subject[:name][:formatted]).to eq(user.name) - end - - it 'contains the first name' do - expect(subject[:name][:givenName]).to eq(user.first_name) - end - - it 'contains the last name' do - expect(subject[:name][:familyName]).to eq(user.last_name) - end - - it 'contains the email' do - expect(subject[:emails].first[:value]).to eq(user.email) - end - - it 'contains the username' do - expect(subject[:userName]).to eq(user.username) - end - - it 'contains the resource type' do - expect(subject[:meta][:resourceType]).to eq('User') - end - - context 'with a SCIM identity' do - let(:identity) { build(:scim_identity, user: user) } - - it 'contains active false when the identity is not active' do - identity.active = false - - expect(subject[:active]).to be false - end - end -end diff --git a/ee/spec/lib/ee/api/entities/scim/users_spec.rb b/ee/spec/lib/ee/api/entities/scim/users_spec.rb deleted file mode 100644 index c7b6795537798e..00000000000000 --- a/ee/spec/lib/ee/api/entities/scim/users_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::API::Entities::Scim::Users do - let(:user) { build(:user) } - let(:identity) { build(:group_saml_identity, user: user) } - - let(:entity) do - described_class.new(resources: [identity]) - end - - subject { entity.as_json } - - it 'contains the schemas' do - expect(subject[:schemas]).to eq(['urn:ietf:params:scim:api:messages:2.0:ListResponse']) - end - - it 'calculates the totalResults' do - expect(subject[:totalResults]).to eq(1) - end - - it 'contains the default itemsPerPage' do - expect(subject[:itemsPerPage]).to eq(20) - end - - it 'contains the default startIndex' do - expect(subject[:startIndex]).to eq(1) - end - - it 'contains the user' do - expect(subject[:Resources]).not_to be_empty - end - - it 'contains the user ID' do - expect(subject[:Resources].first[:id]).to eq(identity.extern_uid) - end - - context 'with configured values' do - let(:entity) do - described_class.new(resources: [identity], total_results: 31, items_per_page: 10, start_index: 30) - end - - it 'contains the configured totalResults' do - expect(subject[:totalResults]).to eq(31) - end - - it 'contains the configured itemsPerPage' do - expect(subject[:itemsPerPage]).to eq(10) - end - - it 'contains the configured startIndex' do - expect(subject[:startIndex]).to eq(30) - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/attribute_transform_spec.rb b/ee/spec/lib/ee/gitlab/scim/attribute_transform_spec.rb deleted file mode 100644 index 14d1caa8e46565..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/attribute_transform_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe EE::Gitlab::Scim::AttributeTransform do - using RSpec::Parameterized::TableSyntax - - describe '#valid?' do - it 'is true for accepted keys' do - expect(described_class.new(:userName)).to be_valid - end - - it 'is false for unused keys' do - expect(described_class.new(:someUnknownKey)).not_to be_valid - end - end - - describe '#gitlab_key' do - where(:scim_key, :expected) do - :id | :extern_uid - :displayName | :name - 'name.formatted' | :name - 'emails[type eq "work"].value' | :email - :active | :active - :externalId | :extern_uid - :userName | :username - end - - with_them do - it do - expect(described_class.new(scim_key).gitlab_key).to eq expected - end - end - end - - describe '#map_to' do - it 'returns an empty hash for unknown keys' do - expect(described_class.new(:abc).map_to(double)).to eq({}) - end - - it 'typecasts input' do - expect(described_class.new(:active).map_to('true')).to eq(active: true) - end - - it 'creates a hash from transformed key to a typecasted value' do - expect(described_class.new(:userName).map_to('"my_handle"')).to eq(username: 'my_handle') - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/deprovisioning_service_spec.rb b/ee/spec/lib/ee/gitlab/scim/deprovisioning_service_spec.rb deleted file mode 100644 index f70db846605972..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/deprovisioning_service_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::Gitlab::Scim::DeprovisioningService, feature_category: :system_access do - describe '#execute' do - let(:identity) { create(:scim_identity, active: true) } - let(:user) { identity.user } - - let(:service) { described_class.new(identity) } - - context 'when user is successfully removed' do - it 'deactivates scim identity' do - expect { service.execute }.to change { identity.active }.from(true).to(false) - end - - it 'blocks the user' do - service.execute - - expect(user.ldap_blocked?).to eq(true) - end - - it 'returns the successful deprovision message' do - response = service.execute - - expect(response.message).to include("User #{user.name} SCIM identity is deactivated") - end - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/filter_parser_spec.rb b/ee/spec/lib/ee/gitlab/scim/filter_parser_spec.rb deleted file mode 100644 index 0f48cb3d944d9a..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/filter_parser_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe EE::Gitlab::Scim::FilterParser, feature_category: :system_access do - describe '#operator' do - it 'is extracted from the filter' do - expect(described_class.new('displayName ne ""').operator).to eq 'ne' - end - end - - describe '#valid?' do - it 'succeeds when the operator is supported' do - expect(described_class.new('userName eq "nick"')).to be_valid - end - - it 'fails with unsupported operators' do - expect(described_class.new('userName is "nick"')).not_to be_valid - end - - it 'fails when the attribute path is unsupported' do - expect(described_class.new('user_name eq "nick"')).not_to be_valid - end - end - - describe '#params' do - it 'returns a mapping to filter on' do - expect(described_class.new('userName eq "nick"').params).to eq(username: 'nick') - end - - it 'returns an empty hash when invalid' do - expect(described_class.new('userName is "nick"').params).to eq({}) - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/group/provisioning_service_spec.rb b/ee/spec/lib/ee/gitlab/scim/group/provisioning_service_spec.rb index 2df5be22d2eded..0966263e8a3e73 100644 --- a/ee/spec/lib/ee/gitlab/scim/group/provisioning_service_spec.rb +++ b/ee/spec/lib/ee/gitlab/scim/group/provisioning_service_spec.rb @@ -315,7 +315,7 @@ def user end let(:provision_response) do - ::EE::Gitlab::Scim::ProvisioningResponse.new(identity: nil, + Gitlab::Scim::ProvisioningResponse.new(identity: nil, status: :error, message: "Extern uid can't be blank") end @@ -339,7 +339,7 @@ def user it 'returns provision response error' do response = service.execute - expect(response).to be_a(::EE::Gitlab::Scim::ProvisioningResponse) + expect(response).to be_a(Gitlab::Scim::ProvisioningResponse) expect(response.as_json).to match(hash_including('status' => 'error', 'message' => /^undefined method `save' for nil/, 'identity' => nil)) diff --git a/ee/spec/lib/ee/gitlab/scim/group_membership_cache_service_spec.rb b/ee/spec/lib/ee/gitlab/scim/group_membership_cache_service_spec.rb deleted file mode 100644 index 27d54f87996dab..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/group_membership_cache_service_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe EE::Gitlab::Scim::GroupMembershipCacheService, feature_category: :system_access do - let(:scim_group_uid) { SecureRandom.uuid } - - let_it_be(:user1) { create(:user) } - let_it_be(:user2) { create(:user) } - let_it_be(:user3) { create(:user) } - - let(:service) { described_class.new(scim_group_uid: scim_group_uid) } - - describe '#add_users' do - it 'adds users to the SCIM group cache' do - expect { service.add_users([user1.id, user2.id]) }.to change { Authn::ScimGroupMembership.count }.by(2) - - expect(Authn::ScimGroupMembership.by_scim_group_uid(scim_group_uid).map(&:user_id)) - .to match_array([user1.id, user2.id]) - end - - it 'does nothing if users is empty' do - expect { service.add_users([]) }.not_to change { Authn::ScimGroupMembership.count } - end - end - - describe '#remove_users' do - before do - create(:scim_group_membership, user: user1, scim_group_uid: scim_group_uid) - create(:scim_group_membership, user: user2, scim_group_uid: scim_group_uid) - create(:scim_group_membership, user: user3, scim_group_uid: scim_group_uid) - end - - it 'removes specified users from the SCIM group cache' do - expect { service.remove_users([user1.id, user2.id]) }.to change { Authn::ScimGroupMembership.count }.by(-2) - - remaining_user_ids = Authn::ScimGroupMembership.by_scim_group_uid(scim_group_uid).map(&:user_id) - expect(remaining_user_ids).to eq([user3.id]) - end - - it 'does nothing if users is empty' do - expect { service.remove_users([]) }.not_to change { Authn::ScimGroupMembership.count } - end - end - - describe '#replace_users' do - before do - create(:scim_group_membership, user: user1, scim_group_uid: scim_group_uid) - create(:scim_group_membership, user: user2, scim_group_uid: scim_group_uid) - end - - it 'replaces all users in the SCIM group cache' do - service.replace_users([user2.id, user3.id]) - - user_ids = Authn::ScimGroupMembership.by_scim_group_uid(scim_group_uid).map(&:user_id) - expect(user_ids).to match_array([user2.id, user3.id]) - end - - it 'clears cache when replacing with empty list' do - expect { service.replace_users([]) } - .to change { Authn::ScimGroupMembership.by_scim_group_uid(scim_group_uid).count }.to(0) - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/group_sync_deletion_service_spec.rb b/ee/spec/lib/ee/gitlab/scim/group_sync_deletion_service_spec.rb deleted file mode 100644 index 1e0f1b92a18320..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/group_sync_deletion_service_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe EE::Gitlab::Scim::GroupSyncDeletionService, feature_category: :system_access do - let(:scim_group_uid) { SecureRandom.uuid } - let(:service) { described_class.new(scim_group_uid: scim_group_uid) } - - describe '#execute' do - let!(:saml_group_link) do - create(:saml_group_link, saml_group_name: 'engineering', scim_group_uid: scim_group_uid) - end - - it 'returns success' do - result = service.execute - - expect(result).to be_success - end - - it 'clears scim_group_uid from SAML group link' do - expect { service.execute }.to change { saml_group_link.reload.scim_group_uid }.from(scim_group_uid).to(nil) - end - - context 'with multiple group links' do - let!(:another_group_link) do - create(:saml_group_link, saml_group_name: 'engineering', scim_group_uid: scim_group_uid) - end - - it 'clears scim_group_uid from all matching links' do - service.execute - - expect(saml_group_link.reload.scim_group_uid).to be_nil - expect(another_group_link.reload.scim_group_uid).to be_nil - end - end - - it 'schedules Authn::CleanupScimGroupMembershipsWorker' do - expect(::Authn::CleanupScimGroupMembershipsWorker).to receive(:perform_async).with(scim_group_uid) - - service.execute - end - - context 'when database error occurs' do - before do - allow(SamlGroupLink).to receive(:by_scim_group_uid).and_raise(ActiveRecord::ActiveRecordError, 'Database error') - end - - it 'returns error response' do - result = service.execute - - expect(result).to be_error - expect(result.message).to include('Database error') - end - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/group_sync_patch_service_spec.rb b/ee/spec/lib/ee/gitlab/scim/group_sync_patch_service_spec.rb deleted file mode 100644 index f08d7fc8d6b5a0..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/group_sync_patch_service_spec.rb +++ /dev/null @@ -1,177 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe EE::Gitlab::Scim::GroupSyncPatchService, feature_category: :system_access do - let(:scim_group_uid) { SecureRandom.uuid } - let(:service) { described_class.new(scim_group_uid: scim_group_uid, operations: operations) } - - describe '#execute' do - context 'with add operation' do - let_it_be(:user) { create(:user) } - let_it_be(:identity) { create(:scim_identity, user: user, group: nil) } - let(:operations) do - [ - { - op: 'Add', - path: 'members', - value: [{ value: identity.extern_uid }] - } - ] - end - - it 'schedules the worker to add members' do - expect(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - .with(scim_group_uid, [user.id], 'add') - - service.execute - end - - it 'returns success' do - allow(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - - result = service.execute - - expect(result).to be_success - end - - context 'with case-insensitive operation' do - let(:operations) do - [ - { - op: 'ADD', - path: 'MEMBERS', - value: [{ value: identity.extern_uid }] - } - ] - end - - it 'schedules the worker correctly' do - expect(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - .with(scim_group_uid, [user.id], 'add') - - service.execute - end - end - - context 'with non-existent user identity' do - let(:operations) do - [ - { - op: 'Add', - path: 'members', - value: [{ value: 'non-existent' }] - } - ] - end - - it 'does not schedule the worker' do - expect(Authn::SyncScimGroupMembersWorker).not_to receive(:perform_async) - - service.execute - end - end - - context 'with externalId operation' do - let(:operations) do - [ - { - op: 'Add', - path: 'externalId', - value: 'new-external-id' - } - ] - end - - it 'does not schedule the worker' do - expect(Authn::SyncScimGroupMembersWorker).not_to receive(:perform_async) - - service.execute - end - - it 'returns success' do - result = service.execute - - expect(result).to be_success - end - end - end - - context 'with remove operation' do - let_it_be(:user) { create(:user) } - let_it_be(:identity) { create(:scim_identity, user: user, group: nil) } - let(:operations) do - [ - { - op: 'Remove', - path: 'members', - value: [{ value: identity.extern_uid }] - } - ] - end - - it 'schedules the worker to remove members' do - expect(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - .with(scim_group_uid, [user.id], 'remove') - - service.execute - end - - it 'returns success' do - allow(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - - result = service.execute - - expect(result).to be_success - end - end - - context 'with multiple operations' do - let_it_be(:user1) { create(:user) } - let_it_be(:user2) { create(:user) } - let_it_be(:identity1) { create(:scim_identity, user: user1, group: nil) } - let_it_be(:identity2) { create(:scim_identity, user: user2, group: nil) } - let(:operations) do - [ - { - op: 'Add', - path: 'members', - value: [{ value: identity1.extern_uid }] - }, - { - op: 'Remove', - path: 'members', - value: [{ value: identity2.extern_uid }] - } - ] - end - - it 'schedules workers for each operation' do - expect(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - .with(scim_group_uid, [user1.id], 'add') - expect(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - .with(scim_group_uid, [user2.id], 'remove') - - service.execute - end - end - - context 'with empty or invalid values' do - let(:operations) do - [ - { - op: 'Add', - path: 'members', - value: [] - } - ] - end - - it 'does not schedule the worker' do - expect(Authn::SyncScimGroupMembersWorker).not_to receive(:perform_async) - - service.execute - end - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/group_sync_provisioning_service_spec.rb b/ee/spec/lib/ee/gitlab/scim/group_sync_provisioning_service_spec.rb deleted file mode 100644 index 9aad0b9192fa7b..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/group_sync_provisioning_service_spec.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::Gitlab::Scim::GroupSyncProvisioningService, feature_category: :system_access do - let(:service) { described_class.new(service_params) } - let(:scim_group_uid) { SecureRandom.uuid } - let(:saml_group_name) { 'engineering' } - let(:service_params) do - { - saml_group_name: saml_group_name, - scim_group_uid: scim_group_uid - } - end - - describe '#execute' do - shared_examples 'success response' do - it 'contains a success status' do - expect(service.execute.status).to eq(:success) - end - - it 'contains a group_link in the response' do - expect(service.execute.group_link).to be_a(SamlGroupLink) - end - end - - context 'when valid params' do - let!(:saml_group_link) { create(:saml_group_link, saml_group_name: saml_group_name) } - - it_behaves_like 'success response' - - it 'updates the SCIM group ID' do - expect { service.execute }.to change { saml_group_link.reload.scim_group_uid }.from(nil).to(scim_group_uid) - end - - context 'with multiple matching group links' do - let!(:another_group_link) { create(:saml_group_link, saml_group_name: saml_group_name) } - - it 'updates all matching group links' do - service.execute - - expect(saml_group_link.reload.scim_group_uid).to eq(scim_group_uid) - expect(another_group_link.reload.scim_group_uid).to eq(scim_group_uid) - end - - it 'returns the first matching group link in the response' do - response = service.execute - - expect(response.group_link).to eq(saml_group_link) - end - end - end - - context 'when invalid params' do - context 'with missing required params' do - shared_examples 'missing param error' do |param| - let(:service_params) { base_params.except(param) } - let(:base_params) do - { - saml_group_name: saml_group_name, - scim_group_uid: scim_group_uid - } - end - - it 'fails with error status' do - expect(service.execute.status).to eq(:error) - end - - it 'includes the missing param in the error message' do - expect(service.execute.message).to eq("Missing params: [:#{param}]") - end - end - - it_behaves_like 'missing param error', :saml_group_name - it_behaves_like 'missing param error', :scim_group_uid - end - - context 'with blank params' do - shared_examples 'blank param error' do |param| - let(:service_params) do - { - saml_group_name: saml_group_name, - scim_group_uid: scim_group_uid - }.merge(param => '') - end - - it 'fails with error status' do - expect(service.execute.status).to eq(:error) - end - - it 'includes the blank param in the error message' do - expect(service.execute.message).to include("Missing params: [:#{param}]") - end - end - - it_behaves_like 'blank param error', :saml_group_name - it_behaves_like 'blank param error', :scim_group_uid - end - - context 'when scim_group_uid is not a valid UUID' do - let(:service_params) do - { - saml_group_name: saml_group_name, - scim_group_uid: 'not-a-valid-uuid' - } - end - - it 'fails with error status' do - expect(service.execute.status).to eq(:error) - end - - it 'includes appropriate error message' do - expect(service.execute.message).to eq('Invalid UUID for scim_group_uid') - end - end - end - - context 'when no matching SAML group exists' do - let(:saml_group_name) { 'nonexistent_group' } - - it 'fails with error status' do - expect(service.execute.status).to eq(:error) - end - - it 'includes appropriate error message' do - expect(service.execute.message).to eq("No matching SAML group found with name: #{saml_group_name}") - end - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/group_sync_put_service_spec.rb b/ee/spec/lib/ee/gitlab/scim/group_sync_put_service_spec.rb deleted file mode 100644 index 190848713ee888..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/group_sync_put_service_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe EE::Gitlab::Scim::GroupSyncPutService, feature_category: :system_access do - let(:scim_group_uid) { SecureRandom.uuid } - let(:service) do - described_class.new( - scim_group_uid: scim_group_uid, - members: members, - display_name: 'Engineering' - ) - end - - describe '#execute' do - context 'with valid members' do - let_it_be(:user) { create(:user) } - let_it_be(:identity) { create(:scim_identity, user: user, group: nil) } - let(:members) do - [ - { value: identity.extern_uid, display: user.name } - ] - end - - it 'schedules the worker to replace members' do - expect(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - .with(scim_group_uid, [user.id], 'replace') - - service.execute - end - - it 'returns success' do - allow(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - - result = service.execute - - expect(result).to be_success - end - end - - context 'with empty members array' do - let(:members) { [] } - - it 'schedules the worker with empty user IDs' do - expect(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - .with(scim_group_uid, [], 'replace') - - service.execute - end - end - - context 'with non-existent user identities' do - let(:members) do - [ - { value: 'non-existent-identity' } - ] - end - - it 'schedules the worker with empty user IDs' do - expect(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - .with(scim_group_uid, [], 'replace') - - service.execute - end - end - - context 'with mixed valid and invalid identities' do - let_it_be(:user) { create(:user) } - let_it_be(:identity) { create(:scim_identity, user: user, group: nil) } - let(:members) do - [ - { value: identity.extern_uid }, - { value: 'non-existent-identity' } - ] - end - - it 'schedules the worker with only valid user IDs' do - expect(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - .with(scim_group_uid, [user.id], 'replace') - - service.execute - end - end - - context 'with nil members' do - let(:members) { nil } - - it 'does not schedule the worker' do - expect(Authn::SyncScimGroupMembersWorker).not_to receive(:perform_async) - - service.execute - end - - it 'returns success' do - result = service.execute - - expect(result).to be_success - end - end - - context 'with blank members' do - let(:members) do - [ - { value: '' }, - { value: nil }, - {} - ] - end - - it 'schedules the worker with empty user IDs after filtering blanks' do - expect(Authn::SyncScimGroupMembersWorker).to receive(:perform_async) - .with(scim_group_uid, [], 'replace') - - service.execute - end - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/params_parser_spec.rb b/ee/spec/lib/ee/gitlab/scim/params_parser_spec.rb deleted file mode 100644 index 90f6345ce3632c..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/params_parser_spec.rb +++ /dev/null @@ -1,155 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe EE::Gitlab::Scim::ParamsParser, feature_category: :system_access do - describe '#filter_params' do - it 'returns the correct filter attributes' do - filter = 'id eq "6ba81b08-77da"' - - expect(described_class.new(filter: filter).filter_params).to eq(extern_uid: '6ba81b08-77da') - end - - it 'returns an empty hash for the wrong filter' do - filter = 'blah eq "6ba81b08-77da"' - - expect(described_class.new(filter: filter).filter_params).to eq({}) - end - end - - describe '#filter_operator' do - it 'returns the operator as a symbol' do - parser = described_class.new(filter: 'id eq 1') - - expect(parser.filter_operator).to eq(:eq) - end - - it 'returns nil if the filter is invalid' do - parser = described_class.new(filter: 'this eq that') - - expect(parser.filter_operator).to eq(nil) - end - end - - describe '#update_params' do - shared_examples 'scim operation active false' do - it 'returns the correct operation attributes' do - expect(described_class.new(Operations: operations).update_params).to eq(active: false) - end - end - - shared_examples 'scim operation empty' do - it 'returns an empty hash for the wrong operations' do - expect(described_class.new(Operations: operations).update_params).to eq({}) - end - end - - shared_examples 'scim operation update name' do - it 'can update name from displayName' do - expect(described_class.new(Operations: operations).update_params).to include(name: 'My Name Is') - end - end - - context 'when path key is present' do - it_behaves_like 'scim operation active false' do - let(:operations) { [{ op: 'replace', path: 'active', value: 'False' }] } - end - - it_behaves_like 'scim operation empty' do - let(:operations) { [{ op: 'replace', path: 'test', value: 'False' }] } - end - - it_behaves_like 'scim operation update name' do - let(:operations) { [{ op: 'replace', path: 'displayName', value: 'My Name Is' }] } - end - end - - context 'when path key is not present' do - it_behaves_like 'scim operation active false' do - let(:operations) { [{ op: 'replace', value: { active: false } }] } - end - - it_behaves_like 'scim operation empty' do - let(:operations) { [{ op: 'replace', value: { test: false } }] } - end - - it_behaves_like 'scim operation update name' do - let(:operations) { [{ op: 'replace', value: { displayName: 'My Name Is' } }] } - end - end - - context 'with capitalized op values for Azure' do - it_behaves_like 'scim operation active false' do - let(:operations) { [{ op: 'Replace', path: 'active', value: 'False' }] } - end - end - end - - describe '#post_params' do - it 'returns a parsed hash for POST params' do - params = { - externalId: 'test', - active: nil, - userName: 'username', - emails: [ - { primary: nil, type: 'work', value: 'work@example.com' }, - { primary: nil, type: 'home', value: 'home@example.com' } - ], - name: { formatted: 'Test A. Name', familyName: 'Name', givenName: 'Test' }, - displayName: 'Test A', - extra: true - } - - expect(described_class.new(params).post_params).to eq(email: 'work@example.com', - extern_uid: 'test', - name: 'Test A. Name', - username: 'username') - end - - it 'can construct a name from givenName and familyName' do - params = { name: { givenName: 'Fred', familyName: 'Nurk' } } - - expect(described_class.new(params).post_params).to include(name: 'Fred Nurk') - end - - it 'falls back to displayName when other names are missing' do - params = { displayName: 'My Name' } - - expect(described_class.new(params).post_params).to include(name: 'My Name') - end - end - - describe '#deprovision_user?' do - it 'returns true when deprovisioning' do - operations = [{ op: 'replace', path: 'active', value: 'False' }] - - expect(described_class.new(Operations: operations).deprovision_user?).to be true - end - - it 'returns false when not deprovisioning' do - operations = [{ op: 'replace', path: 'active', value: 'True' }] - - expect(described_class.new(Operations: operations).deprovision_user?).to be false - end - - it 'returns true when deprovisioning without a path key' do - operations = [{ op: 'replace', value: { active: false } }] - - expect(described_class.new(Operations: operations).deprovision_user?).to be true - end - end - - describe '#reprovision_user?' do - it 'returns true when reprovisioning' do - operations = [{ op: 'replace', path: 'active', value: 'True' }] - - expect(described_class.new(Operations: operations).reprovision_user?).to be true - end - - it 'returns false when not reprovisioning' do - operations = [{ op: 'replace', path: 'active', value: 'False' }] - - expect(described_class.new(Operations: operations).reprovision_user?).to be false - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/reprovisioning_service_spec.rb b/ee/spec/lib/ee/gitlab/scim/reprovisioning_service_spec.rb deleted file mode 100644 index b6ba4ead27b14c..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/reprovisioning_service_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::EE::Gitlab::Scim::ReprovisioningService, feature_category: :system_access do - include LoginHelpers - - describe '#execute' do - let_it_be(:identity) { create(:scim_identity, active: false) } - let_it_be(:user) { identity.user } - let!(:saml_provider) do - stub_basic_saml_config - end - - let(:service) { described_class.new(identity) } - - it 'activates scim identity' do - service.execute - - expect(user).to be_active - end - - it 'activates the user which was in blocked state' do - user.ldap_block - - service.execute - - expect(user.state).to eq('active') - end - - it 'returns the successful reprovisiong message' do - response = service.execute - - expect(response.message).to include("User #{user.name} SCIM identity is reactivated") - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/value_parser_spec.rb b/ee/spec/lib/ee/gitlab/scim/value_parser_spec.rb deleted file mode 100644 index 5814ae078c8bc9..00000000000000 --- a/ee/spec/lib/ee/gitlab/scim/value_parser_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe EE::Gitlab::Scim::ValueParser do - using RSpec::Parameterized::TableSyntax - - describe '#type_cast' do - where(:input, :expected_output) do - 'True' | true - 'true' | true - 'False' | false - 'false' | false - '"Quoted String"' | 'Quoted String' - true | true - false | false - 123 | 123 - end - - with_them do - it do - expect(described_class.new(input).type_cast).to eq expected_output - end - end - end -end diff --git a/ee/spec/lib/ee/gitlab/scim/provisioning_service_spec.rb b/ee/spec/lib/gitlab/scim/provisioning_service_spec.rb similarity index 96% rename from ee/spec/lib/ee/gitlab/scim/provisioning_service_spec.rb rename to ee/spec/lib/gitlab/scim/provisioning_service_spec.rb index 19aacd97d4526d..3e5916bbc068ca 100644 --- a/ee/spec/lib/ee/gitlab/scim/provisioning_service_spec.rb +++ b/ee/spec/lib/gitlab/scim/provisioning_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ::EE::Gitlab::Scim::ProvisioningService, feature_category: :system_access do +RSpec.describe Gitlab::Scim::ProvisioningService, feature_category: :system_access do include LoginHelpers describe '#execute' do @@ -158,7 +158,7 @@ def user end let(:provision_response) do - ::EE::Gitlab::Scim::ProvisioningResponse.new(identity: nil, + Gitlab::Scim::ProvisioningResponse.new(identity: nil, status: :error, message: "Extern uid can't be blank") end @@ -206,7 +206,7 @@ def user end let(:provision_response) do - ::EE::Gitlab::Scim::ProvisioningResponse.new(identity: nil, + Gitlab::Scim::ProvisioningResponse.new(identity: nil, status: :error, message: "Extern uid can't be blank") end diff --git a/ee/spec/requests/api/scim/group_scim_spec.rb b/ee/spec/requests/api/scim/group_scim_spec.rb index fd8063d1591310..25f02cf5c4d1d0 100644 --- a/ee/spec/requests/api/scim/group_scim_spec.rb +++ b/ee/spec/requests/api/scim/group_scim_spec.rb @@ -168,7 +168,7 @@ def scim_api(url, token: true) before do allow_next_instance_of(::EE::Gitlab::Scim::Group::ProvisioningService) do |instance| allow(instance).to receive(:execute).and_return( - ::EE::Gitlab::Scim::ProvisioningResponse.new(status: :error) + Gitlab::Scim::ProvisioningResponse.new(status: :error) ) end diff --git a/ee/spec/requests/api/scim/instance_scim_spec.rb b/ee/spec/requests/api/scim/instance_scim_spec.rb index c889cfda9c14e4..e60ec37c7ce1ac 100644 --- a/ee/spec/requests/api/scim/instance_scim_spec.rb +++ b/ee/spec/requests/api/scim/instance_scim_spec.rb @@ -243,7 +243,7 @@ it 'responds with 201 and the scim user attributes' do create(:user, email: email) - expect(::EE::Gitlab::Scim::ProvisioningService).to receive(:new).with( + expect(::Gitlab::Scim::ProvisioningService).to receive(:new).with( hash_including(organization_id: organization.id) ).and_call_original @@ -257,9 +257,9 @@ context 'when a provisioning error occurs' do before do - allow_next_instance_of(::EE::Gitlab::Scim::ProvisioningService) do |instance| + allow_next_instance_of(::Gitlab::Scim::ProvisioningService) do |instance| allow(instance).to receive(:execute).and_return( - ::EE::Gitlab::Scim::ProvisioningResponse.new(status: :error) + ::Gitlab::Scim::ProvisioningResponse.new(status: :error) ) end end @@ -276,9 +276,9 @@ context 'when a conflict occurs' do before do - allow_next_instance_of(::EE::Gitlab::Scim::ProvisioningService) do |instance| + allow_next_instance_of(::Gitlab::Scim::ProvisioningService) do |instance| allow(instance).to receive(:execute).and_return( - ::EE::Gitlab::Scim::ProvisioningResponse.new(status: :conflict) + ::Gitlab::Scim::ProvisioningResponse.new(status: :conflict) ) end end @@ -390,7 +390,7 @@ end before do - allow_next_instance_of(::EE::Gitlab::Scim::DeprovisioningService) do |instance| + allow_next_instance_of(::Gitlab::Scim::DeprovisioningService) do |instance| allow(instance).to receive(:execute).and_raise(ActiveRecord::RecordInvalid) end end @@ -410,7 +410,7 @@ end before do - allow_next_instance_of(::EE::Gitlab::Scim::ReprovisioningService) do |instance| + allow_next_instance_of(::Gitlab::Scim::ReprovisioningService) do |instance| allow(instance).to receive(:execute).and_raise(ActiveRecord::RecordInvalid) end end @@ -499,7 +499,7 @@ context 'when deprovision fails' do before do - allow_next_instance_of(::EE::Gitlab::Scim::DeprovisioningService) do |instance| + allow_next_instance_of(::Gitlab::Scim::DeprovisioningService) do |instance| allow(instance).to receive(:execute).and_raise(ActiveRecord::RecordInvalid) end end @@ -844,7 +844,7 @@ let(:filter_query) { '?excludedAttributes=members,meta' } it 'passes excluded attributes to the presenter' do - expect(::EE::API::Entities::Scim::Groups).to receive(:represent) + expect(::API::Entities::Scim::Groups).to receive(:represent) .with(anything, hash_including(excluded_attributes: %w[members meta])) .and_call_original @@ -1446,7 +1446,7 @@ end it 'calls the deletion service' do - expect(::EE::Gitlab::Scim::GroupSyncDeletionService).to receive(:new) + expect(::Gitlab::Scim::GroupSyncDeletionService).to receive(:new) .with(scim_group_uid: scim_group_uid) .and_call_original @@ -1469,7 +1469,7 @@ end it 'returns 404 without calling the service' do - expect(::EE::Gitlab::Scim::GroupSyncDeletionService).not_to receive(:new) + expect(::Gitlab::Scim::GroupSyncDeletionService).not_to receive(:new) api_request @@ -1480,7 +1480,7 @@ context 'when service returns error' do before do - allow_next_instance_of(::EE::Gitlab::Scim::GroupSyncDeletionService) do |service| + allow_next_instance_of(::Gitlab::Scim::GroupSyncDeletionService) do |service| allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'Database error')) end end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index b075779e9f392b..c68e14f2acf964 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -750,13 +750,6 @@ - './ee/spec/lib/ee/api/entities/groups/repository_storage_move_spec.rb' - './ee/spec/lib/ee/api/entities/member_spec.rb' - './ee/spec/lib/ee/api/entities/project_spec.rb' -- './ee/spec/lib/ee/api/entities/scim/conflict_spec.rb' -- './ee/spec/lib/ee/api/entities/scim/emails_spec.rb' -- './ee/spec/lib/ee/api/entities/scim/error_spec.rb' -- './ee/spec/lib/ee/api/entities/scim/not_found_spec.rb' -- './ee/spec/lib/ee/api/entities/scim/user_name_spec.rb' -- './ee/spec/lib/ee/api/entities/scim/user_spec.rb' -- './ee/spec/lib/ee/api/entities/scim/users_spec.rb' - './ee/spec/lib/ee/api/entities/user_with_admin_spec.rb' - './ee/spec/lib/ee/api/entities/vulnerability_export_spec.rb' - './ee/spec/lib/ee/api/entities/vulnerability_spec.rb' @@ -838,11 +831,7 @@ - './ee/spec/lib/ee/gitlab/rack_attack/request_spec.rb' - './ee/spec/lib/ee/gitlab/repo_path_spec.rb' - './ee/spec/lib/ee/gitlab/repository_size_checker_spec.rb' -- './ee/spec/lib/ee/gitlab/scim/attribute_transform_spec.rb' -- './ee/spec/lib/ee/gitlab/scim/filter_parser_spec.rb' -- './ee/spec/lib/ee/gitlab/scim/params_parser_spec.rb' - './ee/spec/lib/ee/gitlab/scim/provisioning_service_spec.rb' -- './ee/spec/lib/ee/gitlab/scim/value_parser_spec.rb' - './ee/spec/lib/ee/gitlab/search_results_spec.rb' - './ee/spec/lib/ee/gitlab/security/scan_configuration_spec.rb' - './ee/spec/lib/ee/gitlab/snippet_search_results_spec.rb' -- GitLab