devise_scim 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +124 -0
  3. data/CHANGELOG.md +47 -0
  4. data/CODE_OF_CONDUCT.md +11 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +348 -0
  7. data/Rakefile +21 -0
  8. data/app/controllers/devise_scim/application_controller.rb +69 -0
  9. data/app/controllers/devise_scim/groups_controller.rb +67 -0
  10. data/app/controllers/devise_scim/resource_types_controller.rb +43 -0
  11. data/app/controllers/devise_scim/schemas_controller.rb +55 -0
  12. data/app/controllers/devise_scim/service_provider_controller.rb +34 -0
  13. data/app/controllers/devise_scim/users_controller.rb +281 -0
  14. data/docs/contributing.md +163 -0
  15. data/docs/custom_adapter.md +456 -0
  16. data/docs/idp_setup.md +335 -0
  17. data/docs/multi_tenant.md +328 -0
  18. data/docs/testing.md +444 -0
  19. data/lib/devise_scim/auth/base_strategy.rb +16 -0
  20. data/lib/devise_scim/auth/oauth_strategy.rb +28 -0
  21. data/lib/devise_scim/auth/token_strategy.rb +25 -0
  22. data/lib/devise_scim/concerns/scim_group_identifiable.rb +21 -0
  23. data/lib/devise_scim/concerns/scim_tenant.rb +41 -0
  24. data/lib/devise_scim/configuration.rb +92 -0
  25. data/lib/devise_scim/engine.rb +15 -0
  26. data/lib/devise_scim/filter/arel_visitor.rb +77 -0
  27. data/lib/devise_scim/filter/parser.rb +190 -0
  28. data/lib/devise_scim/middleware/authenticator.rb +51 -0
  29. data/lib/devise_scim/minitest.rb +57 -0
  30. data/lib/devise_scim/models/scim_tenant.rb +14 -0
  31. data/lib/devise_scim/models/scim_tenant_user.rb +15 -0
  32. data/lib/devise_scim/routing.rb +43 -0
  33. data/lib/devise_scim/rspec/factories.rb +17 -0
  34. data/lib/devise_scim/rspec/scim_helpers.rb +43 -0
  35. data/lib/devise_scim/rspec/shared_examples/discovery_endpoints.rb +94 -0
  36. data/lib/devise_scim/rspec/shared_examples/groups_endpoint.rb +148 -0
  37. data/lib/devise_scim/rspec/shared_examples/users_endpoint.rb +301 -0
  38. data/lib/devise_scim/rspec.rb +7 -0
  39. data/lib/devise_scim/scim/error.rb +59 -0
  40. data/lib/devise_scim/scim/group.rb +66 -0
  41. data/lib/devise_scim/scim/list_response.rb +32 -0
  42. data/lib/devise_scim/scim/patch_operation.rb +55 -0
  43. data/lib/devise_scim/scim/user.rb +161 -0
  44. data/lib/devise_scim/scim_adapter.rb +84 -0
  45. data/lib/devise_scim/version.rb +5 -0
  46. data/lib/devise_scim.rb +48 -0
  47. data/lib/generators/devise_scim/adapter_generator.rb +17 -0
  48. data/lib/generators/devise_scim/install_generator.rb +117 -0
  49. data/lib/generators/devise_scim/templates/add_scim_to_tenant.rb.tt +17 -0
  50. data/lib/generators/devise_scim/templates/add_scim_to_users.rb.tt +15 -0
  51. data/lib/generators/devise_scim/templates/application_scim_adapter.rb.tt +34 -0
  52. data/lib/generators/devise_scim/templates/create_scim_tenant_users.rb.tt +22 -0
  53. data/lib/generators/devise_scim/templates/create_scim_tenants.rb.tt +18 -0
  54. data/lib/generators/devise_scim/templates/devise_scim.rb.tt +53 -0
  55. data/sig/devise_scim.rbs +4 -0
  56. metadata +146 -0
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module DeviseScim
6
+ module Scim
7
+ USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
8
+ ENTERPRISE_SCHEMA = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
9
+
10
+ Name = Struct.new(:formatted, :given_name, :family_name, :middle_name,
11
+ :honorific_prefix, :honorific_suffix, keyword_init: true)
12
+ Email = Struct.new(:value, :type, :primary, keyword_init: true)
13
+ PhoneNumber = Struct.new(:value, :type, :primary, keyword_init: true)
14
+ Meta = Struct.new(:resource_type, :created, :last_modified, :version, :location,
15
+ keyword_init: true)
16
+
17
+ # rubocop:disable Metrics/ClassLength
18
+ class User
19
+ SCHEMAS = [USER_SCHEMA].freeze
20
+
21
+ attr_accessor :id, :external_id, :user_name, :display_name, :nick_name,
22
+ :profile_url, :title, :user_type, :preferred_language,
23
+ :locale, :timezone, :active, :name, :emails, :phone_numbers,
24
+ :groups, :meta
25
+
26
+ class << self
27
+ def from_h(hash)
28
+ user = new
29
+ assign_scalars(user, hash)
30
+ assign_name(user, hash)
31
+ user.emails = parse_emails(hash)
32
+ user.phone_numbers = parse_phones(hash)
33
+ user
34
+ end
35
+
36
+ private
37
+
38
+ # rubocop:disable Metrics/AbcSize
39
+ def assign_scalars(user, hash)
40
+ user.id = hash["id"]
41
+ user.external_id = hash["externalId"]
42
+ user.user_name = hash["userName"]
43
+ user.display_name = hash["displayName"]
44
+ user.nick_name = hash["nickName"]
45
+ user.profile_url = hash["profileUrl"]
46
+ user.title = hash["title"]
47
+ user.user_type = hash["userType"]
48
+ user.preferred_language = hash["preferredLanguage"]
49
+ user.locale = hash["locale"]
50
+ user.timezone = hash["timezone"]
51
+ user.active = hash.key?("active") ? hash["active"] : true
52
+ end
53
+ # rubocop:enable Metrics/AbcSize
54
+
55
+ def assign_name(user, hash)
56
+ return unless (n = hash["name"])
57
+
58
+ user.name = Name.new(
59
+ formatted: n["formatted"],
60
+ given_name: n["givenName"],
61
+ family_name: n["familyName"],
62
+ middle_name: n["middleName"],
63
+ honorific_prefix: n["honorificPrefix"],
64
+ honorific_suffix: n["honorificSuffix"]
65
+ )
66
+ end
67
+
68
+ def parse_emails(hash)
69
+ Array(hash["emails"]).map do |entry|
70
+ Email.new(value: entry["value"], type: entry["type"], primary: entry["primary"])
71
+ end
72
+ end
73
+
74
+ def parse_phones(hash)
75
+ Array(hash["phoneNumbers"]).map do |entry|
76
+ PhoneNumber.new(value: entry["value"], type: entry["type"], primary: entry["primary"])
77
+ end
78
+ end
79
+ end
80
+
81
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
82
+ def to_h
83
+ h = base_hash.compact
84
+ h["schemas"] = SCHEMAS
85
+ h["name"] = serialize_name if name
86
+ h["emails"] = serialize_emails if emails&.any?
87
+ h["phoneNumbers"] = serialize_phones if phone_numbers&.any?
88
+ h["groups"] = groups if groups&.any?
89
+ h["meta"] = serialize_meta(meta, "User") if meta
90
+ h
91
+ end
92
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
93
+
94
+ def to_json(*)
95
+ to_h.to_json
96
+ end
97
+
98
+ def primary_email
99
+ emails&.find(&:primary)&.value || user_name
100
+ end
101
+
102
+ private
103
+
104
+ def base_hash
105
+ {
106
+ "schemas" => SCHEMAS,
107
+ "id" => id,
108
+ "externalId" => external_id,
109
+ "userName" => user_name,
110
+ "displayName" => display_name,
111
+ "nickName" => nick_name,
112
+ "profileUrl" => profile_url,
113
+ "title" => title,
114
+ "userType" => user_type,
115
+ "preferredLanguage" => preferred_language,
116
+ "locale" => locale,
117
+ "timezone" => timezone,
118
+ "active" => active
119
+ }
120
+ end
121
+
122
+ def serialize_name
123
+ {
124
+ "formatted" => name.formatted,
125
+ "givenName" => name.given_name,
126
+ "familyName" => name.family_name,
127
+ "middleName" => name.middle_name,
128
+ "honorificPrefix" => name.honorific_prefix,
129
+ "honorificSuffix" => name.honorific_suffix
130
+ }.compact
131
+ end
132
+
133
+ def serialize_emails
134
+ emails.map do |email|
135
+ { "value" => email.value, "type" => email.type, "primary" => email.primary }.compact
136
+ end
137
+ end
138
+
139
+ def serialize_phones
140
+ phone_numbers.map do |phone|
141
+ { "value" => phone.value, "type" => phone.type, "primary" => phone.primary }.compact
142
+ end
143
+ end
144
+
145
+ def serialize_meta(met, resource_type)
146
+ {
147
+ "resourceType" => met.resource_type || resource_type,
148
+ "created" => iso8601_or_raw(met.created),
149
+ "lastModified" => iso8601_or_raw(met.last_modified),
150
+ "version" => met.version,
151
+ "location" => met.location
152
+ }.compact
153
+ end
154
+
155
+ def iso8601_or_raw(value)
156
+ value.respond_to?(:iso8601) ? value.iso8601 : value
157
+ end
158
+ end
159
+ # rubocop:enable Metrics/ClassLength
160
+ end
161
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseScim
4
+ class ScimAdapter
5
+ attr_reader :record, :scim_user, :scim_group, :tenant
6
+
7
+ def initialize(record, scim_object, tenant: nil)
8
+ @record = record
9
+ @scim_user = scim_object if scim_object.is_a?(Scim::User)
10
+ @scim_group = scim_object if scim_object.is_a?(Scim::Group)
11
+ @tenant = tenant
12
+ end
13
+
14
+ def attributes_for_create
15
+ base_user_attributes
16
+ end
17
+
18
+ def attributes_for_update
19
+ base_user_attributes
20
+ end
21
+
22
+ def after_provision; end
23
+ def after_deprovision; end
24
+
25
+ def handle_group_create; end
26
+ def handle_group_update; end
27
+ def handle_group_destroy; end
28
+
29
+ def to_scim
30
+ scim = Scim::User.new
31
+ scim.id = record.id.to_s
32
+ scim.user_name = record.email
33
+ scim.active = resolve_active
34
+ scim.emails = [Scim::Email.new(value: record.email, type: "work", primary: true)]
35
+ scim.name = build_name
36
+ scim.meta = build_meta("User")
37
+ scim
38
+ end
39
+
40
+ def group_to_scim
41
+ raise NotImplementedError, "#{self.class}#group_to_scim must be implemented when enable_groups is true"
42
+ end
43
+
44
+ private
45
+
46
+ def base_user_attributes
47
+ attrs = { email: scim_user.user_name || scim_user.primary_email }
48
+ attrs[:first_name] = scim_user.name&.given_name if column?(:first_name)
49
+ attrs[:last_name] = scim_user.name&.family_name if column?(:last_name)
50
+ attrs
51
+ end
52
+
53
+ def column?(name)
54
+ record.class.respond_to?(:column_names) &&
55
+ record.class.column_names.include?(name.to_s)
56
+ end
57
+
58
+ def resolve_active
59
+ if column?(:scim_active)
60
+ record.scim_active
61
+ elsif column?(:deleted_at)
62
+ record.deleted_at.nil?
63
+ else
64
+ true
65
+ end
66
+ end
67
+
68
+ def build_name
69
+ given = column?(:first_name) ? record.first_name : nil
70
+ family = column?(:last_name) ? record.last_name : nil
71
+ return nil unless given || family
72
+
73
+ Scim::Name.new(given_name: given, family_name: family)
74
+ end
75
+
76
+ def build_meta(resource_type)
77
+ Scim::Meta.new(
78
+ resource_type: resource_type,
79
+ created: record.created_at,
80
+ last_modified: record.updated_at
81
+ )
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseScim
4
+ VERSION = "0.1.11"
5
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "devise_scim/version"
4
+ require_relative "devise_scim/configuration"
5
+
6
+ module DeviseScim
7
+ class Error < StandardError; end
8
+ class ConfigurationError < Error; end
9
+ class NotFound < Error; end
10
+ class Conflict < Error; end
11
+ class InvalidFilter < Error; end
12
+ class Unauthorized < Error; end
13
+
14
+ class << self
15
+ def configure
16
+ yield configuration
17
+ end
18
+
19
+ def configuration
20
+ @configuration ||= Configuration.new
21
+ end
22
+
23
+ def reset_configuration!
24
+ @configuration = Configuration.new
25
+ end
26
+ end
27
+ end
28
+
29
+ if defined?(Rails)
30
+ require_relative "devise_scim/concerns/scim_tenant"
31
+ require_relative "devise_scim/models/scim_tenant"
32
+ require_relative "devise_scim/models/scim_tenant_user"
33
+ require_relative "devise_scim/scim/user"
34
+ require_relative "devise_scim/scim/group"
35
+ require_relative "devise_scim/scim/list_response"
36
+ require_relative "devise_scim/scim/error"
37
+ require_relative "devise_scim/scim/patch_operation"
38
+ require_relative "devise_scim/scim_adapter"
39
+ require_relative "devise_scim/concerns/scim_group_identifiable"
40
+ require_relative "devise_scim/filter/parser"
41
+ require_relative "devise_scim/filter/arel_visitor"
42
+ require_relative "devise_scim/auth/base_strategy"
43
+ require_relative "devise_scim/auth/token_strategy"
44
+ require_relative "devise_scim/auth/oauth_strategy"
45
+ require_relative "devise_scim/middleware/authenticator"
46
+ require_relative "devise_scim/routing"
47
+ require_relative "devise_scim/engine"
48
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module DeviseScim
6
+ module Generators
7
+ class AdapterGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Creates a pre-filled ScimAdapter in app/scim/application_scim_adapter.rb"
11
+
12
+ def copy_adapter
13
+ template "application_scim_adapter.rb.tt", "app/scim/application_scim_adapter.rb"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module DeviseScim
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ argument :model_name, type: :string, default: "User",
14
+ desc: "Devise model to add SCIM fields to (e.g. User)"
15
+
16
+ class_option :multi_tenant, type: :boolean, default: false,
17
+ desc: "Generate multi-tenant migrations"
18
+ class_option :oauth, type: :boolean, default: false,
19
+ desc: "Configure OAuth 2.0 client-credentials auth"
20
+ class_option :tenant_model, type: :string, default: nil,
21
+ desc: "Existing model to use as the SCIM tenant (e.g. Org). " \
22
+ "Omit to use the built-in DeviseScim::ScimTenant."
23
+
24
+ def self.next_migration_number(dirname)
25
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
26
+ end
27
+
28
+ def preflight_check
29
+ return unless needs_doorkeeper?
30
+
31
+ unless doorkeeper_in_gemfile?
32
+ say_status :error, "Doorkeeper gem not found in Gemfile.", :red
33
+ say " Add `gem 'doorkeeper', '~> 5.6'` to your Gemfile and run `bundle install`.", :red
34
+ raise Thor::Error, "Aborting: Doorkeeper required for #{doorkeeper_reason}."
35
+ end
36
+
37
+ return if doorkeeper_installed?
38
+
39
+ unless yes?("Doorkeeper not yet installed. Run `rails g doorkeeper:install` now?")
40
+ raise Thor::Error, "Aborting. Run `rails g doorkeeper:install` before proceeding."
41
+ end
42
+
43
+ generate "doorkeeper:install"
44
+ end
45
+
46
+ def copy_user_migration
47
+ migration_template(
48
+ "add_scim_to_users.rb.tt",
49
+ "db/migrate/add_scim_to_#{table_name}.rb"
50
+ )
51
+ end
52
+
53
+ def copy_tenant_migrations
54
+ return unless options[:multi_tenant]
55
+
56
+ if options[:tenant_model]
57
+ migration_template "add_scim_to_tenant.rb.tt",
58
+ "db/migrate/add_scim_to_#{tenant_table_name}.rb"
59
+ else
60
+ migration_template "create_scim_tenants.rb.tt", "db/migrate/create_scim_tenants.rb"
61
+ end
62
+ migration_template "create_scim_tenant_users.rb.tt", "db/migrate/create_scim_tenant_users.rb"
63
+ end
64
+
65
+ def copy_initializer
66
+ template "devise_scim.rb.tt", "config/initializers/devise_scim.rb"
67
+ end
68
+
69
+ private
70
+
71
+ def needs_doorkeeper?
72
+ options[:oauth] || options[:multi_tenant]
73
+ end
74
+
75
+ def doorkeeper_reason
76
+ options[:multi_tenant] ? "--multi-tenant mode" : "--oauth auth"
77
+ end
78
+
79
+ def doorkeeper_in_gemfile?
80
+ gemfile = File.join(destination_root, "Gemfile")
81
+ File.exist?(gemfile) && File.read(gemfile).include?("doorkeeper")
82
+ end
83
+
84
+ def doorkeeper_installed?
85
+ Dir[File.join(destination_root, "db/migrate/*doorkeeper*")].any? ||
86
+ Dir[File.join(destination_root, "db/migrate/*create_doorkeeper_tables*")].any?
87
+ end
88
+
89
+ def table_name
90
+ model_name.underscore.pluralize
91
+ end
92
+
93
+ def tenant_table_name
94
+ options[:tenant_model] ? options[:tenant_model].underscore.pluralize : "scim_tenants"
95
+ end
96
+
97
+ def tenant_ref_name
98
+ options[:tenant_model] ? options[:tenant_model].underscore : "scim_tenant"
99
+ end
100
+
101
+ def tenant_fk_column
102
+ options[:tenant_model] ? "#{options[:tenant_model].underscore}_id" : "scim_tenant_id"
103
+ end
104
+
105
+ def migration_version
106
+ "[#{ActiveRecord::Migration.current_version}]"
107
+ end
108
+
109
+ def scim_raw_type
110
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
111
+ adapter.include?("postgresql") ? "jsonb" : "text"
112
+ rescue StandardError
113
+ "text"
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddScimTo<%= options[:tenant_model].camelize.pluralize %> < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ unless column_exists?(:<%= tenant_table_name %>, :token_digest)
6
+ add_column :<%= tenant_table_name %>, :token_digest, :string
7
+ end
8
+ unless column_exists?(:<%= tenant_table_name %>, :auth_method)
9
+ add_column :<%= tenant_table_name %>, :auth_method, :string, null: false, default: "token"
10
+ end
11
+ unless column_exists?(:<%= tenant_table_name %>, :doorkeeper_application_id)
12
+ add_column :<%= tenant_table_name %>, :doorkeeper_application_id, :bigint
13
+ add_foreign_key :<%= tenant_table_name %>, :oauth_applications,
14
+ column: :doorkeeper_application_id, on_delete: :nullify
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddScimTo<%= model_name.camelize.pluralize %> < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ <%- unless options[:multi_tenant] -%>
6
+ add_column :<%= table_name %>, :scim_uid, :string
7
+ add_index :<%= table_name %>, :scim_uid, unique: true
8
+ <%- end -%>
9
+ add_column :<%= table_name %>, :scim_provisioned, :boolean, default: false, null: false
10
+ add_column :<%= table_name %>, :scim_active, :boolean, default: true, null: false
11
+ add_column :<%= table_name %>, :scim_source, :string
12
+ add_column :<%= table_name %>, :scim_deprovisioned_at, :datetime
13
+ add_column :<%= table_name %>, :scim_raw, :<%= scim_raw_type %>
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationScimAdapter < DeviseScim::ScimAdapter
4
+ # Override to customize attributes assigned on create.
5
+ # Must return a Hash of AR attribute names to values.
6
+ #
7
+ # def attributes_for_create
8
+ # super.merge(role: "user")
9
+ # end
10
+
11
+ # Override to customize attributes assigned on update.
12
+ #
13
+ # def attributes_for_update
14
+ # super
15
+ # end
16
+
17
+ # Lifecycle hooks — called after provision/deprovision completes.
18
+ # def after_provision; end
19
+ # def after_deprovision; end
20
+
21
+ # Implement to handle SCIM Group operations.
22
+ # def handle_group_create; end
23
+ # def handle_group_update; end
24
+ # def handle_group_destroy; end
25
+
26
+ # Required when enable_groups is true. Serialize a group AR record → Scim::Group.
27
+ #
28
+ # def group_to_scim
29
+ # DeviseScim::Scim::Group.new.tap do |g|
30
+ # g.id = record.id.to_s
31
+ # g.display_name = record.name
32
+ # end
33
+ # end
34
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateScimTenantUsers < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :scim_tenant_users do |t|
6
+ t.references :<%= tenant_ref_name %>, null: false,
7
+ foreign_key: { to_table: :<%= tenant_table_name %> }
8
+ t.bigint :user_id, null: false
9
+ t.string :scim_uid
10
+ t.datetime :provisioned_at
11
+ t.datetime :scim_claimed_at
12
+ t.boolean :active, null: false, default: true
13
+ t.<%= scim_raw_type %> :scim_raw
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :scim_tenant_users, [:<%= tenant_fk_column %>, :user_id], unique: true
18
+ add_index :scim_tenant_users, [:<%= tenant_fk_column %>, :scim_uid], unique: true,
19
+ where: "scim_uid IS NOT NULL"
20
+ add_foreign_key :scim_tenant_users, :<%= table_name %>, column: :user_id
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateScimTenants < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :scim_tenants do |t|
6
+ t.string :name, null: false
7
+ t.string :auth_method, null: false, default: "token"
8
+ t.string :token_digest
9
+ t.bigint :doorkeeper_application_id
10
+ t.boolean :active, null: false, default: true
11
+ t.timestamps
12
+ end
13
+
14
+ add_foreign_key :scim_tenants, :oauth_applications,
15
+ column: :doorkeeper_application_id,
16
+ on_delete: :nullify
17
+ end
18
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ DeviseScim.configure do |config|
4
+ # Mount path for all SCIM 2.0 endpoints.
5
+ config.route_prefix = "/scim/v2"
6
+
7
+ # Tenancy mode. :single = one IdP; :multi = per-tenant credentials and scoping.
8
+ config.tenancy = :<%= options[:multi_tenant] ? "multi" : "single" %>
9
+
10
+ # Devise model that represents provisioned users.
11
+ config.devise_model = "<%= model_name %>"
12
+
13
+ # Expose /Groups endpoints. Implement handle_group_* in your ScimAdapter when true.
14
+ config.enable_groups = false
15
+
16
+ # true = destroy record on DELETE; false = set scim_active=false only.
17
+ config.soft_delete = true
18
+
19
+ # Behaviour when a DELETE targets a user not originally provisioned via SCIM.
20
+ # false = 200 OK, no action (default); true = deprovision anyway; :error = 409 Conflict.
21
+ config.deprovision_manual_users = false
22
+
23
+ # Adapter class that maps SCIM attributes to your model.
24
+ # Run `rails g devise_scim:adapter` to generate a pre-filled starting point.
25
+ config.adapter = nil # replace with ApplicationScimAdapter after running the adapter generator
26
+
27
+ # ── Single-tenant auth ────────────────────────────────────────────────────────
28
+ # Ignored in multi-tenant mode (credentials live on each ScimTenant record).
29
+
30
+ # :token = Authorization: Bearer <token>; :oauth = Doorkeeper client credentials.
31
+ config.auth_method = :token
32
+
33
+ # Bearer token (plain-text). Store in an env var — never commit to source control.
34
+ config.token = ENV.fetch("SCIM_BEARER_TOKEN", nil)
35
+
36
+ # OAuth 2.0 client credentials. Requires the doorkeeper gem.
37
+ config.oauth_client_id = ENV.fetch("SCIM_CLIENT_ID", nil)
38
+ config.oauth_client_secret = ENV.fetch("SCIM_CLIENT_SECRET", nil)
39
+
40
+ # ── Multi-tenant options ──────────────────────────────────────────────────────
41
+ # Ignored in single-tenant mode.
42
+ <% if options[:tenant_model] %>
43
+ # AR model used as the SCIM tenant. Defaults to DeviseScim::ScimTenant.
44
+ config.tenant_model = "<%= options[:tenant_model] %>"
45
+ <% end %>
46
+ # :multiple = a user may belong to many tenants (default).
47
+ # :one_to_one = a user may belong to at most one tenant.
48
+ config.user_exclusivity = :multiple
49
+
50
+ # Behaviour when :one_to_one and a POST /Users matches a user in another tenant.
51
+ # :error = 409 Conflict (default); :reassign = move user to new tenant.
52
+ config.exclusivity_conflict = :error
53
+ end
@@ -0,0 +1,4 @@
1
+ module DeviseScim
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end