scimitar 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.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +16 -0
  3. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +180 -0
  4. data/app/controllers/scimitar/application_controller.rb +129 -0
  5. data/app/controllers/scimitar/resource_types_controller.rb +28 -0
  6. data/app/controllers/scimitar/resources_controller.rb +203 -0
  7. data/app/controllers/scimitar/schemas_controller.rb +16 -0
  8. data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
  9. data/app/models/scimitar/authentication_error.rb +9 -0
  10. data/app/models/scimitar/authentication_scheme.rb +18 -0
  11. data/app/models/scimitar/bulk.rb +8 -0
  12. data/app/models/scimitar/complex_types/address.rb +18 -0
  13. data/app/models/scimitar/complex_types/base.rb +41 -0
  14. data/app/models/scimitar/complex_types/email.rb +12 -0
  15. data/app/models/scimitar/complex_types/entitlement.rb +12 -0
  16. data/app/models/scimitar/complex_types/ims.rb +12 -0
  17. data/app/models/scimitar/complex_types/name.rb +12 -0
  18. data/app/models/scimitar/complex_types/phone_number.rb +12 -0
  19. data/app/models/scimitar/complex_types/photo.rb +12 -0
  20. data/app/models/scimitar/complex_types/reference_group.rb +12 -0
  21. data/app/models/scimitar/complex_types/reference_member.rb +12 -0
  22. data/app/models/scimitar/complex_types/role.rb +12 -0
  23. data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
  24. data/app/models/scimitar/engine_configuration.rb +24 -0
  25. data/app/models/scimitar/error_response.rb +20 -0
  26. data/app/models/scimitar/errors.rb +14 -0
  27. data/app/models/scimitar/filter.rb +11 -0
  28. data/app/models/scimitar/filter_error.rb +22 -0
  29. data/app/models/scimitar/invalid_syntax_error.rb +9 -0
  30. data/app/models/scimitar/lists/count.rb +64 -0
  31. data/app/models/scimitar/lists/query_parser.rb +730 -0
  32. data/app/models/scimitar/meta.rb +7 -0
  33. data/app/models/scimitar/not_found_error.rb +10 -0
  34. data/app/models/scimitar/resource_invalid_error.rb +9 -0
  35. data/app/models/scimitar/resource_type.rb +29 -0
  36. data/app/models/scimitar/resources/base.rb +159 -0
  37. data/app/models/scimitar/resources/group.rb +13 -0
  38. data/app/models/scimitar/resources/mixin.rb +964 -0
  39. data/app/models/scimitar/resources/user.rb +13 -0
  40. data/app/models/scimitar/schema/address.rb +24 -0
  41. data/app/models/scimitar/schema/attribute.rb +123 -0
  42. data/app/models/scimitar/schema/base.rb +86 -0
  43. data/app/models/scimitar/schema/derived_attributes.rb +24 -0
  44. data/app/models/scimitar/schema/email.rb +10 -0
  45. data/app/models/scimitar/schema/entitlement.rb +10 -0
  46. data/app/models/scimitar/schema/group.rb +27 -0
  47. data/app/models/scimitar/schema/ims.rb +10 -0
  48. data/app/models/scimitar/schema/name.rb +20 -0
  49. data/app/models/scimitar/schema/phone_number.rb +10 -0
  50. data/app/models/scimitar/schema/photo.rb +10 -0
  51. data/app/models/scimitar/schema/reference_group.rb +23 -0
  52. data/app/models/scimitar/schema/reference_member.rb +21 -0
  53. data/app/models/scimitar/schema/role.rb +10 -0
  54. data/app/models/scimitar/schema/user.rb +52 -0
  55. data/app/models/scimitar/schema/vdtp.rb +18 -0
  56. data/app/models/scimitar/schema/x509_certificate.rb +22 -0
  57. data/app/models/scimitar/service_provider_configuration.rb +49 -0
  58. data/app/models/scimitar/supportable.rb +14 -0
  59. data/app/views/layouts/scimitar/application.html.erb +14 -0
  60. data/config/initializers/scimitar.rb +82 -0
  61. data/config/routes.rb +6 -0
  62. data/lib/scimitar.rb +23 -0
  63. data/lib/scimitar/engine.rb +63 -0
  64. data/lib/scimitar/version.rb +13 -0
  65. data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
  66. data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
  67. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
  68. data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
  69. data/spec/apps/dummy/app/models/mock_group.rb +83 -0
  70. data/spec/apps/dummy/app/models/mock_user.rb +104 -0
  71. data/spec/apps/dummy/config/application.rb +17 -0
  72. data/spec/apps/dummy/config/boot.rb +2 -0
  73. data/spec/apps/dummy/config/environment.rb +2 -0
  74. data/spec/apps/dummy/config/environments/test.rb +15 -0
  75. data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
  76. data/spec/apps/dummy/config/initializers/scimitar.rb +14 -0
  77. data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
  78. data/spec/apps/dummy/config/routes.rb +24 -0
  79. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +15 -0
  80. data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
  81. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +8 -0
  82. data/spec/apps/dummy/db/schema.rb +42 -0
  83. data/spec/controllers/scimitar/application_controller_spec.rb +173 -0
  84. data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
  85. data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
  86. data/spec/controllers/scimitar/schemas_controller_spec.rb +75 -0
  87. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
  88. data/spec/models/scimitar/complex_types/address_spec.rb +19 -0
  89. data/spec/models/scimitar/complex_types/email_spec.rb +23 -0
  90. data/spec/models/scimitar/lists/count_spec.rb +147 -0
  91. data/spec/models/scimitar/lists/query_parser_spec.rb +763 -0
  92. data/spec/models/scimitar/resource_type_spec.rb +21 -0
  93. data/spec/models/scimitar/resources/base_spec.rb +289 -0
  94. data/spec/models/scimitar/resources/base_validation_spec.rb +61 -0
  95. data/spec/models/scimitar/resources/mixin_spec.rb +2127 -0
  96. data/spec/models/scimitar/resources/user_spec.rb +55 -0
  97. data/spec/models/scimitar/schema/attribute_spec.rb +80 -0
  98. data/spec/models/scimitar/schema/base_spec.rb +64 -0
  99. data/spec/models/scimitar/schema/group_spec.rb +87 -0
  100. data/spec/models/scimitar/schema/user_spec.rb +710 -0
  101. data/spec/requests/active_record_backed_resources_controller_spec.rb +569 -0
  102. data/spec/requests/application_controller_spec.rb +49 -0
  103. data/spec/requests/controller_configuration_spec.rb +17 -0
  104. data/spec/requests/engine_spec.rb +20 -0
  105. data/spec/spec_helper.rb +66 -0
  106. metadata +315 -0
