scim_engine 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +25 -0
  4. data/app/controllers/scim_engine/application_controller.rb +36 -0
  5. data/app/controllers/scim_engine/resource_types_controller.rb +22 -0
  6. data/app/controllers/scim_engine/resources_controller.rb +71 -0
  7. data/app/controllers/scim_engine/schemas_controller.rb +16 -0
  8. data/app/controllers/scim_engine/service_provider_configurations_controller.rb +8 -0
  9. data/app/models/scim_engine/authentication_error.rb +9 -0
  10. data/app/models/scim_engine/authentication_scheme.rb +12 -0
  11. data/app/models/scim_engine/bulk.rb +5 -0
  12. data/app/models/scim_engine/complex_types/base.rb +19 -0
  13. data/app/models/scim_engine/complex_types/email.rb +11 -0
  14. data/app/models/scim_engine/complex_types/name.rb +7 -0
  15. data/app/models/scim_engine/complex_types/reference.rb +7 -0
  16. data/app/models/scim_engine/error_response.rb +13 -0
  17. data/app/models/scim_engine/errors.rb +14 -0
  18. data/app/models/scim_engine/filter.rb +5 -0
  19. data/app/models/scim_engine/meta.rb +7 -0
  20. data/app/models/scim_engine/not_found_error.rb +10 -0
  21. data/app/models/scim_engine/resource_invalid_error.rb +9 -0
  22. data/app/models/scim_engine/resource_type.rb +28 -0
  23. data/app/models/scim_engine/resources/base.rb +110 -0
  24. data/app/models/scim_engine/resources/group.rb +13 -0
  25. data/app/models/scim_engine/resources/user.rb +13 -0
  26. data/app/models/scim_engine/schema/attribute.rb +82 -0
  27. data/app/models/scim_engine/schema/base.rb +30 -0
  28. data/app/models/scim_engine/schema/derived_attributes.rb +24 -0
  29. data/app/models/scim_engine/schema/email.rb +13 -0
  30. data/app/models/scim_engine/schema/group.rb +25 -0
  31. data/app/models/scim_engine/schema/name.rb +15 -0
  32. data/app/models/scim_engine/schema/reference.rb +12 -0
  33. data/app/models/scim_engine/schema/user.rb +28 -0
  34. data/app/models/scim_engine/service_provider_configuration.rb +30 -0
  35. data/app/models/scim_engine/supportable.rb +10 -0
  36. data/app/views/layouts/scim_engine/application.html.erb +14 -0
  37. data/config/routes.rb +6 -0
  38. data/lib/scim_engine/engine.rb +35 -0
  39. data/lib/scim_engine/version.rb +3 -0
  40. data/lib/scim_engine.rb +13 -0
  41. data/lib/tasks/scim_engine_tasks.rake +4 -0
  42. data/spec/controllers/scim_engine/application_controller_spec.rb +104 -0
  43. data/spec/controllers/scim_engine/resource_types_controller_spec.rb +78 -0
  44. data/spec/controllers/scim_engine/resources_controller_spec.rb +134 -0
  45. data/spec/controllers/scim_engine/schemas_controller_spec.rb +66 -0
  46. data/spec/controllers/scim_engine/service_provider_configurations_controller_spec.rb +21 -0
  47. data/spec/dummy/README.rdoc +28 -0
  48. data/spec/dummy/Rakefile +6 -0
  49. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  50. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  51. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  52. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  53. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  54. data/spec/dummy/bin/bundle +3 -0
  55. data/spec/dummy/bin/rails +4 -0
  56. data/spec/dummy/bin/rake +4 -0
  57. data/spec/dummy/bin/setup +29 -0
  58. data/spec/dummy/config/application.rb +16 -0
  59. data/spec/dummy/config/boot.rb +5 -0
  60. data/spec/dummy/config/environment.rb +5 -0
  61. data/spec/dummy/config/environments/development.rb +41 -0
  62. data/spec/dummy/config/environments/production.rb +79 -0
  63. data/spec/dummy/config/environments/test.rb +42 -0
  64. data/spec/dummy/config/initializers/assets.rb +11 -0
  65. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  66. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  67. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  68. data/spec/dummy/config/initializers/inflections.rb +16 -0
  69. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  70. data/spec/dummy/config/initializers/session_store.rb +3 -0
  71. data/spec/dummy/config/initializers/to_time_preserves_timezone.rb +10 -0
  72. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  73. data/spec/dummy/config/locales/en.yml +23 -0
  74. data/spec/dummy/config/routes.rb +4 -0
  75. data/spec/dummy/config/secrets.yml +22 -0
  76. data/spec/dummy/config.ru +4 -0
  77. data/spec/dummy/log/test.log +863 -0
  78. data/spec/dummy/public/404.html +67 -0
  79. data/spec/dummy/public/422.html +67 -0
  80. data/spec/dummy/public/500.html +66 -0
  81. data/spec/dummy/public/favicon.ico +0 -0
  82. data/spec/models/scim_engine/complex_types/email_spec.rb +23 -0
  83. data/spec/models/scim_engine/resource_type_spec.rb +21 -0
  84. data/spec/models/scim_engine/resources/base_spec.rb +246 -0
  85. data/spec/models/scim_engine/resources/base_validation_spec.rb +61 -0
  86. data/spec/models/scim_engine/resources/user_spec.rb +55 -0
  87. data/spec/models/scim_engine/schema/attribute_spec.rb +80 -0
  88. data/spec/models/scim_engine/schema/base_spec.rb +19 -0
  89. data/spec/models/scim_engine/schema/group_spec.rb +66 -0
  90. data/spec/models/scim_engine/schema/user_spec.rb +140 -0
  91. data/spec/rails_helper.rb +57 -0
  92. data/spec/spec_helper.rb +99 -0
  93. metadata +246 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 84aac5eb3121ba10036412a4c5d8ae959cf8fc99
