scimitar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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