@@ -0,0 +1,13 @@
1
+ module Scimitar
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,24 @@
1
+ module Scimitar
2
+ module Schema
3
+
4
+ # Represents the schema for the Address complex type.
5
+ #
6
+ # See also Scimitar::ComplexTypes::Address
7
+ #
8
+ class Address < Base
9
+
10
+ def self.scim_attributes
11
+ @scim_attributes ||= [
12
+ Attribute.new(name: 'type', type: 'string'),
13
+ Attribute.new(name: 'formatted', type: 'string'),
14
+ Attribute.new(name: 'streetAddress', type: 'string'),
15
+ Attribute.new(name: 'locality', type: 'string'),
16
+ Attribute.new(name: 'region', type: 'string'),
17
+ Attribute.new(name: 'postalCode', type: 'string'),
18
+ Attribute.new(name: 'country', type: 'string'),
19
+ ]
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,123 @@
1
+ module Scimitar
2
+ module Schema
3
+
4
+ # Represents an attribute of a SCIM resource that is declared in its
5
+ # schema.
6
+ #
7
+ # Attributes can be simple or complex. A complex attribute needs to have
8
+ # its own schema that is passed to the initialize method when the attribute
9
+ # is instantiated.
10
+ #
11
+ # Examples:
12
+ #
13
+ # Attribute.new(name: 'userName', type: 'string', uniqueness: 'server')
14
+ # Attribute.new(name: 'name', complexType: Scimitar::ComplexTypes::Name)
15
+ #
16
+ class Attribute
17
+ include ActiveModel::Model
18
+ include Scimitar::Errors
19
+
20
+ attr_accessor :name,
21
+ :type,
22
+ :multiValued,
23
+ :required,
24
+ :caseExact,
25
+ :mutability,
26
+ :returned,
27
+ :uniqueness,
28
+ :subAttributes,
29
+ :complexType,
30
+ :canonicalValues
31
+
32
+ # +options+:: Hash of values to be used for instantiating the attribute
33
+ # object. Some of the instance variables of the objects will
34
+ # have default values if this hash does not contain anything
35
+ # for them.
36
+ #
37
+ def initialize(options = {})
38
+ defaults = {
39
+ multiValued: false,
40
+ required: false,
41
+ caseExact: false,
42
+ mutability: 'readWrite',
43
+ uniqueness: 'none',
44
+ returned: 'default',
45
+ canonicalValues: []
46
+ }
47
+
48
+ if options[:complexType]
49
+ defaults.merge!(type: 'complex', subAttributes: options[:complexType].schema.scim_attributes)
50
+ end
51
+
52
+ super(defaults.merge(options || {}))
53
+ end
54
+
55
+ # Validates a value against this attribute object. For simple attributes,
56
+ # it checks if blank is valid or not and if the type matches. For complex
57
+ # attributes, it delegates it to the valid? method of the complex type
58
+ # schema.
59
+ #
60
+ # If the value is not valid, validation message(s) are added to the
61
+ # #errors attribute of this object.
62
+ #
63
+ # +value+:: Value to check.
64
+ #
65
+ # Returns +true+ if value is valid for this attribute, else +false+.
66
+ #
67
+ def valid?(value)
68
+ return valid_blank? if value.blank? && !value.is_a?(FalseClass)
69
+
70
+ if type == 'complex'
71
+ return all_valid?(complexType, value) if multiValued
72
+ valid_complex_type?(value)
73
+ else
74
+ valid_simple_type?(value)
75
+ end
76
+ end
77
+
78
+ def valid_blank?
79
+ return true unless self.required
80
+ errors.add(self.name, 'is required')
81
+ false
82
+ end
83
+
84
+ def valid_complex_type?(value)
85
+ if !value.class.respond_to?(:schema) || value.class.schema != complexType.schema
86
+ errors.add(self.name, 'has to follow the complexType format.')
87
+ return false
88
+ end
89
+ value.class.schema.valid?(value)
90
+ return true if value.errors.empty?
91
+ add_errors_from_hash(value.errors.to_hash, prefix: self.name)
92
+ false
93
+ end
94
+
95
+ def valid_simple_type?(value)
96
+ valid = (type == 'string' && value.is_a?(String)) ||
97
+ (type == 'boolean' && (value.is_a?(TrueClass) || value.is_a?(FalseClass))) ||
98
+ (type == 'integer' && (value.is_a?(Integer))) ||
99
+ (type == 'dateTime' && valid_date_time?(value))
100
+ errors.add(self.name, "has the wrong type. It has to be a(n) #{self.type}.") unless valid
101
+ valid
102
+ end
103
+
104
+ def valid_date_time?(value)
105
+ !!Time.iso8601(value)
106
+ rescue ArgumentError
107
+ false
108
+ end
109
+
110
+ def all_valid?(complex_type, value)
111
+ validations = value.map {|value_in_array| valid_complex_type?(value_in_array)}
112
+ validations.all?
113
+ end
114
+
115
+ def as_json(options = {})
116
+ options[:except] ||= ['complexType']
117
+ options[:except] << 'canonicalValues' if canonicalValues.empty?
118
+ super.except(options)
119
+ end
120
+
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,86 @@
1
+ module Scimitar
2
+ module Schema
3
+ # The base class that each schema class must inherit from.
4
+ # These classes represent the schema of a SCIM resource or a complex type that could be used in a resource.
5
+ class Base
6
+ include ActiveModel::Model
7
+ attr_accessor :id, :name, :description, :scim_attributes, :meta
8
+
9
+ def initialize(options = {})
10
+ super
11
+ @meta = Meta.new(resourceType: 'Schema')
12
+ end
13
+
14
+ # Converts the schema to its json representation that will be returned by /SCHEMAS end-point of a SCIM service provider.
15
+ def as_json(options = {})
16
+ @meta.location = Scimitar::Engine.routes.url_helpers.scim_schemas_path(name: id)
17
+ original = super
18
+ original.merge('attributes' => original.delete('scim_attributes'))
19
+ end
20
+
21
+ # Validates the resource against specific validations of each attribute,
22
+ # for example if the type of the attribute matches the one defined in the
23
+ # schema.
24
+ #
25
+ # +resource+:: A resource object that uses this schema.
26
+ #
27
+ def self.valid?(resource)
28
+ cloned_scim_attributes.each do |scim_attribute|
29
+ resource.add_errors_from_hash(scim_attribute.errors.to_hash) unless scim_attribute.valid?(resource.send(scim_attribute.name))
30
+ end
31
+ end
32
+
33
+ def self.cloned_scim_attributes
34
+ scim_attributes.map { |scim_attribute| scim_attribute.clone }
35
+ end
36
+
37
+ # Find a given attribute this schema, travelling down a path to any
38
+ # sub-attributes within. Given that callers might be dealing with paths
39
+ # into actual SCIM data, array indices for multi-value attributes are
40
+ # allowed (as integers) and simply skipped - only the names are of
41
+ # interest.
42
+ #
43
+ # This is typically used to access attribute properties such as intended
44
+ # mutability ('readOnly', 'readWrite', 'immutable', 'writeOnly').
45
+ #
46
+ # Returns the found Scimitar::Schema::Attribute or "nil".
47
+ #
48
+ # *path:: One or more attribute names as Strings, or Integer indices.
49
+ #
50
+ # For example, in a User schema, passing "name", "givenName" would find
51
+ # the "givenName" attribute. Passing "emails", 0, "value" would find the
52
+ # schema attribute for "value" under "emails", ignoring the array index
53
+ # (since the schema is identical for each item in an array of values).
54
+ #
55
+ # See also Scimitar::Resources::Base::find_attribute
56
+ #
57
+ def self.find_attribute(*path)
58
+ found_attribute = nil
59
+ current_attributes = self.scim_attributes()
60
+
61
+ until path.empty? do
62
+ current_path_entry = path.shift()
63
+ next if current_path_entry.is_a?(Integer) # Skip array indicies arising from multi-value attributes
64
+
65
+ found_attribute = current_attributes.find do | attribute_to_check |
66
+ attribute_to_check.name == current_path_entry
67
+ end
68
+
69
+ if found_attribute && path.present? # Any sub-attributes to check?...
70
+ if found_attribute.subAttributes.present? # ...and are any defined?
71
+ current_attributes = found_attribute.subAttributes
72
+ else
73
+ found_attribute = nil
74
+ break # NOTE EARLY EXIT - tried to find a sub-attribute but there are none
75
+ end
76
+ else
77
+ break # NOTE EARLY EXIT - no found attribute, or found target item at end of path
78
+ end
79
+ end # "until path.empty() do"
80
+
81
+ return found_attribute
82
+ end
83
+
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,24 @@
1
+ module Scimitar
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
@@ -0,0 +1,10 @@
1
+ module Scimitar
2
+ module Schema
3
+
4
+ # Represents the schema for the Email complex type.
5
+ #
6
+ # See also Scimitar::ComplexTypes::Email
7
+ #
8
+ class Email < Vdtp; end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Scimitar
2
+ module Schema
3
+
4
+ # Represents the schema for the Entitlement complex type.
5
+ #
6
+ # See also Scimitar::ComplexTypes::Entitlement
7
+ #
8
+ class Entitlement < Vdtp; end
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ module Scimitar
2
+ module Schema
3
+ # Represents the schema for the Group resource
4
+ # See also Scimitar::Resources::Group
5
+ class Group < Base
6
+
7
+ def initialize(options = {})
8
+ super(name: 'Group',
9
+ id: self.class.id,
10
+ description: 'Represents a Group',
11
+ scim_attributes: self.class.scim_attributes)
12
+ end
13
+
14
+ def self.id
15
+ 'urn:ietf:params:scim:schemas:core:2.0:Group'
16
+ end
17
+
18
+ def self.scim_attributes
19
+ [
20
+ Attribute.new(name: 'displayName', type: 'string', required: true),
21
+ Attribute.new(name: 'members', multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceMember, mutability: 'readWrite')
22
+ ]
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ module Scimitar
2
+ module Schema
3
+
4
+ # Represents the schema for the Ims (Instant Messaging) complex type.
5
+ #
6
+ # See also Scimitar::ComplexTypes::Ims
7
+ #
8
+ class Ims < Vdtp; end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module Scimitar
2
+ module Schema
3
+ # Represents the schema for the Name complex type
4
+ # See also Scimitar::ComplexTypes::Name
5
+ class Name < Base
6
+
7
+ def self.scim_attributes
8
+ @scim_attributes ||= [
9
+ Attribute.new(name: 'familyName', type: 'string', required: true),
10
+ Attribute.new(name: 'givenName', type: 'string', required: true),
11
+ Attribute.new(name: 'middleName', type: 'string'),
12
+ Attribute.new(name: 'formatted', type: 'string'),
13
+ Attribute.new(name: 'honorificPrefix', type: 'string'),
14
+ Attribute.new(name: 'honorificSuffix', type: 'string'),
15
+ ]
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ module Scimitar
2
+ module Schema
3
+
4
+ # Represents the schema for the PhoneNumber complex type.
5
+ #
6
+ # See also Scimitar::ComplexTypes::PhoneNumber
7
+ #
8
+ class PhoneNumber < Vdtp; end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Scimitar
2
+ module Schema
3
+
4
+ # Represents the schema for the Photo complex type.
5
+ #
6
+ # See also Scimitar::ComplexTypes::Photo
7
+ #
8
+ class Photo < Vdtp; end
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ module Scimitar
2
+ module Schema
3
+
4
+ # Represents the schema for the ReferenceGroup complex type,
5
+ # referring to a group of which a user is a member - used in
6
+ # a User SCIM resource's "groups" array.
7
+ #
8
+ # These are always read-only, with no ability to change the
9
+ # membership list through a User. Change via Groups instead.
10
+ #
11
+ # See also Scimitar::ComplexTypes::ReferenceGroup
12
+ #
13
+ class ReferenceGroup < Base
14
+ def self.scim_attributes
15
+ @scim_attributes ||= [
16
+ Attribute.new(name: 'value', type: 'string', mutability: 'readOnly', required: true),
17
+ Attribute.new(name: 'display', type: 'string', mutability: 'readOnly'),
18
+ Attribute.new(name: 'type', type: 'string', mutability: 'readOnly'),
19
+ ]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module Scimitar
2
+ module Schema
3
+
4
+ # Represents the schema for the ReferenceMember complex type,
5
+ # referring to a member of a group (where members can themselves
6
+ # be Users or Groups, identified by the "type" attribute). Used
7
+ # by the Group SCIM resource's "members" array.
8
+ #
9
+ # See also Scimitar::ComplexTypes::ReferenceMember
10
+ #
11
+ class ReferenceMember < Base
12
+ def self.scim_attributes
13
+ @scim_attributes ||= [
14
+ Attribute.new(name: 'value', type: 'string', mutability: 'immutable', required: true),
15
+ Attribute.new(name: 'type', type: 'string', mutability: 'immutable'),
16
+ Attribute.new(name: 'display', type: 'string', mutability: 'immutable'),
17
+ ]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ module Scimitar
2
+ module Schema
3
+
4
+ # Represents the schema for the Role complex type.
5
+ #
6
+ # See also Scimitar::ComplexTypes::Role
7
+ #
8
+ class Role < Vdtp; end
9
+ end
10
+ end