4
+ data.tar.gz: f9c50a53333b47f4e6961dfa2a9fe256d2f207c7
5
+ SHA512:
6
+ metadata.gz: 0bbf31701459d913599fdd83dd90b0120e19fc3c1cbff4133070293d93191044fb82fd2b2df79257544ee24618704d90946ebfea69ea632f243e30af0965584c
7
+ data.tar.gz: a53d3895e691a9aaa7ae5b4adfd7755d430c129b36b820c91a48568b03548a84e1b4673ab207dfae95b78ea03fd15c731369dda4a2c9bcc95c181e881cde6bdd
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ScimEngine'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,36 @@
1
+ module ScimEngine
2
+ class ApplicationController < ActionController::Base
3
+ before_action :require_scim
4
+ before_action :authenticate
5
+
6
+ protected
7
+
8
+ def authenticated?
9
+ authenticate_with_http_basic do |name, password|
10
+ name == ScimEngine::Engine.username && password == ScimEngine::Engine.password
11
+ end
12
+ end
13
+
14
+ def authenticate
15
+ handle_scim_error(ScimEngine::AuthenticationError.new) unless authenticated?
16
+ end
17
+
18
+ def handle_resource_not_found(exception)
19
+ handle_scim_error(NotFoundError.new(params[:id]))
20
+ end
21
+
22
+ def handle_record_invalid(error_message)
23
+ handle_scim_error(ErrorResponse.new(status: 400, detail: "Operation failed since record has become invalid: #{error_message}"))
24
+ end
25
+
26
+ def handle_scim_error(error_response)
27
+ render json: error_response, status: error_response.status
28
+ end
29
+
30
+ def require_scim
31
+ unless request.format == :scim
32
+ handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{Mime::Type.lookup_by_extension(:scim)} type is accepted."))
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,22 @@
1
+ require_dependency "scim_engine/application_controller"
2
+
3
+ module ScimEngine
4
+ class ResourceTypesController < ApplicationController
5
+ def index
6
+ resource_types = ScimEngine::Engine.resources.map do |resource|
7
+ resource.resource_type(scim_resource_type_url(name: resource.resource_type_id))
8
+ end
9
+
10
+ render json: resource_types
11
+ end
12
+
13
+ def show
14
+ resource_types = ScimEngine::Engine.resources.reduce({}) do |hash, resource|
15
+ hash[resource.resource_type_id] = resource.resource_type(scim_resource_type_url(name: resource.resource_type_id))
16
+ hash
17
+ end
18
+
19
+ render json: resource_types[params[:name]]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,71 @@
1
+ require_dependency "scim_engine/application_controller"
2
+
3
+ module ScimEngine
4
+ class ResourcesController < ApplicationController
5
+
6
+ def show(&block)
7
+ scim_user = yield resource_params[:id]
8
+ render json: scim_user
9
+ rescue ErrorResponse => error
10
+ handle_scim_error(error)
11
+ end
12
+
13
+ def create(resource_type, &block)
14
+ if resource_params[:id].present? || resource_params[:externalId].present?
15
+ handle_scim_error(ErrorResponse.new(status: 400, detail: 'id and externalId are not valid parameters for create'))
16
+ return
17
+ end
18
+ with_scim_resource(resource_type) do |resource|
19
+ render json: yield(resource, is_create: true), status: :created
20
+ end
21
+ end
22
+
23
+ def update(resource_type, &block)
24
+ with_scim_resource(resource_type) do |resource|
25
+ render json: yield(resource)
26
+ end
27
+ end
28
+
29
+ def destroy
30
+ if yield(resource_params[:id]) != false
31
+ head :no_content
32
+ else
33
+ handle_scim_error(ErrorResponse.new(status: 500, detail: "Failed to delete the resource with id '#{params[:id]}'. Please try again later"))
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def validate_request
40
+ if request.raw_post.blank?
41
+ raise ScimEngine::ErrorResponse.new(status: 400, detail: 'must provide a request body')
42
+ end
43
+ end
44
+
45
+ def with_scim_resource(resource_type)
46
+ validate_request
47
+ begin
48
+ resource = resource_type.new(resource_params.to_h)
49
+ unless resource.valid?
50
+ raise ScimEngine::ErrorResponse.new(status: 400,
51
+ detail: "Invalid resource: #{resource.errors.full_messages.join(', ')}.",
52
+ scimType: 'invalidValue')
53
+ end
54
+
55
+ yield(resource)
56
+ rescue NoMethodError => error
57
+ Rails.logger.error error
58
+ raise ScimEngine::ErrorResponse.new(status: 400, detail: 'invalid request')
59
+ end
60
+ rescue ScimEngine::ErrorResponse => error
61
+ handle_scim_error(error)
62
+ end
63
+
64
+ private
65
+
66
+ def resource_params
67
+ params.permit!
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,16 @@
1
+ require_dependency "scim_engine/application_controller"
2
+
3
+ module ScimEngine
4
+ class SchemasController < ApplicationController
5
+ def index
6
+ schemas = ScimEngine::Engine.schemas
7
+ schemas_by_id = schemas.reduce({}) do |hash, schema|
8
+ hash[schema.id] = schema
9
+ hash
10
+ end
11
+
12
+ render json: schemas_by_id[params[:name]] || schemas
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ require_dependency "scim_engine/application_controller"
2
+ module ScimEngine
3
+ class ServiceProviderConfigurationsController < ApplicationController
4
+ def show
5
+ render json: ScimEngine.service_provider_configuration(location: request.url)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ module ScimEngine
2
+
3
+ class AuthenticationError < ErrorResponse
4
+ def initialize
5
+ super(status: 401, detail: 'Requires authentication')
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module ScimEngine
2
+ class AuthenticationScheme
3
+ include ActiveModel::Model
4
+ attr_accessor :type, :name, :description
5
+
6
+ def self.basic
7
+ new type: 'httpbasic',
8
+ name: 'HTTP Basic',
9
+ description: 'Authentication scheme using the HTTP Basic Standard'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module ScimEngine
2
+ class Bulk < Supportable
3
+ attr_accessor :maxOperations, :maxPayloadSize
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ module ScimEngine
2
+ module ComplexTypes
3
+ class Base
4
+ include ActiveModel::Model
5
+ include ScimEngine::Schema::DerivedAttributes
6
+ include ScimEngine::Errors
7
+
8
+ def initialize(options={})
9
+ super
10
+ @errors = ActiveModel::Errors.new(self)
11
+ end
12
+
13
+ def as_json(options={})
14
+ options[:except] ||= ['errors']
15
+ super.except(options)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ module ScimEngine
2
+ module ComplexTypes
3
+ class Email < Base
4
+ set_schema ScimEngine::Schema::Email
5
+
6
+ def as_json(options = {})
7
+ {'type' => 'work', 'primary' => true}.merge(super(options))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module ScimEngine
2
+ module ComplexTypes
3
+ class Name < Base
4
+ set_schema ScimEngine::Schema::Name
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module ScimEngine
2
+ module ComplexTypes
3
+ class Reference < Base
4
+ set_schema ScimEngine::Schema::Reference
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module ScimEngine
2
+ class ErrorResponse < StandardError
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor :status, :detail, :scimType
6
+
7
+ def as_json(options = {})
8
+ {'schemas': ['urn:ietf:params:scim:api:messages:2.0:Error'],
9
+ 'detail': detail,
10
+ 'status': "#{status}"}.merge(scimType ? {'scimType': scimType} : {})
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module ScimEngine
2
+ module Errors
3
+ def add_errors_from_hash(errors_hash, prefix: nil)
4
+ errors_hash.each_pair do |key, value|
5
+ new_key = prefix.nil? ? key : "#{prefix}.#{key}".to_sym
6
+ if value.is_a?(Array)
7
+ value.each {|error| errors.add(new_key, error)}
8
+ else
9
+ errors.add(new_key, value)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module ScimEngine
2
+ class Filter < Supportable
3
+ attr_accessor :maxResults
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module ScimEngine
2
+ class Meta
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor :resourceType, :created, :lastModified, :location, :version
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ module ScimEngine
2
+
3
+ class NotFoundError < ErrorResponse
4
+
5
+ def initialize(id)
6
+ super(status: 404, detail: "Resource #{id} not found")
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module ScimEngine
2
+ class ResourceInvalidError < ErrorResponse
3
+
4
+ def initialize(error_message)
5
+ super(status: 400, scimType: 'invalidValue', detail:"Operation failed since record has become invalid: #{error_message}")
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,28 @@
1
+ module ScimEngine
2
+ class ResourceType
3
+ include ActiveModel::Model
4
+ attr_accessor :meta, :endpoint, :schema, :schemas, :id, :name, :schemaExtensions
5
+
6
+ def initialize(attributes = {})
7
+ default_attributes = {
8
+ meta: Meta.new(
9
+ 'resourceType': 'ResourceType'
10
+ ),
11
+ schemas: ['urn:ietf:params:scim:schemas:core:2.0:ResourceType']
12
+ }
13
+ super(default_attributes.merge(attributes))
14
+ end
15
+
16
+
17
+ def as_json(options = {})
18
+ without_extensions = super(except: 'schemaExtensions')
19
+ if schemaExtensions.present?
20
+ extensions = schemaExtensions.map{|extension| {"schema" => extension, "required" => false}}
21
+ without_extensions.merge('schemaExtensions' => extensions)
22
+ else
23
+ without_extensions
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,110 @@
1
+ module ScimEngine
2
+ module Resources
3
+ class Base
4
+ include ActiveModel::Model
5
+ include ScimEngine::Schema::DerivedAttributes
6
+ include ScimEngine::Errors
7
+
8
+ attr_accessor :id, :externalId, :meta
9
+ attr_reader :errors
10
+ validate :validate_resource
11
+
12
+ def initialize(options = {})
13
+ flattended_attributes = flatten_extension_attributes(options)
14
+ attributes = flattended_attributes.with_indifferent_access.slice(*self.class.all_attributes)
15
+ super(attributes)
16
+ constantize_complex_types(attributes)
17
+ @errors = ActiveModel::Errors.new(self)
18
+ end
19
+
20
+ def flatten_extension_attributes(options)
21
+ flattened = options.dup
22
+ self.class.extended_schemas.each do |extended_schema|
23
+ if extension_attrs = flattened.delete(extended_schema.id)
24
+ flattened.merge!(extension_attrs)
25
+ end
26
+ end
27
+ flattened
28
+ end
29
+
30
+ def self.extend_schema(schema)
31
+ derive_attributes_from_schema(schema)
32
+ extended_schemas << schema
33
+ end
34
+
35
+ def self.extended_schemas
36
+ @extended_schemas ||= []
37
+ end
38
+
39
+ def self.schemas
40
+ ([schema] + extended_schemas).flatten
41
+ end
42
+
43
+ def self.all_attributes
44
+ scim_attributes = schemas.map(&:scim_attributes).flatten.map(&:name)
45
+ scim_attributes + [:id, :externalId, :meta]
46
+ end
47
+
48
+ def self.complex_scim_attributes
49
+ schema.scim_attributes.select(&:complexType).group_by(&:name)
50
+ end
51
+
52
+ def complex_type_from_hash(scim_attribute, attr_value)
53
+ if attr_value.is_a?(Hash)
54
+ scim_attribute.complexType.new(attr_value)
55
+ else
56
+ attr_value
57
+ end
58
+ end
59
+
60
+ def constantize_complex_types(hash)
61
+ hash.with_indifferent_access.each_pair do |attr_name, attr_value|
62
+ scim_attribute = self.class.complex_scim_attributes[attr_name].try(:first)
63
+ if scim_attribute && scim_attribute.complexType
64
+ if scim_attribute.multiValued
65
+ self.send("#{attr_name}=", attr_value.map {|attr_for_each_item| complex_type_from_hash(scim_attribute, attr_for_each_item)})
66
+ else
67
+ self.send("#{attr_name}=", complex_type_from_hash(scim_attribute, attr_value))
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def as_json(options = {})
74
+ self.meta = Meta.new unless self.meta
75
+ meta.resourceType = self.class.resource_type_id
76
+ original_hash = super(options).except('errors')
77
+ original_hash.merge!("schemas" => self.class.schemas.map(&:id))
78
+ self.class.extended_schemas.each do |extension_schema|
79
+ extension_attributes = extension_schema.scim_attributes.map(&:name)
80
+ original_hash.merge!(extension_schema.id => original_hash.extract!(*extension_attributes))
81
+ end
82
+ original_hash
83
+ end
84
+
85
+ def self.resource_type_id
86
+ name.demodulize
87
+ end
88
+
89
+ def self.resource_type(location)
90
+ resource_type = ResourceType.new(
91
+ endpoint: endpoint,
92
+ schema: schema.id,
93
+ id: resource_type_id,
94
+ name: resource_type_id,
95
+ schemaExtensions: extended_schemas.map(&:id)
96
+ )
97
+
98
+ resource_type.meta.location = location
99
+ resource_type
100
+ end
101
+
102
+ def validate_resource
103
+ self.class.schema.valid?(self)
104
+ self.class.extended_schemas.each do |extended_schema|
105
+ extended_schema.valid?(self)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,13 @@
1
+ module ScimEngine
2
+ module Resources
3
+ class Group < Base
4
+
5
+ set_schema Schema::Group
6
+
7
+ def self.endpoint
8
+ "/Groups"
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module ScimEngine
2
+ module Resources
3
+ class User < Base
4
+
5
+ set_schema Schema::User
6
+
7
+ def self.endpoint
8
+ "/Users"
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,82 @@
1
+ module ScimEngine
2
+ module Schema
3
+ class Attribute
4
+ include ActiveModel::Model
5
+ include ScimEngine::Errors
6
+ attr_accessor :name, :type, :multiValued, :required, :caseExact, :mutability, :returned, :uniqueness, :subAttributes, :complexType, :canonicalValues
7
+
8
+ def initialize(options = {})
9
+ defaults = {
10
+ multiValued: false,
11
+ required: true,
12
+ caseExact: false,
13
+ mutability: 'readWrite',
14
+ uniqueness: 'none',
15
+ returned: 'default',
16
+ canonicalValues: []
17
+ }
18
+
19
+ if options[:complexType]
20
+ defaults.merge!(type: 'complex', subAttributes: options[:complexType].schema.scim_attributes)
21
+ end
22
+
23
+ super(defaults.merge(options || {}))
24
+ end
25
+
26
+ def valid?(value)
27
+ return valid_blank? if value.blank? && !value.is_a?(FalseClass)
28
+
29
+ if type == 'complex'
30
+ return all_valid?(complexType, value) if multiValued
31
+ valid_complex_type?(value)
32
+ else
33
+ valid_simple_type?(value)
34
+ end
35
+ end
36
+
37
+ def valid_blank?
38
+ return true unless self.required
39
+ errors.add(self.name, "is required")
40
+ false
41
+ end
42
+
43
+ def valid_complex_type?(value)
44
+ if !value.class.respond_to?(:schema) || value.class.schema != complexType.schema
45
+ errors.add(self.name, "has to follow the complexType format.")
46
+ return false
47
+ end
48
+ value.class.schema.valid?(value)
49
+ return true if value.errors.empty?
50
+ add_errors_from_hash(value.errors.to_hash, prefix: self.name)
51
+ false
52
+ end
53
+
54
+ def valid_simple_type?(value)
55
+ valid = (type == 'string' && value.is_a?(String)) ||
56
+ (type == 'boolean' && (value.is_a?(TrueClass) || value.is_a?(FalseClass))) ||
57
+ (type == 'integer' && (value.is_a?(Integer))) ||
58
+ (type == 'dateTime' && valid_date_time?(value))
59
+ errors.add(self.name, "has the wrong type. It has to be a(n) #{self.type}.") unless valid
60
+ valid
61
+ end
62
+
63
+ def valid_date_time?(value)
64
+ !!Time.iso8601(value)
65
+ rescue ArgumentError
66
+ false
67
+ end
68
+
69
+ def all_valid?(complex_type, value)
70
+ validations = value.map {|value_in_array| valid_complex_type?(value_in_array)}
71
+ validations.all?
72
+ end
73
+
74
+ def as_json(options = {})
75
+ options[:except] ||= ['complexType']
76
+ options[:except] << 'canonicalValues' if canonicalValues.empty?
77
+ super.except(options)
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,30 @@
1
+ module ScimEngine
2
+ module Schema
3
+ class Base
4
+ include ActiveModel::Model
5
+ attr_accessor :id, :name, :description, :scim_attributes, :meta
6
+
7
+ def initialize(options = {})
8
+ super
9
+ @meta = Meta.new(resourceType: "Schema")
10
+ end
11
+
12
+ def as_json(options = {})
13
+ @meta.location = ScimEngine::Engine.routes.url_helpers.scim_schemas_path(name: id)
14
+ original = super
15
+ original.merge('attributes' => original.delete('scim_attributes'))
16
+ end
17
+
18
+ def self.valid?(resource)
19
+ cloned_scim_attributes.each do |scim_attribute|
20
+ resource.add_errors_from_hash(scim_attribute.errors.to_hash) unless scim_attribute.valid?(resource.send(scim_attribute.name))
21
+ end
22
+ end
23
+
24
+ def self.cloned_scim_attributes
25
+ scim_attributes.map { |scim_attribute| scim_attribute.clone }
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ module ScimEngine
2
+ module Schema
3
+ module DerivedAttributes
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def set_schema(schema)
8
+ @schema = schema
9
+ derive_attributes_from_schema(schema)
10
+ schema
11
+ end
12
+
13
+ def derive_attributes_from_schema(schema)
14
+ attr_accessor *schema.scim_attributes.map(&:name)
15
+ end
16
+
17
+ def schema
18
+ @schema
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end