scim_engine 1.0.0
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +25 -0
- data/app/controllers/scim_engine/application_controller.rb +36 -0
- data/app/controllers/scim_engine/resource_types_controller.rb +22 -0
- data/app/controllers/scim_engine/resources_controller.rb +71 -0
- data/app/controllers/scim_engine/schemas_controller.rb +16 -0
- data/app/controllers/scim_engine/service_provider_configurations_controller.rb +8 -0
- data/app/models/scim_engine/authentication_error.rb +9 -0
- data/app/models/scim_engine/authentication_scheme.rb +12 -0
- data/app/models/scim_engine/bulk.rb +5 -0
- data/app/models/scim_engine/complex_types/base.rb +19 -0
- data/app/models/scim_engine/complex_types/email.rb +11 -0
- data/app/models/scim_engine/complex_types/name.rb +7 -0
- data/app/models/scim_engine/complex_types/reference.rb +7 -0
- data/app/models/scim_engine/error_response.rb +13 -0
- data/app/models/scim_engine/errors.rb +14 -0
- data/app/models/scim_engine/filter.rb +5 -0
- data/app/models/scim_engine/meta.rb +7 -0
- data/app/models/scim_engine/not_found_error.rb +10 -0
- data/app/models/scim_engine/resource_invalid_error.rb +9 -0
- data/app/models/scim_engine/resource_type.rb +28 -0
- data/app/models/scim_engine/resources/base.rb +110 -0
- data/app/models/scim_engine/resources/group.rb +13 -0
- data/app/models/scim_engine/resources/user.rb +13 -0
- data/app/models/scim_engine/schema/attribute.rb +82 -0
- data/app/models/scim_engine/schema/base.rb +30 -0
- data/app/models/scim_engine/schema/derived_attributes.rb +24 -0
- data/app/models/scim_engine/schema/email.rb +13 -0
- data/app/models/scim_engine/schema/group.rb +25 -0
- data/app/models/scim_engine/schema/name.rb +15 -0
- data/app/models/scim_engine/schema/reference.rb +12 -0
- data/app/models/scim_engine/schema/user.rb +28 -0
- data/app/models/scim_engine/service_provider_configuration.rb +30 -0
- data/app/models/scim_engine/supportable.rb +10 -0
- data/app/views/layouts/scim_engine/application.html.erb +14 -0
- data/config/routes.rb +6 -0
- data/lib/scim_engine/engine.rb +35 -0
- data/lib/scim_engine/version.rb +3 -0
- data/lib/scim_engine.rb +13 -0
- data/lib/tasks/scim_engine_tasks.rake +4 -0
- data/spec/controllers/scim_engine/application_controller_spec.rb +104 -0
- data/spec/controllers/scim_engine/resource_types_controller_spec.rb +78 -0
- data/spec/controllers/scim_engine/resources_controller_spec.rb +134 -0
- data/spec/controllers/scim_engine/schemas_controller_spec.rb +66 -0
- data/spec/controllers/scim_engine/service_provider_configurations_controller_spec.rb +21 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config/application.rb +16 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/to_time_preserves_timezone.rb +10 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/log/test.log +863 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/models/scim_engine/complex_types/email_spec.rb +23 -0
- data/spec/models/scim_engine/resource_type_spec.rb +21 -0
- data/spec/models/scim_engine/resources/base_spec.rb +246 -0
- data/spec/models/scim_engine/resources/base_validation_spec.rb +61 -0
- data/spec/models/scim_engine/resources/user_spec.rb +55 -0
- data/spec/models/scim_engine/schema/attribute_spec.rb +80 -0
- data/spec/models/scim_engine/schema/base_spec.rb +19 -0
- data/spec/models/scim_engine/schema/group_spec.rb +66 -0
- data/spec/models/scim_engine/schema/user_spec.rb +140 -0
- data/spec/rails_helper.rb +57 -0
- data/spec/spec_helper.rb +99 -0
- 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,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,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,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,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,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
|