├── lib ├── scim │ ├── kit │ │ ├── v2 │ │ │ ├── templates │ │ │ │ ├── nil_class.json.jbuilder │ │ │ │ ├── supportable.json.jbuilder │ │ │ │ ├── authentication_scheme.json.jbuilder │ │ │ │ ├── meta.json.jbuilder │ │ │ │ ├── schema.json.jbuilder │ │ │ │ ├── attribute.json.jbuilder │ │ │ │ ├── resource_type.json.jbuilder │ │ │ │ ├── attribute_type.json.jbuilder │ │ │ │ ├── service_provider_configuration.json.jbuilder │ │ │ │ └── resource.json.jbuilder │ │ │ ├── messages.rb │ │ │ ├── uniqueness.rb │ │ │ ├── returned.rb │ │ │ ├── schemas.rb │ │ │ ├── unknown_attribute.rb │ │ │ ├── supportable.rb │ │ │ ├── mutability.rb │ │ │ ├── error.rb │ │ │ ├── filter │ │ │ │ ├── node.rb │ │ │ │ └── visitor.rb │ │ │ ├── meta.rb │ │ │ ├── complex_attribute_validator.rb │ │ │ ├── resource_type.rb │ │ │ ├── service_provider_configuration.rb │ │ │ ├── resource.rb │ │ │ ├── schema.rb │ │ │ ├── authentication_scheme.rb │ │ │ ├── configuration.rb │ │ │ ├── attribute.rb │ │ │ ├── attributable.rb │ │ │ ├── attribute_type.rb │ │ │ └── filter.rb │ │ ├── version.rb │ │ ├── dynamic_attributes.rb │ │ ├── template.rb │ │ ├── http.rb │ │ ├── templatable.rb │ │ └── v2.rb │ └── kit.rb └── scim-kit.rb ├── .rspec ├── exe └── scim-kit ├── spec ├── fixtures │ └── avatar.png ├── scim │ ├── kit_spec.rb │ └── kit │ │ ├── v2_spec.rb │ │ └── v2 │ │ ├── error_spec.rb │ │ ├── resource_type_spec.rb │ │ ├── configuration_spec.rb │ │ ├── service_provider_configuration_spec.rb │ │ ├── filter_spec.rb │ │ ├── schema_spec.rb │ │ ├── attribute_spec.rb │ │ ├── attribute_type_spec.rb │ │ └── resource_spec.rb └── spec_helper.rb ├── bin ├── shipit ├── style ├── audit ├── test ├── setup └── console ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── Gemfile ├── Rakefile ├── LICENSE.txt ├── scim-kit.gemspec ├── .rubocop.yml ├── README.md ├── Gemfile.lock └── CHANGELOG.md /lib/scim/kit/v2/templates/nil_class.json.jbuilder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/scim-kit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'scim/kit' 4 | -------------------------------------------------------------------------------- /exe/scim-kit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'scim/kit' 5 | -------------------------------------------------------------------------------- /spec/fixtures/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xlgmokha/scim-kit/HEAD/spec/fixtures/avatar.png -------------------------------------------------------------------------------- /bin/shipit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | bundle exec rake release 8 | -------------------------------------------------------------------------------- /bin/style: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | bundle exec rubocop "$@" 8 | -------------------------------------------------------------------------------- /bin/audit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | bundle exec rake bundle:audit 8 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | export RUBYOPT='-W0' 8 | 9 | bundle exec rspec "$@" 10 | -------------------------------------------------------------------------------- /lib/scim/kit/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | VERSION = '0.7.2' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | gem update --system 8 | gem install bundler 9 | bundle install 10 | -------------------------------------------------------------------------------- /spec/scim/kit_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit do 4 | specify { expect(Scim::Kit::VERSION).not_to be_nil } 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | .byebug_history 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "bundler" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | assignees: 9 | - "xlgmokha" 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Specify your gem's dependencies in scim-kit.gemspec 8 | gemspec 9 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/templates/supportable.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.key_format! camelize: :lower 4 | json.supported supported 5 | @dynamic_attributes.each do |key, value| 6 | json.set! key.to_s.delete('='), value 7 | end 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/audit/task' 4 | require 'bundler/gem_tasks' 5 | require 'rspec/core/rake_task' 6 | require 'rubocop/rake_task' 7 | 8 | RSpec::Core::RakeTask.new(:spec) 9 | RuboCop::RakeTask.new(:rubocop) 10 | Bundler::Audit::Task.new 11 | 12 | task default: :spec 13 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/templates/authentication_scheme.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.key_format! camelize: :lower 4 | json.name name 5 | json.description description 6 | json.spec_uri spec_uri 7 | json.documentation_uri documentation_uri 8 | json.type type 9 | json.primary primary if primary 10 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/templates/meta.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.key_format! camelize: :lower 4 | json.location location 5 | json.resource_type resource_type 6 | json.created created.iso8601 if created 7 | json.last_modified last_modified.iso8601 if last_modified 8 | json.version version if version 9 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/templates/schema.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.key_format! camelize: :lower 4 | json.meta do 5 | render meta, json: json 6 | end 7 | json.id id 8 | json.name name 9 | json.description description 10 | json.attributes attributes do |attribute| 11 | render attribute, json: json 12 | end 13 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/templates/attribute.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.key_format! camelize: :lower 4 | if _type.complex? && !_type.multi_valued 5 | json.set! _type.name do 6 | dynamic_attributes.each_value do |attribute| 7 | render attribute, json: json 8 | end 9 | end 10 | elsif renderable? 11 | json.set! _type.name, _value 12 | end 13 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/templates/resource_type.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.key_format! camelize: :lower 4 | json.meta do 5 | render meta, json: json 6 | end 7 | json.schemas [Scim::Kit::V2::Schemas::RESOURCE_TYPE] 8 | json.id id 9 | json.name name 10 | json.description description 11 | json.endpoint endpoint 12 | json.schema schema 13 | json.schema_extensions schema_extensions 14 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'scim/kit' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /spec/scim/kit/v2_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit::V2 do 4 | subject { described_class } 5 | 6 | describe '.configure' do 7 | specify do 8 | subject.configure do |config| 9 | expect(config).to be_instance_of(Scim::Kit::V2::Configuration::Builder) 10 | end 11 | end 12 | 13 | specify do 14 | called = false 15 | subject.configure { |_config| called = true } 16 | expect(called).to be(true) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/scim/kit/dynamic_attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | # Allows dynamic creation of attributes. 6 | module DynamicAttributes 7 | def method_missing(method, *args) 8 | return super unless respond_to_missing?(method) 9 | 10 | @dynamic_attributes[method] = args[0] 11 | end 12 | 13 | def respond_to_missing?(method, _include_private = false) 14 | @dynamic_attributes.key?(method) || super 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/messages.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | module Messages 7 | CORE = 'urn:ietf:params:scim:api:messages:2.0' 8 | BULK_REQUEST = "#{CORE}:BulkRequest".freeze 9 | BULK_RESPONSE = "#{CORE}:BulkResponse".freeze 10 | ERROR = "#{CORE}:Error".freeze 11 | LIST_RESPONSE = "#{CORE}:ListResponse".freeze 12 | PATCH_OP = "#{CORE}:PatchOp".freeze 13 | SEARCH_REQUEST = "#{CORE}:SearchRequest".freeze 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/uniqueness.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents the valid Uniqueness values 7 | class Uniqueness 8 | NONE = 'none' 9 | SERVER = 'server' 10 | GLOBAL = 'global' 11 | VALID = { 12 | none: NONE, 13 | server: SERVER, 14 | global: GLOBAL 15 | }.freeze 16 | 17 | def self.find(value) 18 | VALID[value.to_sym] || (raise ArgumentError, :uniqueness) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/scim/kit/v2/error_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit::V2::Error do 4 | subject { described_class.new } 5 | 6 | before do 7 | subject.scim_type = :invalidSyntax 8 | subject.detail = 'error' 9 | subject.status = 400 10 | end 11 | 12 | specify { expect(subject.to_h[:schemas]).to match_array([Scim::Kit::V2::Messages::ERROR]) } 13 | specify { expect(subject.to_h[:scimType]).to eql('invalidSyntax') } 14 | specify { expect(subject.to_h[:detail]).to eql('error') } 15 | specify { expect(subject.to_h[:status]).to eql('400') } 16 | end 17 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/returned.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents the valid Returned values 7 | class Returned 8 | ALWAYS = 'always' 9 | NEVER = 'never' 10 | DEFAULT = 'default' 11 | REQUEST = 'request' 12 | VALID = { 13 | always: ALWAYS, 14 | never: NEVER, 15 | default: DEFAULT, 16 | request: REQUEST 17 | }.freeze 18 | 19 | def self.find(value) 20 | VALID[value.to_sym] || (raise ArgumentError, :returned) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'scim/kit' 5 | require 'ffaker' 6 | require 'json' 7 | require 'parslet/convenience' 8 | require 'parslet/rig/rspec' 9 | require 'webmock/rspec' 10 | 11 | Scim::Kit.logger = Logger.new('/dev/null') 12 | 13 | RSpec.configure do |config| 14 | # Enable flags like --only-failures and --next-failure 15 | config.example_status_persistence_file_path = '.rspec_status' 16 | 17 | # Disable RSpec exposing methods globally on `Module` and `main` 18 | config.disable_monkey_patching! 19 | 20 | config.expect_with :rspec do |c| 21 | c.syntax = :expect 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/templates/attribute_type.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.key_format! camelize: :lower 4 | json.description description 5 | json.multi_valued multi_valued 6 | json.mutability mutability 7 | json.name name.camelize(:lower) 8 | json.required required 9 | json.returned returned 10 | json.type type 11 | json.uniqueness uniqueness 12 | json.case_exact(case_exact) if string? || reference? 13 | json.reference_types(reference_types) if reference? 14 | json.canonical_values(canonical_values) if canonical_values 15 | if complex? 16 | json.sub_attributes attributes do |attribute| 17 | render attribute, json: json 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/schemas.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | module Schemas 7 | ROOT = 'urn:ietf:params:scim:schemas' 8 | 9 | CORE = "#{ROOT}:core:2.0".freeze 10 | EXTENSION = "#{ROOT}:extension".freeze 11 | ENTERPRISE_USER = "#{EXTENSION}:enterprise:2.0:User".freeze 12 | GROUP = "#{CORE}:Group".freeze 13 | RESOURCE_TYPE = "#{CORE}:ResourceType".freeze 14 | SCHEMA = "#{CORE}:Schema".freeze 15 | SERVICE_PROVIDER_CONFIGURATION = "#{CORE}:ServiceProviderConfig".freeze 16 | USER = "#{CORE}:User".freeze 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/scim/kit/template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | # Represents a Jbuilder template 6 | class Template 7 | TEMPLATES_DIR = Pathname.new(File.join(__dir__, 'v2/templates/')) 8 | 9 | attr_reader :target 10 | 11 | def initialize(target) 12 | @target = target 13 | end 14 | 15 | def to_json(options = {}) 16 | template.render(target, options) 17 | end 18 | 19 | private 20 | 21 | def template_path 22 | TEMPLATES_DIR.join(target.template_name) 23 | end 24 | 25 | def template 26 | @template ||= Tilt.new(template_path.to_s) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/unknown_attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents an Unknown/Unrecognized Attribute 7 | class UnknownAttribute 8 | include ::ActiveModel::Validations 9 | validate :unknown 10 | attr_reader :name 11 | 12 | def initialize(name) 13 | @name = name 14 | end 15 | 16 | def _assign(*_args) 17 | valid? 18 | end 19 | 20 | def _value=(*_args) 21 | raise Scim::Kit::UnknownAttributeError, name 22 | end 23 | 24 | def unknown 25 | errors.add(name, I18n.t('errors.messages.invalid')) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/templates/service_provider_configuration.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.key_format! camelize: :lower 4 | json.meta do 5 | render meta, json: json 6 | end 7 | json.schemas [Scim::Kit::V2::Schemas::SERVICE_PROVIDER_CONFIGURATION] 8 | json.documentation_uri documentation_uri 9 | json.patch do 10 | render patch, json: json 11 | end 12 | json.bulk do 13 | render bulk, json: json 14 | end 15 | json.filter do 16 | render filter, json: json 17 | end 18 | json.change_password do 19 | render change_password, json: json 20 | end 21 | json.sort do 22 | render sort, json: json 23 | end 24 | json.etag do 25 | render etag, json: json 26 | end 27 | json.authentication_schemes authentication_schemes do |authentication_scheme| 28 | render authentication_scheme, json: json 29 | end 30 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/templates/resource.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.key_format! camelize: :lower 4 | if mode?(:server) 5 | json.meta do 6 | render meta, json: json 7 | end 8 | end 9 | json.schemas schemas.map(&:id) 10 | json.id id if mode?(:server) 11 | json.external_id external_id if mode?(:client) && external_id 12 | schemas.each do |schema| 13 | if schema.core? 14 | schema.attributes.each do |type| 15 | attribute = dynamic_attributes[type.name] 16 | render attribute, json: json 17 | end 18 | else 19 | json.set! schema.id do 20 | schema.attributes.each do |type| 21 | attribute = dynamic_attributes[type.fully_qualified_name] || 22 | dynamic_attributes[type.name] 23 | render attribute, json: json 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/supportable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents a Feature 7 | class Supportable 8 | include Templatable 9 | include DynamicAttributes 10 | 11 | attr_accessor :supported 12 | 13 | def initialize(*dynamic_attributes) 14 | dynamic_attributes.delete(:supported) 15 | @dynamic_attributes = Hash[ 16 | dynamic_attributes.map { |x| ["#{x}=".to_sym, nil] } 17 | ] 18 | @supported = false 19 | end 20 | 21 | class << self 22 | def from(hash) 23 | x = new(*hash.keys) 24 | hash.each do |key, value| 25 | x.public_send("#{key}=", value) 26 | end 27 | x 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/scim/kit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_model' 4 | require 'active_support/core_ext/hash/indifferent_access' 5 | require 'json' 6 | require 'logger' 7 | require 'net/hippie' 8 | require 'pathname' 9 | require 'tilt' 10 | require 'tilt/jbuilder' 11 | 12 | require 'scim/kit/dynamic_attributes' 13 | require 'scim/kit/http' 14 | require 'scim/kit/templatable' 15 | require 'scim/kit/template' 16 | require 'scim/kit/v2' 17 | require 'scim/kit/version' 18 | 19 | module Scim 20 | # @api 21 | module Kit 22 | class Error < StandardError; end 23 | class UnknownAttributeError < Error; end 24 | class NotImplementedError < Error; end 25 | TYPE_ERROR = ArgumentError.new(:type) 26 | 27 | def self.logger 28 | @logger ||= Logger.new($stdout) 29 | end 30 | 31 | def self.logger=(logger) 32 | @logger = logger 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/mutability.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents the valid Mutability values 7 | class Mutability 8 | READ_ONLY = 'readOnly' 9 | READ_WRITE = 'readWrite' 10 | IMMUTABLE = 'immutable' 11 | WRITE_ONLY = 'writeOnly' 12 | VALID = { 13 | immutable: IMMUTABLE, 14 | readOnly: READ_ONLY, 15 | readWrite: READ_WRITE, 16 | read_only: READ_ONLY, 17 | read_write: READ_WRITE, 18 | readonly: READ_ONLY, 19 | readwrite: READ_WRITE, 20 | writeOnly: WRITE_ONLY, 21 | write_only: WRITE_ONLY, 22 | writeonly: WRITE_ONLY 23 | }.freeze 24 | 25 | def self.find(value) 26 | VALID[value.to_sym] || (raise ArgumentError, :mutability) 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | ruby-version: ['3.2', '3.3', '3.4'] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: ${{ matrix.ruby-version }} 19 | - run: sh bin/setup 20 | - name: Running tests… 21 | run: sh bin/test 22 | style: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: '3.2' 29 | bundler-cache: true 30 | - name: Running style checks… 31 | run: sh bin/style 32 | audit: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: ruby/setup-ruby@v1 37 | with: 38 | ruby-version: '3.2' 39 | bundler-cache: true 40 | - name: Running audit… 41 | run: sh bin/audit 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 mo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents a SCIM Error 7 | class Error < Resource 8 | SCIM_TYPES = %w[ 9 | invalidFilter 10 | invalidPath 11 | invalidSyntax 12 | invalidValue 13 | invalidVers 14 | mutability 15 | noTarget 16 | sensitive 17 | tooMany 18 | uniqueness 19 | ].freeze 20 | 21 | def initialize(schemas: [self.class.default_schema]) 22 | super(schemas: schemas) 23 | end 24 | 25 | def template_name 26 | 'resource.json.jbuilder' 27 | end 28 | 29 | def self.default_schema 30 | Schema.new(id: Messages::ERROR, name: 'Error', location: nil) do |x| 31 | x.add_attribute(name: :scim_type) do |attribute| 32 | attribute.canonical_values = SCIM_TYPES 33 | end 34 | x.add_attribute(name: :detail) 35 | x.add_attribute(name: :status, type: :string) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/filter/node.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'parslet' 4 | 5 | module Scim 6 | module Kit 7 | module V2 8 | class Filter 9 | # @private 10 | class Node 11 | def initialize(hash) 12 | @hash = hash 13 | end 14 | 15 | def operator 16 | self[:operator].to_sym 17 | end 18 | 19 | def attribute 20 | self[:attribute].to_s 21 | end 22 | 23 | def value 24 | self[:value].to_s[1..-2] 25 | end 26 | 27 | def not? 28 | @hash.key?(:not) 29 | end 30 | 31 | def accept(visitor) 32 | visitor.visit(self) 33 | end 34 | 35 | def left 36 | self.class.new(self[:left]) 37 | end 38 | 39 | def right 40 | self.class.new(self[:right]) 41 | end 42 | 43 | def inspect 44 | @hash.inspect 45 | end 46 | 47 | private 48 | 49 | def [](key) 50 | @hash[key] 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/meta.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents a meta section 7 | class Meta 8 | include Templatable 9 | 10 | attr_accessor :created, :last_modified, :version 11 | attr_reader :location 12 | attr_reader :resource_type 13 | 14 | def initialize(resource_type, location) 15 | @resource_type = resource_type || 'Unknown' 16 | @location = location 17 | @created = @last_modified = Time.now 18 | @version = @created.to_i 19 | end 20 | 21 | def disable_timestamps 22 | @version = @created = @last_modified = nil 23 | end 24 | 25 | def self.from(hash) 26 | meta = Meta.new(hash[:resourceType], hash[:location]) 27 | meta.created = parse_date(hash[:created]) 28 | meta.last_modified = parse_date(hash[:lastModified]) 29 | meta.version = hash[:version] 30 | meta 31 | end 32 | 33 | def self.parse_date(date) 34 | DateTime.parse(date).to_time 35 | rescue StandardError 36 | nil 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/scim/kit/http.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | class Http 6 | attr_reader :driver, :retries 7 | 8 | def initialize(driver: Http.default_driver, retries: 3) 9 | @driver = driver 10 | @retries = retries 11 | end 12 | 13 | def get(uri) 14 | driver.with_retry(retries: retries) do |client| 15 | response = client.get(uri) 16 | ok?(response) ? JSON.parse(response.body, symbolize_names: true) : {} 17 | end 18 | rescue *Net::Hippie::CONNECTION_ERRORS => error 19 | Scim::Kit.logger.error(error) 20 | {} 21 | end 22 | 23 | def self.default_driver 24 | @default_driver ||= Net::Hippie::Client.new( 25 | follow_redirects: 3, 26 | headers: headers, 27 | logger: Scim::Kit.logger, 28 | open_timeout: 1, 29 | read_timeout: 5 30 | ) 31 | end 32 | 33 | def self.headers 34 | { 35 | 'Accept' => 'application/scim+json', 36 | 'Content-Type' => 'application/scim+json', 37 | 'User-Agent' => "scim/kit #{Scim::Kit::VERSION}" 38 | } 39 | end 40 | 41 | private 42 | 43 | def ok?(response) 44 | response.is_a?(Net::HTTPSuccess) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/complex_attribute_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Validates a complex attribute 7 | class ComplexAttributeValidator < ::ActiveModel::Validator 8 | def validate(item) 9 | if item._type.multi_valued 10 | multi_valued_validation(item) 11 | else 12 | item.each do |attribute| 13 | item.errors.merge!(attribute.errors) unless attribute.valid? 14 | end 15 | end 16 | end 17 | 18 | private 19 | 20 | def multi_valued_validation(item) 21 | item.each_value do |hash| 22 | validated = hash.map do |key, value| 23 | attribute = item.attribute_for(key) 24 | attribute._assign(value) 25 | item.errors.merge!(attribute.errors) unless attribute.valid? 26 | 27 | key.to_sym 28 | end 29 | validate_missing(item, hash, validated) 30 | end 31 | end 32 | 33 | def validate_missing(item, hash, validated) 34 | not_validated = item.map { |x| x._type.name.to_sym } - validated 35 | not_validated.each do |key| 36 | attribute = item.attribute_for(key) 37 | attribute._assign(hash[key]) 38 | item.errors.merge!(attribute.errors) unless attribute.valid? 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/scim/kit/templatable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | # Implement methods necessary to generate json from jbuilder templates. 6 | module Templatable 7 | # Returns the JSON representation of the item. 8 | # @param options [Hash] the hash of options to forward to jbuilder 9 | # return [String] the json string 10 | def to_json(options = {}) 11 | render(self, options) 12 | end 13 | 14 | # Returns the hash representation of the JSON 15 | # @return [Hash] the hash representation of the items JSON. 16 | def as_json(_options = nil) 17 | to_h 18 | end 19 | 20 | # Returns the hash representation of the JSON 21 | # @return [Hash] the hash representation of the items JSON. 22 | def to_h 23 | JSON.parse(to_json, symbolize_names: true).with_indifferent_access 24 | end 25 | 26 | # Renders the model to JSON. 27 | # @param model [Object] the model to render. 28 | # @param options [Hash] the hash of options to pass to jbuilder. 29 | # @return [String] the JSON. 30 | def render(model, options) 31 | Template.new(model).to_json(options) 32 | end 33 | 34 | # Returns the file name of the jbuilder template. 35 | # @return [String] name of the jbuilder template. 36 | def template_name 37 | "#{self.class.name.split('::').last.underscore}.json.jbuilder" 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/resource_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents a ResourceType Schema 7 | # https://tools.ietf.org/html/rfc7643#section-6 8 | class ResourceType 9 | include Templatable 10 | attr_accessor :id 11 | attr_accessor :name 12 | attr_accessor :description 13 | attr_accessor :endpoint 14 | attr_accessor :schema 15 | attr_reader :schema_extensions 16 | attr_accessor :meta 17 | 18 | def initialize(location:) 19 | @meta = Meta.new('ResourceType', location) 20 | @meta.version = @meta.created = @meta.last_modified = nil 21 | @schema_extensions = [] 22 | end 23 | 24 | def add_schema_extension(schema:, required: false) 25 | @schema_extensions.push(schema: schema, required: required) 26 | end 27 | 28 | class << self 29 | def build(**args) 30 | item = new(**args) 31 | yield item 32 | item 33 | end 34 | 35 | def from(hash) 36 | x = new(location: hash[:location]) 37 | x.meta = Meta.from(hash[:meta]) 38 | %i[id name description endpoint schema].each do |key| 39 | x.public_send("#{key}=", hash[key]) 40 | end 41 | hash[:schemaExtensions].each do |y| 42 | x.add_schema_extension(schema: y[:schema], required: y[:required]) 43 | end 44 | x 45 | end 46 | 47 | def parse(json) 48 | from(JSON.parse(json, symbolize_names: true)) 49 | end 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /scim-kit.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'scim/kit/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'scim-kit' 9 | spec.version = Scim::Kit::VERSION 10 | spec.authors = ['mo'] 11 | spec.email = ['mo@mokhan.ca'] 12 | 13 | spec.summary = 'A simple toolkit for working with SCIM 2.0' 14 | spec.description = 'A simple toolkit for working with SCIM 2.0' 15 | spec.homepage = 'https://github.com/xlgmokha/scim-kit' 16 | spec.license = 'MIT' 17 | 18 | # Specify which files should be added to the gem when it is released. 19 | # The `git ls-files -z` loads the files that have been added into git. 20 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 21 | `git ls-files -z`.split("\x0").reject do |file| 22 | file.match(%r{^(test|spec|features)/}) 23 | end 24 | end 25 | spec.bindir = 'exe' 26 | spec.executables = spec.files.grep(%r{^exe/}) do |file| 27 | File.basename(file) 28 | end 29 | spec.require_paths = ['lib'] 30 | spec.required_ruby_version = Gem::Requirement.new('>= 3.2.0') 31 | spec.metadata['yard.run'] = 'yri' 32 | 33 | spec.add_dependency 'activemodel', '>= 6.1' 34 | spec.add_dependency 'net-hippie', '~> 1.0' 35 | spec.add_dependency 'parslet', '~> 2.0' 36 | spec.add_dependency 'tilt', '~> 2.0' 37 | spec.add_dependency 'tilt-jbuilder', '~> 0.7' 38 | spec.add_development_dependency 'bundler-audit', '~> 0.6' 39 | spec.add_development_dependency 'ffaker', '~> 2.7' 40 | spec.add_development_dependency 'rake', '~> 13.0' 41 | spec.add_development_dependency 'rspec', '~> 3.0' 42 | spec.add_development_dependency 'rubocop', '~> 1.0' 43 | spec.add_development_dependency 'rubocop-rspec', '~> 2.0' 44 | spec.add_development_dependency 'webmock', '~> 3.5' 45 | end 46 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/service_provider_configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents a scim Service Provider Configuration 7 | class ServiceProviderConfiguration 8 | include Templatable 9 | attr_accessor :bulk, :filter 10 | attr_accessor :etag, :sort, :change_password, :patch 11 | attr_accessor :meta, :documentation_uri 12 | attr_accessor :authentication_schemes 13 | 14 | def initialize( 15 | location:, 16 | meta: Meta.new('ServiceProviderConfig', location) 17 | ) 18 | @meta = meta 19 | @authentication_schemes = [] 20 | @etag = Supportable.new 21 | @sort = Supportable.new 22 | @change_password = Supportable.new 23 | @patch = Supportable.new 24 | @bulk = Supportable.new(:max_operations, :max_payload_size) 25 | @filter = Supportable.new(:max_results) 26 | end 27 | 28 | def add_authentication(type, primary: nil) 29 | scheme = AuthenticationScheme.build_for(type, primary: primary) 30 | yield scheme if block_given? 31 | @authentication_schemes << scheme 32 | end 33 | 34 | class << self 35 | def parse(json, hash = JSON.parse(json, symbolize_names: true)) 36 | x = new(location: hash[:location], meta: Meta.from(hash[:meta])) 37 | x.documentation_uri = hash[:documentationUri] 38 | %i[patch changePassword sort etag filter bulk].each do |key| 39 | x.send("#{key.to_s.underscore}=", Supportable.from(hash[key])) 40 | end 41 | schemes = hash[:authenticationSchemes] 42 | x.authentication_schemes = schemes&.map do |auth| 43 | AuthenticationScheme.from(auth) 44 | end 45 | x 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/resource.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents a SCIM Resource 7 | class Resource 8 | include ::ActiveModel::Validations 9 | include Attributable 10 | include Templatable 11 | 12 | attr_reader :meta 13 | attr_reader :schemas 14 | attr_reader :raw_attributes 15 | 16 | validate :schema_validations 17 | 18 | def initialize(schemas:, location: nil, attributes: {}) 19 | @meta = Meta.new(schemas[0]&.name, location) 20 | @meta.disable_timestamps 21 | @schemas = schemas 22 | @raw_attributes = attributes 23 | schemas.each { |x| define_attributes_for(self, x.attributes) } 24 | attribute(AttributeType.new(name: :id), self) 25 | attribute(AttributeType.new(name: :external_id), self) 26 | assign_attributes(attributes) 27 | yield self if block_given? 28 | end 29 | 30 | # Returns the current mode. 31 | # 32 | # @param type [Symbol] The mode `:server` or `:client`. 33 | # @return [Boolean] Returns true if the resource matches the # type of mode 34 | def mode?(type) 35 | case type.to_sym 36 | when :server 37 | meta&.location 38 | else 39 | meta&.location.nil? 40 | end 41 | end 42 | 43 | # Returns the name of the jbuilder template file. 44 | # @return [String] the name of the jbuilder template. 45 | def template_name 46 | 'resource.json.jbuilder' 47 | end 48 | 49 | private 50 | 51 | def schema_validations 52 | schemas.each do |schema| 53 | schema.attributes.each do |type| 54 | validate_attribute(type) 55 | end 56 | end 57 | end 58 | 59 | def validate_attribute(type) 60 | attribute = attribute_for(type.name) 61 | errors.merge!(attribute.errors) unless attribute.valid? 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents a SCIM Schema 7 | class Schema 8 | include Templatable 9 | 10 | attr_reader :id, :name, :attributes 11 | attr_accessor :meta, :description 12 | 13 | def initialize(id:, name:, location:) 14 | @id = id 15 | @name = name 16 | @description = name 17 | @meta = Meta.new('Schema', location) 18 | @meta.created = @meta.last_modified = @meta.version = nil 19 | @attributes = [] 20 | yield self if block_given? 21 | end 22 | 23 | def add_attribute(name:, type: :string) 24 | attribute = AttributeType.new(name: name, type: type, schema: self) 25 | yield attribute if block_given? 26 | attributes << attribute 27 | end 28 | 29 | def core? 30 | id.include?(Schemas::CORE) || id.include?(Messages::CORE) 31 | end 32 | 33 | class << self 34 | def build(**args) 35 | item = new(**args) 36 | yield item 37 | item 38 | end 39 | 40 | def from(hash) 41 | Schema.new( 42 | id: hash[:id], 43 | name: hash[:name], 44 | location: hash[:location] 45 | ) do |x| 46 | x.meta = Meta.from(hash[:meta]) 47 | hash[:attributes].each do |y| 48 | x.attributes << parse_attribute_type(y) 49 | end 50 | end 51 | end 52 | 53 | def parse(json) 54 | from(JSON.parse(json, symbolize_names: true)) 55 | end 56 | 57 | private 58 | 59 | def parse_attribute_type(hash) 60 | attribute_type = AttributeType.from(hash) 61 | hash[:subAttributes]&.each do |sub_attr_hash| 62 | attribute_type.attributes << parse_attribute_type(sub_attr_hash) 63 | end 64 | attribute_type 65 | end 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/authentication_scheme.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents the available Authentication Schemes. 7 | class AuthenticationScheme 8 | DEFAULTS = { 9 | httpbasic: { 10 | description: 'Authentication scheme using the HTTP Basic Standard', 11 | documentation_uri: 'http://example.com/help/httpBasic.html', 12 | name: 'HTTP Basic', 13 | spec_uri: 'http://www.rfc-editor.org/info/rfc2617' 14 | }, 15 | oauthbearertoken: { 16 | description: 17 | 'Authentication scheme using the OAuth Bearer Token Standard', 18 | documentation_uri: 'http://example.com/help/oauth.html', 19 | name: 'OAuth Bearer Token', 20 | spec_uri: 'http://www.rfc-editor.org/info/rfc6750' 21 | } 22 | }.freeze 23 | include Templatable 24 | attr_accessor :name 25 | attr_accessor :description 26 | attr_accessor :documentation_uri 27 | attr_accessor :spec_uri 28 | attr_accessor :type 29 | attr_accessor :primary 30 | 31 | def initialize 32 | yield self if block_given? 33 | end 34 | 35 | class << self 36 | def build_for(type, primary: nil) 37 | defaults = DEFAULTS[type.to_sym] || {} 38 | new do |x| 39 | x.type = type 40 | x.primary = primary 41 | x.description = defaults[:description] 42 | x.documentation_uri = defaults[:documentation_uri] 43 | x.name = defaults[:name] 44 | x.spec_uri = defaults[:spec_uri] 45 | end 46 | end 47 | 48 | def from(hash) 49 | x = build_for(hash[:type], primary: hash[:primary]) 50 | x.description = hash[:description] 51 | x.documentation_uri = hash[:documentationUri] 52 | x.name = hash[:name] 53 | x.spec_uri = hash[:specUri] 54 | x 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/scim/kit/v2/resource_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit::V2::ResourceType do 4 | subject do 5 | described_class.build(location: location) do |x| 6 | x.id = 'Group' 7 | x.description = 'Group' 8 | x.endpoint = 'https://www.example.org/scim/v2/groups' 9 | x.name = 'Group' 10 | x.schema = Scim::Kit::V2::Schemas::GROUP 11 | end 12 | end 13 | 14 | let(:location) { FFaker::Internet.uri('https') } 15 | 16 | specify { expect(subject.to_h[:meta][:location]).to eql(location) } 17 | specify { expect(subject.to_h[:meta][:resourceType]).to eql('ResourceType') } 18 | specify { expect(subject.to_h[:schemas]).to match_array([Scim::Kit::V2::Schemas::RESOURCE_TYPE]) } 19 | specify { expect(subject.to_h[:id]).to eql('Group') } 20 | specify { expect(subject.to_h[:description]).to eql(subject.description) } 21 | specify { expect(subject.to_h[:endpoint]).to eql(subject.endpoint) } 22 | specify { expect(subject.to_h[:name]).to eql(subject.name) } 23 | specify { expect(subject.to_h[:schema]).to eql(subject.schema) } 24 | specify { expect(subject.to_h[:schemaExtensions]).to match_array([]) } 25 | 26 | context 'with a schema extension' do 27 | let(:extension) { 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' } 28 | 29 | before { subject.add_schema_extension(schema: extension, required: false) } 30 | 31 | specify { expect(subject.to_h[:schemaExtensions]).to match_array([{ schema: extension, required: false }]) } 32 | end 33 | 34 | describe '.parse' do 35 | let(:extension) { 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' } 36 | let(:result) { described_class.parse(subject.to_json) } 37 | 38 | before { subject.add_schema_extension(schema: extension, required: false) } 39 | 40 | specify { expect(result.id).to eql(subject.id) } 41 | specify { expect(result.name).to eql(subject.name) } 42 | specify { expect(result.description).to eql(subject.description) } 43 | specify { expect(result.endpoint).to eql(subject.endpoint) } 44 | specify { expect(result.schema).to eql(subject.schema) } 45 | specify { expect(result.schema_extensions).to eql(subject.schema_extensions) } 46 | specify { expect(result.to_h).to eql(subject.to_h) } 47 | specify { expect(result.to_json).to eql(subject.to_json) } 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop/cop/internal_affairs 3 | - rubocop-rspec 4 | AllCops: 5 | Exclude: 6 | - 'coverage/**/*' 7 | - 'pkg/**/*' 8 | - 'tmp/**/*' 9 | - 'vendor/**/*' 10 | TargetRubyVersion: 3.2 11 | 12 | Layout/ArgumentAlignment: 13 | EnforcedStyle: with_fixed_indentation 14 | 15 | Layout/EndOfLine: 16 | EnforcedStyle: lf 17 | 18 | Layout/FirstArrayElementIndentation: 19 | EnforcedStyle: consistent 20 | 21 | Layout/LineLength: 22 | Exclude: 23 | - 'spec/**/*.rb' 24 | IgnoredPatterns: 25 | - '^#*' 26 | Max: 80 27 | 28 | Layout/MultilineMethodCallIndentation: 29 | Enabled: true 30 | EnforcedStyle: indented 31 | 32 | Layout/ParameterAlignment: 33 | Enabled: true 34 | EnforcedStyle: with_fixed_indentation 35 | IndentationWidth: 2 36 | 37 | Lint/AmbiguousBlockAssociation: 38 | Exclude: 39 | - 'spec/**/*.rb' 40 | 41 | Lint/EmptyFile: 42 | Exclude: 43 | - 'lib/scim/kit/v2/templates/nil_class.json.jbuilder' 44 | 45 | Lint/RaiseException: 46 | Enabled: true 47 | 48 | Lint/StructNewOverride: 49 | Enabled: true 50 | 51 | Metrics/AbcSize: 52 | Exclude: 53 | - 'lib/scim/kit/v2/service_provider_configuration.rb' 54 | 55 | Metrics/BlockLength: 56 | Exclude: 57 | - '*.gemspec' 58 | - 'Rakefile' 59 | - 'spec/**/*.rb' 60 | 61 | Metrics/ModuleLength: 62 | Exclude: 63 | - 'spec/**/*.rb' 64 | 65 | Naming/FileName: 66 | Exclude: 67 | - 'lib/scim-kit.rb' 68 | 69 | Naming/RescuedExceptionsVariableName: 70 | PreferredName: error 71 | 72 | Style/AccessorGrouping: 73 | Enabled: false 74 | 75 | Style/Documentation: 76 | Enabled: false 77 | 78 | Style/HashEachMethods: 79 | Enabled: true 80 | 81 | Style/HashTransformKeys: 82 | Enabled: true 83 | 84 | Style/HashTransformValues: 85 | Enabled: true 86 | 87 | Style/IfUnlessModifier: 88 | Exclude: 89 | - 'lib/scim/kit/v2/attribute.rb' 90 | 91 | Style/StringLiterals: 92 | EnforcedStyle: 'single_quotes' 93 | 94 | Style/SymbolProc: 95 | Enabled: false 96 | 97 | Style/TrailingCommaInArrayLiteral: 98 | Enabled: false 99 | 100 | Style/TrailingCommaInHashLiteral: 101 | Enabled: false 102 | 103 | RSpec/FilePath: 104 | Enabled: false 105 | 106 | RSpec/MultipleMemoizedHelpers: 107 | Enabled: false 108 | 109 | RSpec/NamedSubject: 110 | Enabled: false 111 | 112 | RSpec/NestedGroups: 113 | Max: 4 114 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents an application SCIM configuration. 7 | class Configuration 8 | # @private 9 | class Builder 10 | attr_reader :configuration 11 | 12 | def initialize(configuration) 13 | @configuration = configuration 14 | end 15 | 16 | def service_provider_configuration(location:) 17 | configuration.service_provider_configuration = 18 | ServiceProviderConfiguration.new(location: location) 19 | yield configuration.service_provider_configuration 20 | end 21 | 22 | def resource_type(id:, location:) 23 | configuration.resource_types[id] ||= 24 | ResourceType.new(location: location) 25 | configuration.resource_types[id].id = id 26 | yield configuration.resource_types[id] 27 | end 28 | 29 | def schema(id:, name:, location:) 30 | configuration.schemas[id] ||= Schema.new( 31 | id: id, 32 | name: name, 33 | location: location 34 | ) 35 | yield configuration.schemas[id] 36 | end 37 | end 38 | 39 | attr_accessor :service_provider_configuration 40 | attr_accessor :resource_types 41 | attr_accessor :schemas 42 | 43 | def initialize(http: Scim::Kit::Http.new) 44 | @http = http 45 | @resource_types = {} 46 | @schemas = {} 47 | 48 | yield Builder.new(self) if block_given? 49 | end 50 | 51 | def load_from(base_url) 52 | base_url = "#{base_url}/" 53 | uri = URI.join(base_url, 'ServiceProviderConfig') 54 | json = http.get(uri) 55 | 56 | self.service_provider_configuration = ServiceProviderConfiguration.parse(json, json) 57 | 58 | load_items(base_url, 'Schemas', Schema, schemas) 59 | load_items(base_url, 'ResourceTypes', ResourceType, resource_types) 60 | end 61 | 62 | private 63 | 64 | attr_reader :http 65 | 66 | def load_items(base_url, path, type, items) 67 | hashes = http.get(URI.join(base_url, path)) 68 | hashes.each do |hash| 69 | item = type.from(hash) 70 | items[item.id] = item 71 | end 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/filter/visitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'parslet' 4 | 5 | module Scim 6 | module Kit 7 | module V2 8 | class Filter 9 | # @private 10 | class Visitor 11 | VISITORS = { 12 | and: :visit_and, 13 | co: :visit_contains, 14 | eq: :visit_equals, 15 | ew: :visit_ends_with, 16 | ge: :visit_greater_than_equals, 17 | gt: :visit_greater_than, 18 | le: :visit_less_than_equals, 19 | lt: :visit_less_than, 20 | ne: :visit_not_equals, 21 | or: :visit_or, 22 | pr: :visit_presence, 23 | sw: :visit_starts_with 24 | }.freeze 25 | 26 | def visit(node) 27 | visitor_for(node).call(node) 28 | end 29 | 30 | protected 31 | 32 | def visitor_for(node) 33 | method(VISITORS.fetch(node.operator, :visit_unknown)) 34 | end 35 | 36 | def visit_and(node) 37 | visit(node.left).merge(visit(node.right)) 38 | raise error_for(:visit_and) 39 | end 40 | 41 | def visit_or(_node) 42 | raise error_for(:visit_or) 43 | end 44 | 45 | def visit_equals(_node) 46 | raise error_for(:visit_equals) 47 | end 48 | 49 | def visit_not_equals(_node) 50 | raise error_for(:visit_not_equals) 51 | end 52 | 53 | def visit_contains(_node) 54 | raise error_for(:visit_contains) 55 | end 56 | 57 | def visit_starts_with(_node) 58 | raise error_for(:visit_starts_with) 59 | end 60 | 61 | def visit_ends_with(_node) 62 | raise error_for(:visit_ends_with) 63 | end 64 | 65 | def visit_greater_than(_node) 66 | raise error_for(:visit_greater_than) 67 | end 68 | 69 | def visit_greater_than_equals(_node) 70 | raise error_for(:visit_greater_than_equals) 71 | end 72 | 73 | def visit_less_than(_node) 74 | raise error_for(:visit_less_than) 75 | end 76 | 77 | def visit_less_than_equals(_node) 78 | raise error_for(:visit_less_than_equals) 79 | end 80 | 81 | def visit_presence(_node) 82 | raise error_for(:visit_presence) 83 | end 84 | 85 | def visit_unknown(_node) 86 | raise error_for(:visit_unknown) 87 | end 88 | 89 | private 90 | 91 | def error_for(method) 92 | ::Scim::Kit::NotImplementedError.new("#{method} is not implemented") 93 | end 94 | end 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/scim/kit/v2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'scim/kit/v2/attributable' 4 | require 'scim/kit/v2/attribute' 5 | require 'scim/kit/v2/attribute_type' 6 | require 'scim/kit/v2/authentication_scheme' 7 | require 'scim/kit/v2/complex_attribute_validator' 8 | require 'scim/kit/v2/configuration' 9 | require 'scim/kit/v2/messages' 10 | require 'scim/kit/v2/meta' 11 | require 'scim/kit/v2/mutability' 12 | require 'scim/kit/v2/resource' 13 | require 'scim/kit/v2/error' 14 | require 'scim/kit/v2/filter' 15 | require 'scim/kit/v2/filter/node' 16 | require 'scim/kit/v2/filter/visitor' 17 | require 'scim/kit/v2/resource_type' 18 | require 'scim/kit/v2/returned' 19 | require 'scim/kit/v2/schema' 20 | require 'scim/kit/v2/schemas' 21 | require 'scim/kit/v2/service_provider_configuration' 22 | require 'scim/kit/v2/supportable' 23 | require 'scim/kit/v2/uniqueness' 24 | require 'scim/kit/v2/unknown_attribute' 25 | 26 | module Scim 27 | module Kit 28 | # Version 2 of the SCIM RFC https://tools.ietf.org/html/rfc7644 29 | module V2 30 | BASE64 = %r( 31 | \A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z 32 | )x 33 | BOOLEAN_VALUES = [true, false].freeze 34 | DATATYPES = { 35 | string: 'string', 36 | boolean: 'boolean', 37 | decimal: 'decimal', 38 | integer: 'integer', 39 | datetime: 'dateTime', 40 | binary: 'binary', 41 | reference: 'reference', 42 | complex: 'complex' 43 | }.freeze 44 | COERCION = { 45 | binary: lambda { |x| 46 | VALIDATIONS[:binary].call(x) ? x : Base64.strict_encode64(x) 47 | }, 48 | boolean: lambda { |x| 49 | return true if x == 'true' 50 | return false if x == 'false' 51 | 52 | x 53 | }, 54 | datetime: ->(x) { x.is_a?(::String) ? DateTime.parse(x) : x }, 55 | decimal: ->(x) { x.to_f }, 56 | integer: ->(x) { x.to_i }, 57 | string: ->(x) { x.to_s } 58 | }.freeze 59 | VALIDATIONS = { 60 | binary: ->(x) { x.is_a?(String) && x.match?(BASE64) }, 61 | boolean: ->(x) { BOOLEAN_VALUES.include?(x) }, 62 | datetime: ->(x) { x.is_a?(DateTime) }, 63 | decimal: ->(x) { x.is_a?(Float) }, 64 | integer: lambda { |x| 65 | begin 66 | x&.integer? 67 | rescue StandardError 68 | false 69 | end 70 | }, 71 | reference: ->(x) { x&.to_s =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/ }, 72 | string: ->(x) { x.is_a?(String) } 73 | }.freeze 74 | 75 | class << self 76 | def configuration 77 | @configuration ||= ::Scim::Kit::V2::Configuration.new 78 | end 79 | 80 | def configure 81 | yield ::Scim::Kit::V2::Configuration::Builder.new(configuration) 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/scim/kit/v2/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit::V2::Configuration do 4 | subject do 5 | described_class.new do |x| 6 | x.service_provider_configuration(location: sp_location) do |y| 7 | y.add_authentication(:oauthbearertoken) 8 | y.change_password.supported = true 9 | end 10 | x.resource_type(id: 'User', location: user_type_location) do |y| 11 | y.schema = Scim::Kit::V2::Schemas::USER 12 | end 13 | x.resource_type(id: 'Group', location: group_type_location) do |y| 14 | y.schema = Scim::Kit::V2::Schemas::GROUP 15 | end 16 | x.schema(id: 'User', name: 'User', location: user_schema_location) do |y| 17 | y.add_attribute(name: 'userName') 18 | end 19 | end 20 | end 21 | 22 | let(:sp_location) { FFaker::Internet.uri('https') } 23 | let(:user_type_location) { FFaker::Internet.uri('https') } 24 | let(:group_type_location) { FFaker::Internet.uri('https') } 25 | let(:user_schema_location) { FFaker::Internet.uri('https') } 26 | 27 | specify { expect(subject.service_provider_configuration.meta.location).to eql(sp_location) } 28 | specify { expect(subject.service_provider_configuration.authentication_schemes[0].type).to be(:oauthbearertoken) } 29 | specify { expect(subject.service_provider_configuration.change_password.supported).to be(true) } 30 | 31 | specify { expect(subject.resource_types['User'].schema).to eql(Scim::Kit::V2::Schemas::USER) } 32 | specify { expect(subject.resource_types['User'].id).to eql('User') } 33 | specify { expect(subject.resource_types['Group'].schema).to eql(Scim::Kit::V2::Schemas::GROUP) } 34 | specify { expect(subject.resource_types['Group'].id).to eql('Group') } 35 | 36 | specify { expect(subject.schemas['User'].id).to eql('User') } 37 | specify { expect(subject.schemas['User'].name).to eql('User') } 38 | specify { expect(subject.schemas['User'].meta.location).to eql(user_schema_location) } 39 | specify { expect(subject.schemas['User'].attributes[0].name).to eql('user_name') } 40 | 41 | describe '#load_from' do 42 | let(:base_url) { FFaker::Internet.uri('https') } 43 | let(:service_provider_configuration) do 44 | Scim::Kit::V2::ServiceProviderConfiguration.new(location: FFaker::Internet.uri('https')) 45 | end 46 | let(:schema) do 47 | Scim::Kit::V2::Schema.new(id: 'User', name: 'User', location: FFaker::Internet.uri('https')) 48 | end 49 | let(:resource_type) do 50 | x = Scim::Kit::V2::ResourceType.new(location: FFaker::Internet.uri('https')) 51 | x.id = 'User' 52 | x 53 | end 54 | 55 | before do 56 | stub_request(:get, "#{base_url}/ServiceProviderConfig") 57 | .to_return(status: 200, body: service_provider_configuration.to_json) 58 | 59 | stub_request(:get, "#{base_url}/Schemas") 60 | .to_return(status: 200, body: [schema.to_h].to_json) 61 | 62 | stub_request(:get, "#{base_url}/ResourceTypes") 63 | .to_return(status: 200, body: [resource_type.to_h].to_json) 64 | 65 | subject.load_from(base_url) 66 | end 67 | 68 | specify { expect(subject.service_provider_configuration.to_h).to eql(service_provider_configuration.to_h) } 69 | specify { expect(subject.schemas[schema.id].to_h).to eql(schema.to_h) } 70 | specify { expect(subject.resource_types[resource_type.id].to_h).to eql(resource_type.to_h) } 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scim::Kit 2 | 3 | [![Build Status](https://github.com/xlgmokha/scim-kit/workflows/ci/badge.svg)](https://github.com/xlgmokha/scim-kit/actions) 4 | [![Gem Version](https://badge.fury.io/rb/scim-kit.svg)](https://rubygems.org/gems/scim-kit) 5 | 6 | Scim::Kit is a library with the purpose of simplifying the generation 7 | and consumption of SCIM Schema. https://tools.ietf.org/html/rfc7643#section-2 8 | 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ```ruby 15 | gem 'scim-kit' 16 | ``` 17 | 18 | And then execute: 19 | 20 | $ bundle 21 | 22 | Or install it yourself as: 23 | 24 | $ gem install scim-kit 25 | 26 | ## Usage 27 | 28 | ```ruby 29 | def user_schema 30 | Scim::Kit::V2::Schema.build( 31 | id: Scim::Kit::V2::Schemas::USER, 32 | name: "User", 33 | location: scim_v2_schema_url(id: Scim::Kit::V2::Schemas::USER) 34 | ) do |schema| 35 | schema.description = "User Account" 36 | schema.add_attribute(name: 'userName') do |x| 37 | x.description = "Unique identifier for the User" 38 | x.required = true 39 | x.uniqueness = :server 40 | end 41 | schema.add_attribute(name: 'password') do |x| 42 | x.description = "The User's cleartext password." 43 | x.mutability = :write_only 44 | x.required = false 45 | x.returned = :never 46 | end 47 | schema.add_attribute(name: 'emails') do |x| 48 | x.multi_valued = true 49 | x.description = "Email addresses for the user." 50 | x.add_attribute(name: 'value') do |y| 51 | y.description = "Email addresses for the user." 52 | end 53 | x.add_attribute(name: 'primary', type: :boolean) do |y| 54 | y.description = "A Boolean value indicating the preferred email" 55 | end 56 | end 57 | schema.add_attribute(name: 'groups') do |x| 58 | x.multi_valued = true 59 | x.description = "A list of groups to which the user belongs." 60 | x.mutability = :read_only 61 | x.add_attribute(name: 'value') do |y| 62 | y.description = "The identifier of the User's group." 63 | y.mutability = :read_only 64 | end 65 | x.add_attribute(name: '$ref', type: :reference) do |y| 66 | y.reference_types = ['User', 'Group'] 67 | y.description = "The URI of the corresponding 'Group' resource." 68 | y.mutability = :read_only 69 | end 70 | x.add_attribute(name: 'display') do |y| 71 | y.description = "A human-readable name." 72 | y.mutability = :read_only 73 | end 74 | end 75 | end 76 | end 77 | 78 | puts user_schema.to_json 79 | ``` 80 | 81 | ## Development 82 | 83 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 84 | 85 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 86 | 87 | ## Contributing 88 | 89 | Bug reports and pull requests are welcome on GitHub at https://github.com/xlgmokha/scim-kit. 90 | 91 | ## License 92 | 93 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 94 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents a SCIM Attribute 7 | class Attribute 8 | include ::ActiveModel::Validations 9 | include Attributable 10 | include Templatable 11 | attr_reader :_type 12 | attr_reader :_resource 13 | attr_reader :_value 14 | 15 | validate :presence_of_value, if: proc { |x| x._type.required } 16 | validate :inclusion_of_value, if: proc { |x| x._type.canonical_values } 17 | validate :validate_type, unless: proc { |x| x.complex? } 18 | validate :validate_complex, if: proc { |x| x.complex? } 19 | validate :multiple, if: proc { |x| x.multi_valued && !x.complex? } 20 | 21 | delegate :complex?, :multi_valued, to: :_type 22 | 23 | def initialize(resource:, type:, value: nil) 24 | @_type = type 25 | @_value = value || type.multi_valued ? [] : nil 26 | @_resource = resource 27 | 28 | define_attributes_for(resource, type.attributes) 29 | end 30 | 31 | def _assign(new_value, coerce: false) 32 | @_value = coerce ? _type.coerce(new_value) : new_value 33 | end 34 | 35 | def _value=(new_value) 36 | _assign(new_value, coerce: true) 37 | end 38 | 39 | def renderable? 40 | return false if server_only? 41 | return false if client_only? 42 | return false if restricted? 43 | 44 | true 45 | end 46 | 47 | def each_value(&block) 48 | Array(_value).each(&block) 49 | end 50 | 51 | private 52 | 53 | def server_only? 54 | read_only? && _resource.mode?(:client) 55 | end 56 | 57 | def client_only? 58 | write_only? && (_resource.mode?(:server) || _value.blank?) 59 | end 60 | 61 | def restricted? 62 | _resource.mode?(:server) && _type.returned == Returned::NEVER 63 | end 64 | 65 | def presence_of_value 66 | return unless _type.required && _value.blank? 67 | 68 | errors.add(_type.name, I18n.t('errors.messages.blank')) 69 | end 70 | 71 | def inclusion_of_value 72 | return if _type.canonical_values.include?(_value) 73 | 74 | errors.add(_type.name, I18n.t('errors.messages.inclusion')) 75 | end 76 | 77 | def validate_type 78 | return if _value.nil? 79 | return if _type.valid?(_value) 80 | 81 | errors.add(_type.name, I18n.t('errors.messages.invalid')) 82 | end 83 | 84 | def validate_complex 85 | validates_with ComplexAttributeValidator 86 | end 87 | 88 | def multiple 89 | return unless _value.respond_to?(:to_a) 90 | 91 | duped_type = _type.dup 92 | duped_type.multi_valued = false 93 | _value.to_a.each do |x| 94 | unless duped_type.valid?(x) 95 | errors.add(duped_type.name, I18n.t('errors.messages.invalid')) 96 | end 97 | end 98 | end 99 | 100 | def read_only? 101 | _type.mutability == Mutability::READ_ONLY 102 | end 103 | 104 | def write_only? 105 | _type.mutability == Mutability::WRITE_ONLY 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/attributable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents a dynamic attribute that corresponds to a SCIM type 7 | module Attributable 8 | include Enumerable 9 | 10 | # Returns a hash of the generated dynamic attributes 11 | # @return [Hash] the dynamic attributes keys by their name 12 | def dynamic_attributes 13 | @dynamic_attributes ||= {}.with_indifferent_access 14 | end 15 | 16 | # Defines dynamic attributes on the resource for the types provided 17 | # @param resource [Scim::Kit::V2::Resource] the resource to attach dynamic attributes to. 18 | # @param types [Array] the array of types 19 | def define_attributes_for(resource, types) 20 | types.each { |x| attribute(x, resource) } 21 | end 22 | 23 | # Assigns attribute values via the provided hash. 24 | # @param attributes [Hash] The name/values to assign. 25 | def assign_attributes(attributes = {}) 26 | attributes.each do |key, value| 27 | next if key.to_sym == :schemas 28 | 29 | if key.to_s.start_with?(Schemas::EXTENSION) 30 | assign_attributes(value) 31 | else 32 | write_attribute(key, value) 33 | end 34 | end 35 | end 36 | 37 | # Returns the attribute identified by the name. 38 | # @param name [String] the name of the attribute to return 39 | # @return [Scim::Kit::V2::Attribute] the attribute or {Scim::Kit::V2::UnknownAttribute} 40 | def attribute_for(name) 41 | dynamic_attributes[name.to_s.underscore] || 42 | dynamic_attributes[name] || 43 | UnknownAttribute.new(name) 44 | end 45 | 46 | # Returns the value associated with the attribute name 47 | # @param name [String] the name of the attribute 48 | # @return [Object] the value assigned to the attribute 49 | def read_attribute(name) 50 | attribute = attribute_for(name) 51 | return attribute._value if attribute._type.multi_valued 52 | 53 | attribute._type.complex? ? attribute : attribute._value 54 | end 55 | 56 | # Assigns the value to the attribute with the given name 57 | # @param name [String] the name of the attribute 58 | # @param value [Object] the value to assign to the attribute 59 | def write_attribute(name, value) 60 | if value.is_a?(Hash) 61 | attribute_for(name)&.assign_attributes(value) 62 | else 63 | attribute_for(name)._value = value 64 | end 65 | end 66 | 67 | # yields each attribute to the provided block 68 | # @param [Block] the block to yield each attribute to. 69 | def each(&block) 70 | dynamic_attributes.each_value(&block) 71 | end 72 | 73 | private 74 | 75 | def create_module_for(type) 76 | name = type.name.to_sym 77 | Module.new do 78 | define_method(name) do |*_args| 79 | read_attribute(name) 80 | end 81 | 82 | define_method("#{name}=") do |*args| 83 | write_attribute(name, args[0]) 84 | end 85 | end 86 | end 87 | 88 | def attribute(type, resource) 89 | previously_defined = dynamic_attributes.key?(type.name) 90 | dynamic_attributes[previously_defined ? type.fully_qualified_name : type.name] = 91 | Attribute.new(type: type, resource: resource) 92 | extend(create_module_for(type)) unless previously_defined 93 | end 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/attribute_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scim 4 | module Kit 5 | module V2 6 | # Represents a scim Attribute type 7 | class AttributeType 8 | include Templatable 9 | attr_accessor :canonical_values, :case_exact, :description 10 | attr_accessor :multi_valued, :required 11 | attr_reader :mutability, :name, :fully_qualified_name, :type, :attributes 12 | attr_reader :reference_types, :returned, :uniqueness 13 | 14 | def initialize(name:, type: :string, schema: nil) 15 | @name = name.to_s.underscore 16 | @fully_qualified_name = [schema&.id, @name].compact.join('#') 17 | @type = DATATYPES[type.to_sym] ? type.to_sym : (raise TYPE_ERROR) 18 | @description = name.to_s.camelize(:lower) 19 | @multi_valued = @required = @case_exact = false 20 | @mutability = Mutability::READ_WRITE 21 | @returned = Returned::DEFAULT 22 | @uniqueness = Uniqueness::NONE 23 | @attributes = [] 24 | end 25 | 26 | def mutability=(value) 27 | @mutability = Mutability.find(value) 28 | end 29 | 30 | def returned=(value) 31 | @returned = Returned.find(value) 32 | end 33 | 34 | def uniqueness=(value) 35 | @uniqueness = Uniqueness.find(value) 36 | end 37 | 38 | def add_attribute(name:, type: :string) 39 | attribute = AttributeType.new(name: name, type: type) 40 | yield attribute if block_given? 41 | @type = :complex 42 | attributes << attribute 43 | end 44 | 45 | def reference_types=(value) 46 | @type = :reference 47 | @reference_types = value 48 | end 49 | 50 | def complex? 51 | type_is?(:complex) 52 | end 53 | 54 | def coerce(value) 55 | return value if value.nil? 56 | return value if complex? 57 | 58 | if multi_valued 59 | return value unless value.respond_to?(:to_a) 60 | 61 | value.to_a.map { |x| coerce_single(x) } 62 | else 63 | coerce_single(value) 64 | end 65 | end 66 | 67 | def valid?(value) 68 | if multi_valued 69 | return false unless value.respond_to?(:to_a) 70 | 71 | return value.to_a.all? { |x| validate(x) } 72 | end 73 | 74 | complex? ? valid_complex?(value) : valid_simple?(value) 75 | end 76 | 77 | class << self 78 | def from(hash) 79 | x = new(name: hash[:name], type: hash[:type]) 80 | %i[ 81 | canonicalValues caseExact description multiValued mutability 82 | referenceTypes required returned uniqueness 83 | ].each do |y| 84 | x.public_send("#{y.to_s.underscore}=", hash[y]) if hash.key?(y) 85 | end 86 | x 87 | end 88 | end 89 | 90 | private 91 | 92 | def coerce_single(value) 93 | COERCION.fetch(type, ->(x) { x }).call(value) 94 | rescue StandardError => error 95 | Scim::Kit.logger.error(error) 96 | value 97 | end 98 | 99 | def validate(value) 100 | complex? ? valid_complex?(value) : valid_simple?(value) 101 | end 102 | 103 | def valid_simple?(value) 104 | VALIDATIONS[type]&.call(value) 105 | end 106 | 107 | def valid_complex?(item) 108 | return false unless item.is_a?(Hash) 109 | 110 | item.each_key do |key| 111 | return false unless type_for(key)&.valid?(item[key]) 112 | end 113 | end 114 | 115 | def type_for(name) 116 | name = name.to_s.underscore 117 | attributes.find { |x| x.name.to_s.underscore == name } 118 | end 119 | 120 | def string? 121 | type_is?(:string) 122 | end 123 | 124 | def reference? 125 | type_is?(:reference) 126 | end 127 | 128 | def type_is?(expected_type) 129 | type.to_sym == expected_type 130 | end 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | scim-kit (0.7.2) 5 | activemodel (>= 6.1) 6 | net-hippie (~> 1.0) 7 | parslet (~> 2.0) 8 | tilt (~> 2.0) 9 | tilt-jbuilder (~> 0.7) 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | actionview (8.0.0) 15 | activesupport (= 8.0.0) 16 | builder (~> 3.1) 17 | erubi (~> 1.11) 18 | rails-dom-testing (~> 2.2) 19 | rails-html-sanitizer (~> 1.6) 20 | activemodel (8.0.0) 21 | activesupport (= 8.0.0) 22 | activesupport (8.0.0) 23 | base64 24 | benchmark (>= 0.3) 25 | bigdecimal 26 | concurrent-ruby (~> 1.0, >= 1.3.1) 27 | connection_pool (>= 2.2.5) 28 | drb 29 | i18n (>= 1.6, < 2) 30 | logger (>= 1.4.2) 31 | minitest (>= 5.1) 32 | securerandom (>= 0.3) 33 | tzinfo (~> 2.0, >= 2.0.5) 34 | uri (>= 0.13.1) 35 | addressable (2.8.7) 36 | public_suffix (>= 2.0.2, < 7.0) 37 | ast (2.4.2) 38 | base64 (0.2.0) 39 | benchmark (0.4.0) 40 | bigdecimal (3.1.8) 41 | builder (3.3.0) 42 | bundler-audit (0.9.2) 43 | bundler (>= 1.2.0, < 3) 44 | thor (~> 1.0) 45 | concurrent-ruby (1.3.4) 46 | connection_pool (2.4.1) 47 | crack (1.0.0) 48 | bigdecimal 49 | rexml 50 | crass (1.0.6) 51 | diff-lcs (1.5.1) 52 | drb (2.2.1) 53 | erubi (1.13.0) 54 | ffaker (2.23.0) 55 | hashdiff (1.1.2) 56 | i18n (1.14.6) 57 | concurrent-ruby (~> 1.0) 58 | jbuilder (2.13.0) 59 | actionview (>= 5.0.0) 60 | activesupport (>= 5.0.0) 61 | json (2.9.0) 62 | language_server-protocol (3.17.0.3) 63 | logger (1.6.2) 64 | loofah (2.23.1) 65 | crass (~> 1.0.2) 66 | nokogiri (>= 1.12.0) 67 | minitest (5.25.4) 68 | net-hippie (1.2.0) 69 | logger (~> 1.0) 70 | nokogiri (1.16.8-aarch64-linux) 71 | racc (~> 1.4) 72 | nokogiri (1.16.8-arm-linux) 73 | racc (~> 1.4) 74 | nokogiri (1.16.8-arm64-darwin) 75 | racc (~> 1.4) 76 | nokogiri (1.16.8-x86-linux) 77 | racc (~> 1.4) 78 | nokogiri (1.16.8-x86_64-darwin) 79 | racc (~> 1.4) 80 | nokogiri (1.16.8-x86_64-linux) 81 | racc (~> 1.4) 82 | parallel (1.26.3) 83 | parser (3.3.6.0) 84 | ast (~> 2.4.1) 85 | racc 86 | parslet (2.0.0) 87 | public_suffix (6.0.1) 88 | racc (1.8.1) 89 | rails-dom-testing (2.2.0) 90 | activesupport (>= 5.0.0) 91 | minitest 92 | nokogiri (>= 1.6) 93 | rails-html-sanitizer (1.6.1) 94 | loofah (~> 2.21) 95 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 96 | rainbow (3.1.1) 97 | rake (13.2.1) 98 | regexp_parser (2.9.3) 99 | rexml (3.3.9) 100 | rspec (3.13.0) 101 | rspec-core (~> 3.13.0) 102 | rspec-expectations (~> 3.13.0) 103 | rspec-mocks (~> 3.13.0) 104 | rspec-core (3.13.2) 105 | rspec-support (~> 3.13.0) 106 | rspec-expectations (3.13.3) 107 | diff-lcs (>= 1.2.0, < 2.0) 108 | rspec-support (~> 3.13.0) 109 | rspec-mocks (3.13.2) 110 | diff-lcs (>= 1.2.0, < 2.0) 111 | rspec-support (~> 3.13.0) 112 | rspec-support (3.13.2) 113 | rubocop (1.69.1) 114 | json (~> 2.3) 115 | language_server-protocol (>= 3.17.0) 116 | parallel (~> 1.10) 117 | parser (>= 3.3.0.2) 118 | rainbow (>= 2.2.2, < 4.0) 119 | regexp_parser (>= 2.9.3, < 3.0) 120 | rubocop-ast (>= 1.36.2, < 2.0) 121 | ruby-progressbar (~> 1.7) 122 | unicode-display_width (>= 2.4.0, < 4.0) 123 | rubocop-ast (1.36.2) 124 | parser (>= 3.3.1.0) 125 | rubocop-capybara (2.21.0) 126 | rubocop (~> 1.41) 127 | rubocop-factory_bot (2.26.1) 128 | rubocop (~> 1.61) 129 | rubocop-rspec (2.31.0) 130 | rubocop (~> 1.40) 131 | rubocop-capybara (~> 2.17) 132 | rubocop-factory_bot (~> 2.22) 133 | rubocop-rspec_rails (~> 2.28) 134 | rubocop-rspec_rails (2.29.1) 135 | rubocop (~> 1.61) 136 | ruby-progressbar (1.13.0) 137 | securerandom (0.4.0) 138 | thor (1.3.2) 139 | tilt (2.4.0) 140 | tilt-jbuilder (0.7.1) 141 | jbuilder 142 | tilt (>= 1.3.0, < 3) 143 | tzinfo (2.0.6) 144 | concurrent-ruby (~> 1.0) 145 | unicode-display_width (3.1.2) 146 | unicode-emoji (~> 4.0, >= 4.0.4) 147 | unicode-emoji (4.0.4) 148 | uri (1.0.2) 149 | webmock (3.24.0) 150 | addressable (>= 2.8.0) 151 | crack (>= 0.3.2) 152 | hashdiff (>= 0.4.0, < 2.0.0) 153 | 154 | PLATFORMS 155 | aarch64-linux 156 | arm-linux 157 | arm64-darwin 158 | x86-linux 159 | x86_64-darwin 160 | x86_64-linux 161 | 162 | DEPENDENCIES 163 | bundler-audit (~> 0.6) 164 | ffaker (~> 2.7) 165 | rake (~> 13.0) 166 | rspec (~> 3.0) 167 | rubocop (~> 1.0) 168 | rubocop-rspec (~> 2.0) 169 | scim-kit! 170 | webmock (~> 3.5) 171 | 172 | BUNDLED WITH 173 | 2.5.23 174 | -------------------------------------------------------------------------------- /lib/scim/kit/v2/filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'parslet' 4 | 5 | module Scim 6 | module Kit 7 | module V2 8 | # Parses SCIM filter queries 9 | class Filter < Parslet::Parser 10 | root :filter 11 | 12 | # FILTER = attrExp / logExp / valuePath / assignVariable / *1"not" "(" FILTER ")" 13 | rule(:filter) do 14 | logical_expression | filter_atom 15 | end 16 | 17 | rule(:filter_atom) do 18 | (not_op? >> lparen >> filter >> rparen) | attribute_expression | value_path 19 | end 20 | 21 | # valuePath = attrPath "[" valFilter "]" ; FILTER uses sub-attributes of a parent attrPath 22 | rule(:value_path) do 23 | attribute_path >> lbracket >> value_filter >> rbracket 24 | end 25 | 26 | # valFilter = attrExp / logExp / *1"not" "(" valFilter ")" 27 | rule(:value_filter) do 28 | attribute_expression | logical_expression | not_op? >> lparen >> value_filter >> rparen 29 | end 30 | 31 | # attrExp = (attrPath SP "pr") / (attrPath SP compareOp SP compValue) 32 | rule(:attribute_expression) do 33 | (attribute_path >> space >> presence.as(:operator)) | (attribute_path >> space >> comparison_operator.as(:operator) >> space >> comparison_value.as(:value)) 34 | end 35 | 36 | # logExp = FILTER SP ("and" / "or") SP FILTER 37 | rule(:logical_expression) do 38 | filter_atom.as(:left) >> space >> (and_op | or_op).as(:operator) >> space >> filter.as(:right) 39 | end 40 | 41 | # compValue = false / null / true / number / string ; rules from JSON (RFC 7159) 42 | rule(:comparison_value) do 43 | falsey | null | truthy | number | string 44 | end 45 | 46 | # compareOp = "eq" / "ne" / "co" / "sw" / "ew" / "gt" / "lt" / "ge" / "le" 47 | rule(:comparison_operator) do 48 | equal | not_equal | contains | starts_with | ends_with | 49 | greater_than | less_than | less_than_equals | greater_than_equals 50 | end 51 | 52 | # attrPath = [URI ":"] ATTRNAME *1subAttr ; SCIM attribute name ; URI is SCIM "schema" URI 53 | rule(:attribute_path) do 54 | ((uri >> colon).repeat(0, 1) >> attribute_name >> sub_attribute.repeat(0, 1)).as(:attribute) 55 | end 56 | 57 | # ATTRNAME = ALPHA *(nameChar) 58 | rule(:attribute_name) do 59 | alpha >> name_character.repeat(0, nil) 60 | end 61 | 62 | # nameChar = "-" / "_" / DIGIT / ALPHA 63 | rule(:name_character) { hyphen | underscore | digit | alpha } 64 | 65 | # subAttr = "." ATTRNAME ; a sub-attribute of a complex attribute 66 | rule(:sub_attribute) { dot >> attribute_name } 67 | 68 | # uri = 1*ALPHA 1*(":" 1*ALPHA) 69 | rule(:uri) do 70 | # alpha.repeat(1, nil) >> (colon >> (alpha.repeat(1, nil) | version)).repeat(1, nil) 71 | str('urn:ietf:params:scim:schemas:') >> ( 72 | str('core:2.0:User') | 73 | str('core:2.0:Group') | ( 74 | str('extension') >> 75 | colon >> 76 | alpha.repeat(1) >> 77 | colon >> 78 | version >> 79 | colon >> 80 | alpha.repeat(1) 81 | ) 82 | ) 83 | end 84 | 85 | rule(:presence) { str('pr') } 86 | rule(:and_op) { str('and') } 87 | rule(:or_op) { str('or') } 88 | rule(:not_op?) { (str('not') >> space).repeat(0, 1).as(:not) } 89 | rule(:falsey) { str('false') } 90 | rule(:truthy) { str('true') } 91 | rule(:null) { str('null') } 92 | rule(:number) do 93 | str('-').maybe >> ( 94 | str('0') | (match('[1-9]') >> digit.repeat) 95 | ) >> ( 96 | str('.') >> digit.repeat(1) 97 | ).maybe >> ( 98 | match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1) 99 | ).maybe 100 | end 101 | rule(:equal) { str('eq') } 102 | rule(:not_equal) { str('ne') } 103 | rule(:contains) { str('co') } 104 | rule(:starts_with) { str('sw') } 105 | rule(:ends_with) { str('ew') } 106 | rule(:greater_than) { str('gt') } 107 | rule(:less_than) { str('lt') } 108 | rule(:greater_than_equals) { str('ge') } 109 | rule(:less_than_equals) { str('le') } 110 | rule(:string) do 111 | quote >> (str('\\') >> any | str('"').absent? >> any).repeat >> quote 112 | end 113 | rule(:lparen) { str('(') } 114 | rule(:rparen) { str(')') } 115 | rule(:lbracket) { str('[') } 116 | rule(:rbracket) { str(']') } 117 | rule(:digit) { match('\d') } 118 | rule(:quote) { str('"') } 119 | rule(:single_quote) { str("'") } 120 | rule(:space) { match('\s') } 121 | rule(:alpha) { match['a-zA-Z'] } 122 | rule(:dot) { str('.') } 123 | rule(:colon) { str(':') } 124 | rule(:hyphen) { str('-') } 125 | rule(:underscore) { str('_') } 126 | rule(:version) { digit >> dot >> digit } 127 | rule(:assign) { str('=') } 128 | 129 | class << self 130 | def parse(filter) 131 | Node.new(new.parse(filter)) 132 | end 133 | end 134 | end 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Version 0.7.2 2 | 3 | # Changelog 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 7 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | ### Changed 11 | - Allow using activemodel version 8+ 12 | 13 | ## [0.7.2] - 2024-12-05 14 | ### Fixed 15 | - Change references to string during validation 16 | 17 | ## [0.7.1] - 2022-12-12 18 | ### Fixed 19 | - Add support for duplicate attribute names 20 | 21 | ## [0.7.0] - 2022-09-28 22 | ### Added 23 | - Add constant for 'urn:ietf:params:scim:api:messages:2.0:BulkRequest' [RFC-7644](https://www.rfc-editor.org/rfc/rfc7644.html#section-3.7) 24 | - Add constant for 'urn:ietf:params:scim:api:messages:2.0:BulkResponse' [RFC-7644](https://www.rfc-editor.org/rfc/rfc7644.html#section-3.7) 25 | - Add constant for 'urn:ietf:params:scim:api:messages:2.0:PatchOp' [RFC-7644](https://www.rfc-editor.org/rfc/rfc7644.html#section-3.5.2) 26 | - Add constant for 'urn:ietf:params:scim:schemas:core:2.0:Schema' [RFC-7643](https://www.rfc-editor.org/rfc/rfc7643.html#section-7) 27 | 28 | ## [0.6.0] - 2022-05-23 29 | ### Added 30 | - Add support for Ruby 3.1 31 | 32 | ### Removed 33 | 34 | - Drop support for Ruby 2.5 35 | - Drop support for Ruby 2.6 36 | 37 | ## [0.5.3] - 2022-05-13 38 | ### Fixed 39 | 40 | - fix: change `status` attribute to type string in [error schema](https://www.rfc-editor.org/rfc/rfc7644.html#section-3.12) 41 | - fix: remove duplicate `invalidSyntax` 42 | - fix: add mising `invalidFilter` 43 | 44 | ## [0.5.2] - 2020-05-20 45 | ### Fixed 46 | 47 | - fix: Parse sub attributes from schema https://github.com/xlgmokha/scim-kit/pull/38 48 | 49 | ## [0.5.1] - 2020-05-20 50 | ### Fixed 51 | - Specify `Accept: application/scim+json` header when discovering a SCIM API. 52 | - Specify `Content-Type: application/scim+json` header when discovering a SCIM API. 53 | - Specify `User-Agent: scim/kit ` header when discovering a SCIM API. 54 | - Follow HTTP redirects when discovering a SCIM API. 55 | - Retry 3 times with backoff + jitter when a connection to a SCIM discovery API fails. 56 | - Specify a 1 second open timeout. 57 | - Specify a 5 second read timeout. 58 | 59 | ## [0.5.0] - 2020-01-21 60 | ### Added 61 | - Add API to traverse a SCIM filter AST 62 | 63 | ## [0.4.0] - 2019-06-15 64 | ### Added 65 | - add implementation of SCIM 2.0 filter parser. [RFC-7644](https://tools.ietf.org/html/rfc7644#section-3.4.2.2) 66 | 67 | ## [0.3.2] - 2019-02-23 68 | ### Changed 69 | - camelize the default description of attribute names. 70 | 71 | ## [0.3.1] - 2019-02-23 72 | ### Changed 73 | - fix bug in `Scim::Kit::V2.configure` 74 | 75 | ## [0.3.0] - 2019-02-21 76 | ### Added 77 | - add ServiceProviderConfiguration JSON parsing 78 | - add Schema JSON parsing 79 | - add Resource Type JSON parsing 80 | 81 | ## [0.2.16] - 2019-02-03 82 | ### Added 83 | - Default logger 84 | - Attributes now implement Enumerable 85 | - Attributable#attribute\_for now returns a null object instead of nil. 86 | - Validations for multi valued attributes 87 | - Validations for complex attributes 88 | - rescue errors from type coercion. 89 | 90 | ### Changed 91 | - \_assign does not coerce values by default. 92 | - errors are merged together instead of overwritten during attribute validation. 93 | 94 | [Unreleased]: https://github.com/xlgmokha/scim-kit/compare/v0.7.2...HEAD 95 | [0.7.2]: https://github.com/xlgmokha/scim-kit/compare/v0.7.1...v0.7.2 96 | [0.7.1]: https://github.com/xlgmokha/scim-kit/compare/v0.7.0...v0.7.1 97 | [0.7.0]: https://github.com/xlgmokha/scim-kit/compare/v0.6.0...v0.7.0 98 | [0.6.0]: https://github.com/xlgmokha/scim-kit/compare/v0.5.3...v0.6.0 99 | [0.5.3]: https://github.com/xlgmokha/scim-kit/compare/v0.5.2...v0.5.3 100 | [0.5.2]: https://github.com/xlgmokha/scim-kit/compare/v0.5.1...v0.5.2 101 | [0.5.1]: https://github.com/xlgmokha/scim-kit/compare/v0.5.0...v0.5.1 102 | [0.5.0]: https://github.com/xlgmokha/scim-kit/compare/v0.4.0...v0.5.0 103 | [0.4.0]: https://github.com/xlgmokha/scim-kit/compare/v0.3.2...v0.4.0 104 | [0.3.2]: https://github.com/xlgmokha/scim-kit/compare/v0.3.1...v0.3.2 105 | [0.3.1]: https://github.com/xlgmokha/scim-kit/compare/v0.3.0...v0.3.1 106 | [0.3.0]: https://github.com/xlgmokha/scim-kit/compare/v0.2.16...v0.3.0 107 | [0.2.16]: https://github.com/xlgmokha/scim-kit/compare/v0.2.15...v0.2.16 108 | [0.2.15]: https://github.com/xlgmokha/scim-kit/compare/v0.2.14...v0.2.15 109 | [0.2.14]: https://github.com/xlgmokha/scim-kit/compare/v0.2.13...v0.2.14 110 | [0.2.13]: https://github.com/xlgmokha/scim-kit/compare/v0.2.12...v0.2.13 111 | [0.2.12]: https://github.com/xlgmokha/scim-kit/compare/v0.2.11...v0.2.12 112 | [0.2.11]: https://github.com/xlgmokha/scim-kit/compare/v0.2.10...v0.2.11 113 | [0.2.10]: https://github.com/xlgmokha/scim-kit/compare/v0.2.9...v0.2.10 114 | [0.2.9]: https://github.com/xlgmokha/scim-kit/compare/v0.2.8...v0.2.9 115 | [0.2.8]: https://github.com/xlgmokha/scim-kit/compare/v0.2.7...v0.2.8 116 | [0.2.7]: https://github.com/xlgmokha/scim-kit/compare/v0.2.6...v0.2.7 117 | [0.2.6]: https://github.com/xlgmokha/scim-kit/compare/v0.2.5...v0.2.6 118 | [0.2.5]: https://github.com/xlgmokha/scim-kit/compare/v0.2.4...v0.2.5 119 | [0.2.4]: https://github.com/xlgmokha/scim-kit/compare/v0.2.3...v0.2.4 120 | [0.2.3]: https://github.com/xlgmokha/scim-kit/compare/v0.2.2...v0.2.3 121 | [0.2.2]: https://github.com/xlgmokha/scim-kit/compare/v0.2.1...v0.2.2 122 | [0.2.1]: https://github.com/xlgmokha/scim-kit/compare/v0.2.0...v0.2.1 123 | [0.2.0]: https://github.com/xlgmokha/scim-kit/compare/v0.1.0...v0.2.0 124 | -------------------------------------------------------------------------------- /spec/scim/kit/v2/service_provider_configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit::V2::ServiceProviderConfiguration do 4 | subject { described_class.new(location: location) } 5 | 6 | let(:location) { FFaker::Internet.uri('https') } 7 | let(:now) { Time.now } 8 | 9 | describe '#to_json' do 10 | let(:result) { JSON.parse(subject.to_json, symbolize_names: true) } 11 | 12 | specify { expect(result[:schemas]).to match_array(['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig']) } 13 | specify { expect(result[:documentationUri]).to be_blank } 14 | specify { expect(result[:patch][:supported]).to be(false) } 15 | specify { expect(result[:bulk][:supported]).to be(false) } 16 | specify { expect(result[:filter][:supported]).to be(false) } 17 | specify { expect(result[:changePassword][:supported]).to be(false) } 18 | specify { expect(result[:sort][:supported]).to be(false) } 19 | specify { expect(result[:etag][:supported]).to be(false) } 20 | specify { expect(result[:authenticationSchemes]).to be_empty } 21 | specify { expect(result[:meta][:location]).to eql(location) } 22 | specify { expect(result[:meta][:resourceType]).to eql('ServiceProviderConfig') } 23 | specify { expect(result[:meta][:created]).to eql(now.iso8601) } 24 | specify { expect(result[:meta][:lastModified]).to eql(now.iso8601) } 25 | specify { expect(result[:meta][:version]).not_to be_nil } 26 | 27 | context 'with documentation uri' do 28 | before do 29 | subject.documentation_uri = FFaker::Internet.uri('https') 30 | end 31 | 32 | specify { expect(result[:documentationUri]).to eql(subject.documentation_uri) } 33 | end 34 | 35 | context 'with OAuth Bearer Token' do 36 | before { subject.add_authentication(:oauthbearertoken) } 37 | 38 | specify { expect(result[:authenticationSchemes][0][:name]).to eql('OAuth Bearer Token') } 39 | specify { expect(result[:authenticationSchemes][0][:description]).to eql('Authentication scheme using the OAuth Bearer Token Standard') } 40 | specify { expect(result[:authenticationSchemes][0][:specUri]).to eql('http://www.rfc-editor.org/info/rfc6750') } 41 | specify { expect(result[:authenticationSchemes][0][:documentationUri]).to eql('http://example.com/help/oauth.html') } 42 | specify { expect(result[:authenticationSchemes][0][:type]).to eql('oauthbearertoken') } 43 | end 44 | 45 | context 'with http basic' do 46 | before { subject.add_authentication(:httpbasic) } 47 | 48 | specify { expect(result[:authenticationSchemes][0][:name]).to eql('HTTP Basic') } 49 | specify { expect(result[:authenticationSchemes][0][:description]).to eql('Authentication scheme using the HTTP Basic Standard') } 50 | specify { expect(result[:authenticationSchemes][0][:specUri]).to eql('http://www.rfc-editor.org/info/rfc2617') } 51 | specify { expect(result[:authenticationSchemes][0][:documentationUri]).to eql('http://example.com/help/httpBasic.html') } 52 | specify { expect(result[:authenticationSchemes][0][:type]).to eql('httpbasic') } 53 | end 54 | 55 | context 'with multiple schemes' do 56 | before do 57 | subject.add_authentication(:oauthbearertoken, primary: true) 58 | subject.add_authentication(:httpbasic) 59 | end 60 | 61 | specify { expect(result[:authenticationSchemes][0][:name]).to eql('OAuth Bearer Token') } 62 | specify { expect(result[:authenticationSchemes][0][:description]).to eql('Authentication scheme using the OAuth Bearer Token Standard') } 63 | specify { expect(result[:authenticationSchemes][0][:specUri]).to eql('http://www.rfc-editor.org/info/rfc6750') } 64 | specify { expect(result[:authenticationSchemes][0][:documentationUri]).to eql('http://example.com/help/oauth.html') } 65 | specify { expect(result[:authenticationSchemes][0][:type]).to eql('oauthbearertoken') } 66 | specify { expect(result[:authenticationSchemes][0][:primary]).to be(true) } 67 | 68 | specify { expect(result[:authenticationSchemes][1][:name]).to eql('HTTP Basic') } 69 | specify { expect(result[:authenticationSchemes][1][:description]).to eql('Authentication scheme using the HTTP Basic Standard') } 70 | specify { expect(result[:authenticationSchemes][1][:specUri]).to eql('http://www.rfc-editor.org/info/rfc2617') } 71 | specify { expect(result[:authenticationSchemes][1][:documentationUri]).to eql('http://example.com/help/httpBasic.html') } 72 | specify { expect(result[:authenticationSchemes][1][:type]).to eql('httpbasic') } 73 | end 74 | 75 | context 'with custom scheme' do 76 | before do 77 | subject.add_authentication(:custom) do |x| 78 | x.name = 'custom' 79 | x.description = 'custom' 80 | x.spec_uri = 'http://www.rfc-editor.org/info/rfcXXXX' 81 | x.documentation_uri = 'http://example.com/help/custom.html' 82 | end 83 | end 84 | 85 | specify { expect(result[:authenticationSchemes][0][:name]).to eql('custom') } 86 | specify { expect(result[:authenticationSchemes][0][:description]).to eql('custom') } 87 | specify { expect(result[:authenticationSchemes][0][:specUri]).to eql('http://www.rfc-editor.org/info/rfcXXXX') } 88 | specify { expect(result[:authenticationSchemes][0][:documentationUri]).to eql('http://example.com/help/custom.html') } 89 | specify { expect(result[:authenticationSchemes][0][:type]).to eql('custom') } 90 | end 91 | 92 | context 'with etag support' do 93 | before { subject.etag.supported = true } 94 | 95 | specify { expect(result[:etag][:supported]).to be(true) } 96 | end 97 | 98 | context 'with sort support' do 99 | before { subject.sort.supported = true } 100 | 101 | specify { expect(result[:sort][:supported]).to be(true) } 102 | end 103 | 104 | context 'with change_password support' do 105 | before { subject.change_password.supported = true } 106 | 107 | specify { expect(result[:changePassword][:supported]).to be(true) } 108 | end 109 | 110 | context 'with patch support' do 111 | before { subject.patch.supported = true } 112 | 113 | specify { expect(result[:patch][:supported]).to be(true) } 114 | end 115 | 116 | context 'with bulk support' do 117 | before do 118 | subject.bulk.supported = true 119 | subject.bulk.max_operations = 1000 120 | subject.bulk.max_payload_size = 1_048_576 121 | end 122 | 123 | specify { expect(result[:bulk][:supported]).to be(true) } 124 | specify { expect(result[:bulk][:maxOperations]).to be(1000) } 125 | specify { expect(result[:bulk][:maxPayloadSize]).to be(1_048_576) } 126 | end 127 | 128 | context 'with filter support' do 129 | before do 130 | subject.filter.supported = true 131 | subject.filter.max_results = 200 132 | end 133 | 134 | specify { expect(result[:filter][:supported]).to be(true) } 135 | specify { expect(result[:filter][:maxResults]).to be(200) } 136 | end 137 | end 138 | 139 | describe '.parse' do 140 | let(:result) { described_class.parse(subject.to_json) } 141 | 142 | before do 143 | subject.add_authentication(:oauthbearertoken) 144 | subject.bulk.max_operations = 1000 145 | subject.bulk.max_payload_size = 1_048_576 146 | subject.bulk.supported = true 147 | subject.change_password.supported = true 148 | subject.documentation_uri = FFaker::Internet.uri('https') 149 | subject.etag.supported = true 150 | subject.filter.max_results = 200 151 | subject.filter.supported = true 152 | subject.patch.supported = true 153 | subject.sort.supported = true 154 | end 155 | 156 | specify { expect(result.meta.created.to_i).to eql(subject.meta.created.to_i) } 157 | specify { expect(result.meta.last_modified.to_i).to eql(subject.meta.last_modified.to_i) } 158 | specify { expect(result.meta.version).to eql(subject.meta.version) } 159 | specify { expect(result.meta.location).to eql(subject.meta.location) } 160 | specify { expect(result.meta.resource_type).to eql(subject.meta.resource_type) } 161 | specify { expect(result.to_json).to eql(subject.to_json) } 162 | specify { expect(result.to_h).to eql(subject.to_h) } 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /spec/scim/kit/v2/filter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit::V2::Filter do 4 | subject { described_class.new } 5 | 6 | [ 7 | 'userName', 8 | 'name.familyName', 9 | 'urn:ietf:params:scim:schemas:core:2.0:User:userName', 10 | 'meta.lastModified', 11 | 'schemas' 12 | ].each do |attribute| 13 | %w[ 14 | eq 15 | ne 16 | co 17 | sw 18 | ew 19 | gt 20 | lt 21 | ge 22 | le 23 | ].each do |operator| 24 | [ 25 | 'bjensen', 26 | "O'Malley", 27 | 'J', 28 | '2011-05-13T04:42:34Z', 29 | 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' 30 | ].each do |value| 31 | specify { expect(subject.parse_with_debug(%(#{attribute} #{operator} \"#{value}\"))).to be_truthy } 32 | end 33 | end 34 | end 35 | 36 | specify { expect(subject.parse_with_debug('userName eq "jeramy@ziemann.biz"')).to be_truthy } 37 | specify { expect(subject.parse_with_debug(%((title pr) and (userType eq "Employee")))).to be_truthy } 38 | specify { expect(subject.attribute_expression).not_to parse(%(title pr and userType eq "Employee")) } 39 | specify { expect(subject.logical_expression.parse_with_debug(%((title pr) and (userType eq "Employee")))).to be_truthy } 40 | specify { expect(subject.value_path).not_to parse(%(title pr and userType eq "Employee")) } 41 | 42 | [ 43 | 'emails[(type eq "work") and (value co "@example.com")]' 44 | ].each do |x| 45 | specify { expect(subject.value_path.parse_with_debug(x)).to be_truthy } 46 | end 47 | 48 | [ 49 | '(firstName eq "Tsuyoshi") and (lastName eq "Garret")', 50 | '(type eq "work") and (value co "@example.com")', 51 | 'firstName eq "Tsuyoshi"', 52 | 'firstName pr' 53 | ].each do |x| 54 | specify { expect(subject.value_filter).to parse(x) } 55 | end 56 | 57 | [ 58 | 'firstName eq "Tsuyoshi"', 59 | 'firstName pr' 60 | ].each do |x| 61 | specify { expect(subject.attribute_expression).to parse(x) } 62 | end 63 | 64 | [ 65 | '(firstName eq "Tsuyoshi") and (lastName eq "Garret")', 66 | '(firstName eq "Tsuyoshi") or (lastName eq "Garret")', 67 | '(title pr) and (userType eq "Employee")', 68 | '(title pr) or (userType eq "Employee")' 69 | ].each do |x| 70 | specify { expect(subject.logical_expression).to parse(x) } 71 | end 72 | 73 | ['false', 'null', 'true', '1', '"hello"', '"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"', '"Garrett"'].each do |x| 74 | specify { expect(subject.comparison_value).to parse(x) } 75 | end 76 | 77 | %w[eq ne co sw ew gt lt ge le].each do |x| 78 | specify { expect(subject.comparison_operator).to parse(x) } 79 | end 80 | 81 | [ 82 | 'userName', 83 | 'user_name', 84 | 'user-name', 85 | 'username1', 86 | 'meta.lastModified', 87 | 'schemas', 88 | 'name.familyName', 89 | 'urn:ietf:params:scim:schemas:core:2.0:User:userName', 90 | 'urn:ietf:params:scim:schemas:core:2.0:User:name.familyName' 91 | ].each do |x| 92 | specify { expect(subject.attribute_path.parse_with_debug(x)).to be_truthy } 93 | end 94 | 95 | %w[ 96 | userName 97 | user_name 98 | user-name 99 | username1 100 | schemas 101 | ].each do |x| 102 | specify { expect(subject.attribute_name).to parse(x) } 103 | end 104 | 105 | ['-', '_', '0', 'a'].each { |x| specify { expect(subject.name_character).to parse(x) } } 106 | specify { expect(subject.sub_attribute).to parse('.name') } 107 | specify { expect(subject.presence).to parse('pr') } 108 | specify { expect(subject.and_op).to parse('and') } 109 | specify { expect(subject.or_op).to parse('or') } 110 | specify { expect(subject.not_op?).to parse('not ') } 111 | specify { expect(subject.not_op?).to parse('') } 112 | specify { expect(subject.not_op?).not_to parse('not') } 113 | specify { expect(subject.not_op?).not_to parse('not not') } 114 | specify { expect(subject.falsey).to parse('false') } 115 | specify { expect(subject.truthy).to parse('true') } 116 | specify { expect(subject.null).to parse('null') } 117 | 118 | 1.upto(100).each { |n| specify { expect(subject.number).to parse(n.to_s) } } 119 | 120 | [ 121 | 'urn:ietf:params:scim:schemas:core:2.0:User', 122 | 'urn:ietf:params:scim:schemas:core:2.0:Group', 123 | 'urn:ietf:params:scim:schemas:extension:altean:2.0:User' 124 | ].each do |x| 125 | specify { expect(subject.uri).to parse(x) } 126 | end 127 | 128 | [ 129 | # '', 130 | # %Q(userType eq "Employee" and emails[type eq "work" and value co "@example.com"]), 131 | # %Q(emails[type eq "work" and value co "@example.com"] or ims[type eq "xmpp" and value co "@foo.com"]), 132 | %(meta.lastModified ge "2011-05-13T04:42:34Z"), 133 | %(meta.lastModified gt "2011-05-13T04:42:34Z"), 134 | %(meta.lastModified le "2011-05-13T04:42:34Z"), 135 | %(meta.lastModified lt "2011-05-13T04:42:34Z"), 136 | %(name.familyName co "O'Malley"), 137 | %(schemas eq "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"), 138 | %(title pr and userType eq "Employee"), 139 | %(title pr or userType eq "Intern"), 140 | %(title pr), 141 | %(urn:ietf:params:scim:schemas:core:2.0:User:userName sw "J"), 142 | %(userName eq "bjensen"), 143 | %(userName sw "J"), 144 | %(userType eq "Employee" and (emails co "example.com" or emails.value co "example.org")), 145 | %(userType eq "Employee" and (emails.type eq "work")), 146 | %(userType ne "Employee" and not (emails co "example.com" or emails.value co "example.org")), 147 | '(emails[(type eq "work") and (value co "@example.com")]) or (ims[(type eq "xmpp") and (value co "@foo.com")])', 148 | '(title pr) and (userType eq "Employee")', 149 | '(title pr) or (userType eq "Intern")', 150 | '(userType eq "Employee") and (emails.type eq "work")', 151 | '(userType eq "Employee") and (emails[(type eq "work") and (value co "@example.com")])', 152 | 'title pr', 153 | 'userName pr and not (userName eq "hello@example.com")' 154 | ].each do |x| 155 | specify { expect(subject.parse_with_debug(x)).to be_truthy } 156 | end 157 | 158 | specify { expect(subject.parse_with_debug('userName pr and not (userName eq "hello@example.com")')).to be_truthy } 159 | 160 | [ 161 | '"Tsuyoshi"', 162 | '"hello@example.org"', 163 | '"2011-05-13T04:42:34Z"' 164 | ].each do |x| 165 | specify { expect(subject.string).to parse(x) } 166 | end 167 | 168 | specify { expect(subject.hyphen).to parse('-') } 169 | specify { expect(subject.underscore).to parse('_') } 170 | 171 | (0..9).each { |x| specify { expect(subject.digit).to parse(x.to_s) } } 172 | [*'a'..'z', *'A'..'Z'].each { |x| specify { expect(subject.alpha).to parse(x) } } 173 | specify { expect(subject.colon).to parse(':') } 174 | specify { expect(subject.version).to parse('2.0') } 175 | specify { expect(subject.version).to parse('1.0') } 176 | 177 | describe '.parse' do 178 | subject { described_class } 179 | 180 | [ 181 | %(meta.lastModified ge "2011-05-13T04:42:34Z"), 182 | %(meta.lastModified gt "2011-05-13T04:42:34Z"), 183 | %(meta.lastModified le "2011-05-13T04:42:34Z"), 184 | %(meta.lastModified lt "2011-05-13T04:42:34Z"), 185 | %(name.familyName co "O'Malley"), 186 | %(schemas eq "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"), 187 | %(title pr and userType eq "Employee"), 188 | %(title pr or userType eq "Intern"), 189 | %(title pr), 190 | %(urn:ietf:params:scim:schemas:core:2.0:User:userName sw "J"), 191 | %(userName eq "bjensen"), 192 | %(userName sw "J"), 193 | %(userType eq "Employee" and (emails co "example.com" or emails.value co "example.org")), 194 | %(userType eq "Employee" and (emails.type eq "work")), 195 | %(userType ne "Employee" and not (emails co "example.com" or emails.value co "example.org")), 196 | '(emails[(type eq "work") and (value co "@example.com")]) or (ims[(type eq "xmpp") and (value co "@foo.com")])', 197 | '(title pr) and (userType eq "Employee")', 198 | '(title pr) or (userType eq "Intern")', 199 | '(userType eq "Employee") and (emails.type eq "work")', 200 | '(userType eq "Employee") and (emails[(type eq "work") and (value co "@example.com")])', 201 | 'title pr', 202 | 'userName pr and not (userName eq "hello@example.com")' 203 | ].each do |filter| 204 | let(:visitor) { Scim::Kit::V2::Filter::Visitor.new } 205 | 206 | specify { expect(subject.parse(filter)).to be_instance_of(Scim::Kit::V2::Filter::Node) } 207 | 208 | specify do 209 | node = subject.parse(filter) 210 | 211 | expect do 212 | node.accept(visitor) 213 | end.to raise_error(Scim::Kit::NotImplementedError) 214 | end 215 | end 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /spec/scim/kit/v2/schema_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit::V2::Schema do 4 | subject { described_class.new(id: id, name: name, location: location) } 5 | 6 | let(:id) { 'Group' } 7 | let(:name) { 'Group' } 8 | let(:location) { FFaker::Internet.uri('https') } 9 | let(:description) { FFaker::Name.name } 10 | let(:result) { JSON.parse(subject.to_json, symbolize_names: true) } 11 | 12 | specify { expect(result[:id]).to eql(id) } 13 | specify { expect(result[:name]).to eql(name) } 14 | specify { expect(result[:description]).to eql(name) } 15 | specify { expect(result[:meta][:resourceType]).to eql('Schema') } 16 | specify { expect(result[:meta][:location]).to eql(location) } 17 | 18 | context 'with a description' do 19 | before do 20 | subject.description = description 21 | end 22 | 23 | specify { expect(result[:description]).to eql(description) } 24 | end 25 | 26 | context 'with a single simple attribute' do 27 | before do 28 | subject.add_attribute(name: 'displayName') 29 | end 30 | 31 | specify { expect(result[:attributes][0][:name]).to eql('displayName') } 32 | specify { expect(result[:attributes][0][:type]).to eql('string') } 33 | specify { expect(result[:attributes][0][:multiValued]).to be(false) } 34 | specify { expect(result[:attributes][0][:description]).to eql('displayName') } 35 | specify { expect(result[:attributes][0][:required]).to be(false) } 36 | specify { expect(result[:attributes][0][:caseExact]).to be(false) } 37 | specify { expect(result[:attributes][0][:mutability]).to eql('readWrite') } 38 | specify { expect(result[:attributes][0][:returned]).to eql('default') } 39 | specify { expect(result[:attributes][0][:uniqueness]).to eql('none') } 40 | 41 | context 'with a description' do 42 | before do 43 | subject.add_attribute(name: 'userName') do |x| 44 | x.description = 'my description' 45 | end 46 | end 47 | 48 | specify { expect(result[:attributes][1][:description]).to eql('my description') } 49 | end 50 | end 51 | 52 | context 'with a complex attribute' do 53 | before do 54 | subject.add_attribute(name: 'emails') do |x| 55 | x.multi_valued = true 56 | x.add_attribute(name: 'value') 57 | x.add_attribute(name: 'primary', type: 'boolean') 58 | end 59 | end 60 | 61 | specify { expect(result[:attributes][0][:name]).to eql('emails') } 62 | specify { expect(result[:attributes][0][:type]).to eql('complex') } 63 | specify { expect(result[:attributes][0][:multiValued]).to be(true) } 64 | specify { expect(result[:attributes][0][:description]).to eql('emails') } 65 | specify { expect(result[:attributes][0][:required]).to be(false) } 66 | specify { expect(result[:attributes][0].key?(:caseExact)).to be(false) } 67 | specify { expect(result[:attributes][0][:mutability]).to eql('readWrite') } 68 | specify { expect(result[:attributes][0][:returned]).to eql('default') } 69 | specify { expect(result[:attributes][0][:uniqueness]).to eql('none') } 70 | 71 | specify { expect(result[:attributes][0][:subAttributes][0][:name]).to eql('value') } 72 | specify { expect(result[:attributes][0][:subAttributes][0][:type]).to eql('string') } 73 | specify { expect(result[:attributes][0][:subAttributes][0][:multiValued]).to be(false) } 74 | specify { expect(result[:attributes][0][:subAttributes][0][:description]).to eql('value') } 75 | specify { expect(result[:attributes][0][:subAttributes][0][:required]).to be(false) } 76 | specify { expect(result[:attributes][0][:subAttributes][0][:caseExact]).to be(false) } 77 | specify { expect(result[:attributes][0][:subAttributes][0][:mutability]).to eql('readWrite') } 78 | specify { expect(result[:attributes][0][:subAttributes][0][:returned]).to eql('default') } 79 | specify { expect(result[:attributes][0][:subAttributes][0][:uniqueness]).to eql('none') } 80 | 81 | specify { expect(result[:attributes][0][:subAttributes][1][:name]).to eql('primary') } 82 | specify { expect(result[:attributes][0][:subAttributes][1][:type]).to eql('boolean') } 83 | specify { expect(result[:attributes][0][:subAttributes][1][:multiValued]).to be(false) } 84 | specify { expect(result[:attributes][0][:subAttributes][1][:description]).to eql('primary') } 85 | specify { expect(result[:attributes][0][:subAttributes][1][:required]).to be(false) } 86 | specify { expect(result[:attributes][0][:subAttributes][1].key?(:caseExact)).to be(false) } 87 | specify { expect(result[:attributes][0][:subAttributes][1][:mutability]).to eql('readWrite') } 88 | specify { expect(result[:attributes][0][:subAttributes][1][:returned]).to eql('default') } 89 | specify { expect(result[:attributes][0][:subAttributes][1][:uniqueness]).to eql('none') } 90 | end 91 | 92 | context 'with a reference attribute' do 93 | before do 94 | subject.add_attribute(name: '$ref', type: 'reference') do |x| 95 | x.reference_types = %w[User Group] 96 | x.mutability = :read_only 97 | end 98 | end 99 | 100 | specify { expect(result[:attributes][0][:name]).to eql('$ref') } 101 | specify { expect(result[:attributes][0][:type]).to eql('reference') } 102 | specify { expect(result[:attributes][0][:referenceTypes]).to match_array(%w[User Group]) } 103 | specify { expect(result[:attributes][0][:multiValued]).to be(false) } 104 | specify { expect(result[:attributes][0][:description]).to eql('$ref') } 105 | specify { expect(result[:attributes][0][:required]).to be(false) } 106 | specify { expect(result[:attributes][0][:caseExact]).to be(false) } 107 | specify { expect(result[:attributes][0][:mutability]).to eql('readOnly') } 108 | specify { expect(result[:attributes][0][:returned]).to eql('default') } 109 | specify { expect(result[:attributes][0][:uniqueness]).to eql('none') } 110 | end 111 | 112 | describe '.build' do 113 | subject do 114 | described_class.build(id: id, name: name, location: location) do |x| 115 | x.description = description 116 | end 117 | end 118 | 119 | specify { expect(result[:id]).to eql(id) } 120 | specify { expect(result[:name]).to eql(name) } 121 | specify { expect(result[:description]).to eql(description) } 122 | specify { expect(result[:meta][:resourceType]).to eql('Schema') } 123 | specify { expect(result[:meta][:location]).to eql(location) } 124 | end 125 | 126 | describe '.parse' do 127 | let(:result) { described_class.parse(subject.to_json) } 128 | 129 | context 'with reference attribute type' do 130 | before do 131 | subject.add_attribute(name: :display_name) do |x| 132 | x.multi_valued = true 133 | x.required = true 134 | x.case_exact = true 135 | x.mutability = :read_only 136 | x.returned = :never 137 | x.uniqueness = :server 138 | x.canonical_values = ['honerva'] 139 | x.reference_types = %w[User Group] 140 | end 141 | end 142 | 143 | specify { expect(result.id).to eql(subject.id) } 144 | specify { expect(result.name).to eql(subject.name) } 145 | specify { expect(result.description).to eql(subject.description) } 146 | specify { expect(result.meta.created).to eql(subject.meta.created) } 147 | specify { expect(result.meta.last_modified).to eql(subject.meta.last_modified) } 148 | specify { expect(result.meta.version).to eql(subject.meta.version) } 149 | specify { expect(result.meta.location).to eql(subject.meta.location) } 150 | specify { expect(result.meta.resource_type).to eql(subject.meta.resource_type) } 151 | specify { expect(result.attributes.size).to be(1) } 152 | specify { expect(result.attributes.first.to_h).to eql(subject.attributes.first.to_h) } 153 | specify { expect(result.to_json).to eql(subject.to_json) } 154 | specify { expect(result.to_h).to eql(subject.to_h) } 155 | end 156 | 157 | context 'with complex attribute type' do 158 | before do 159 | subject.add_attribute(name: :name) do |x| 160 | x.add_attribute(name: :family_name) 161 | x.add_attribute(name: :given_name) 162 | end 163 | end 164 | 165 | specify { expect(result.id).to eql(subject.id) } 166 | specify { expect(result.name).to eql(subject.name) } 167 | specify { expect(result.description).to eql(subject.description) } 168 | specify { expect(result.meta.created).to eql(subject.meta.created) } 169 | specify { expect(result.meta.last_modified).to eql(subject.meta.last_modified) } 170 | specify { expect(result.meta.version).to eql(subject.meta.version) } 171 | specify { expect(result.meta.location).to eql(subject.meta.location) } 172 | specify { expect(result.meta.resource_type).to eql(subject.meta.resource_type) } 173 | specify { expect(result.attributes.size).to be(1) } 174 | specify { expect(result.attributes.first.to_h).to eql(subject.attributes.first.to_h) } 175 | specify { expect(result.to_json).to eql(subject.to_json) } 176 | specify { expect(result.to_h).to eql(subject.to_h) } 177 | end 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /spec/scim/kit/v2/attribute_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit::V2::Attribute do 4 | subject { described_class.new(type: type, resource: resource) } 5 | 6 | let(:resource) { Scim::Kit::V2::Resource.new(schemas: [schema], location: FFaker::Internet.uri('https')) } 7 | let(:schema) { Scim::Kit::V2::Schema.new(id: Scim::Kit::V2::Schemas::USER, name: 'User', location: FFaker::Internet.uri('https')) } 8 | 9 | context 'with strings' do 10 | let(:type) { Scim::Kit::V2::AttributeType.new(name: 'userName', type: :string) } 11 | 12 | context 'when valid' do 13 | let(:user_name) { FFaker::Internet.user_name } 14 | 15 | before { subject._value = user_name } 16 | 17 | specify { expect(subject._value).to eql(user_name) } 18 | specify { expect(subject.as_json[:userName]).to eql(user_name) } 19 | specify { expect(subject).to be_valid } 20 | end 21 | 22 | context 'when multiple values are allowed' do 23 | before { type.multi_valued = true } 24 | 25 | specify { expect(subject._value).to match_array([]) } 26 | 27 | context 'when multiple valid values are added' do 28 | before do 29 | subject._value = %w[superman batman] 30 | end 31 | 32 | specify { expect(subject._value).to match_array(%w[superman batman]) } 33 | specify { expect(subject).to be_valid } 34 | end 35 | 36 | context 'when multiple invalid values are added' do 37 | before do 38 | subject._assign(['superman', {}], coerce: false) 39 | end 40 | 41 | specify { expect(subject).not_to be_valid } 42 | end 43 | end 44 | 45 | context 'when a single value is provided' do 46 | before do 47 | type.multi_valued = true 48 | subject._value = 'batman' 49 | subject.valid? 50 | end 51 | 52 | specify { expect(subject).not_to be_valid } 53 | specify { expect(subject.errors[:user_name]).to be_present } 54 | end 55 | 56 | context 'when the wrong type is used' do 57 | before do 58 | type.multi_valued = true 59 | subject._assign([1.0, 2.0], coerce: false) 60 | subject.valid? 61 | end 62 | 63 | specify { expect(subject).not_to be_valid } 64 | specify { expect(subject.errors[:user_name]).to be_present } 65 | end 66 | 67 | context 'when integer' do 68 | let(:number) { rand(100) } 69 | 70 | before { subject._value = number } 71 | 72 | specify { expect(subject._value).to eql(number.to_s) } 73 | end 74 | 75 | context 'when datetime' do 76 | let(:datetime) { DateTime.now } 77 | 78 | before { subject._value = datetime } 79 | 80 | specify { expect(subject._value).to eql(datetime.to_s) } 81 | end 82 | 83 | context 'when not matching a canonical value' do 84 | before do 85 | type.canonical_values = %w[batman robin] 86 | subject._value = 'spider man' 87 | subject.valid? 88 | end 89 | 90 | specify { expect(subject).not_to be_valid } 91 | specify { expect(subject.errors[:user_name]).to be_present } 92 | end 93 | 94 | context 'when canonical value is given' do 95 | before do 96 | type.canonical_values = %w[batman robin] 97 | subject._value = 'batman' 98 | end 99 | 100 | specify { expect(subject._value).to eql('batman') } 101 | end 102 | end 103 | 104 | context 'with boolean' do 105 | let(:type) { Scim::Kit::V2::AttributeType.new(name: 'hungry', type: :boolean) } 106 | 107 | context 'when true' do 108 | before { subject._value = true } 109 | 110 | specify { expect(subject._value).to be(true) } 111 | specify { expect(subject.as_json[:hungry]).to be(true) } 112 | end 113 | 114 | context 'when false' do 115 | before { subject._value = false } 116 | 117 | specify { expect(subject._value).to be(false) } 118 | specify { expect(subject.as_json[:hungry]).to be(false) } 119 | end 120 | 121 | context 'when invalid string' do 122 | before { subject._value = 'hello' } 123 | 124 | specify { expect(subject._value).to eql('hello') } 125 | specify { expect(subject).not_to be_valid } 126 | end 127 | end 128 | 129 | context 'with decimal' do 130 | let(:type) { Scim::Kit::V2::AttributeType.new(name: 'measurement', type: :decimal) } 131 | 132 | context 'when given float' do 133 | before { subject._value = Math::PI } 134 | 135 | specify { expect(subject._value).to eql(Math::PI) } 136 | specify { expect(subject.as_json[:measurement]).to be(Math::PI) } 137 | end 138 | 139 | context 'when given an integer' do 140 | before { subject._value = 42 } 141 | 142 | specify { expect(subject._value).to eql(42.to_f) } 143 | specify { expect(subject.as_json[:measurement]).to be(42.to_f) } 144 | end 145 | end 146 | 147 | context 'with integer' do 148 | let(:type) { Scim::Kit::V2::AttributeType.new(name: 'age', type: :integer) } 149 | 150 | context 'when given integer' do 151 | before { subject._value = 34 } 152 | 153 | specify { expect(subject._value).to be(34) } 154 | specify { expect(subject.as_json[:age]).to be(34) } 155 | end 156 | 157 | context 'when given float' do 158 | before { subject._value = Math::PI } 159 | 160 | specify { expect(subject._value).to eql(Math::PI.to_i) } 161 | end 162 | end 163 | 164 | context 'with datetime' do 165 | let(:type) { Scim::Kit::V2::AttributeType.new(name: 'birthdate', type: :datetime) } 166 | let(:datetime) { DateTime.new(2019, 0o1, 0o6, 12, 35, 0o0) } 167 | 168 | context 'when given a date time' do 169 | before { subject._value = datetime } 170 | 171 | specify { expect(subject._value).to eql(datetime) } 172 | specify { expect(subject.as_json[:birthdate]).to eql(datetime.iso8601) } 173 | end 174 | 175 | context 'when given a string' do 176 | before { subject._value = datetime.to_s } 177 | 178 | specify { expect(subject._value).to eql(datetime) } 179 | end 180 | end 181 | 182 | context 'with binary' do 183 | let(:type) { Scim::Kit::V2::AttributeType.new(name: 'photo', type: :binary) } 184 | let(:photo) { IO.read('./spec/fixtures/avatar.png', mode: 'rb') } 185 | 186 | context 'when given a .png' do 187 | before { subject._value = photo } 188 | 189 | specify { expect(subject._value).to eql(Base64.strict_encode64(photo)) } 190 | specify { expect(subject.as_json[:photo]).to eql(Base64.strict_encode64(photo)) } 191 | end 192 | end 193 | 194 | context 'with reference' do 195 | let(:type) { Scim::Kit::V2::AttributeType.new(name: 'group', type: :reference) } 196 | let(:uri) { FFaker::Internet.uri('https') } 197 | 198 | before { subject._value = uri } 199 | 200 | specify { expect(subject._value).to eql(uri) } 201 | specify { expect(subject.as_json[:group]).to eql(uri) } 202 | end 203 | 204 | context 'with complex type' do 205 | let(:type) do 206 | x = Scim::Kit::V2::AttributeType.new(name: 'name', type: :complex) 207 | x.add_attribute(name: 'familyName') 208 | x.add_attribute(name: 'givenName') 209 | x 210 | end 211 | 212 | before do 213 | subject.family_name = 'Garrett' 214 | subject.given_name = 'Tsuyoshi' 215 | end 216 | 217 | specify { expect(subject.family_name).to eql('Garrett') } 218 | specify { expect(subject.given_name).to eql('Tsuyoshi') } 219 | specify { expect(subject.as_json[:name][:familyName]).to eql('Garrett') } 220 | specify { expect(subject.as_json[:name][:givenName]).to eql('Tsuyoshi') } 221 | end 222 | 223 | context 'with single valued complex type' do 224 | let(:type) do 225 | x = Scim::Kit::V2::AttributeType.new(name: :person, type: :complex) 226 | x.add_attribute(name: :name) 227 | x.add_attribute(name: :age, type: :integer) 228 | x 229 | end 230 | 231 | specify do 232 | subject.name = 'mo' 233 | subject.age = 34 234 | expect(subject).to be_valid 235 | end 236 | 237 | specify do 238 | subject.name = 'mo' 239 | subject.age = [] 240 | expect(subject).not_to be_valid 241 | end 242 | end 243 | 244 | context 'with multi valued complex type' do 245 | let(:type) do 246 | x = Scim::Kit::V2::AttributeType.new(name: 'emails', type: :complex) 247 | x.multi_valued = true 248 | x.add_attribute(name: 'value') do |y| 249 | y.required = true 250 | end 251 | x.add_attribute(name: 'primary', type: :boolean) 252 | x 253 | end 254 | let(:email) { FFaker::Internet.email } 255 | let(:other_email) { FFaker::Internet.email } 256 | 257 | before do 258 | subject._value = [ 259 | { value: email, primary: true }, 260 | { value: other_email, primary: false } 261 | ] 262 | end 263 | 264 | specify { expect(subject._value).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) } 265 | specify { expect(subject.as_json[:emails]).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) } 266 | specify { expect(subject).to be_valid } 267 | 268 | context 'when the hash is invalid' do 269 | before do 270 | subject._value = [{ blah: 'blah' }] 271 | subject.valid? 272 | end 273 | 274 | specify { expect(subject).not_to be_valid } 275 | specify { expect(subject.errors[:blah]).to be_present } 276 | specify { expect(subject.errors[:value]).to be_present } 277 | end 278 | end 279 | 280 | context 'when the resource is in server mode' do 281 | let(:type) { Scim::Kit::V2::AttributeType.new(name: 'userName', type: :string) } 282 | let(:resource) { instance_double(Scim::Kit::V2::Resource) } 283 | 284 | before do 285 | allow(resource).to receive(:mode?).with(:server).and_return(true) 286 | allow(resource).to receive(:mode?).with(:client).and_return(false) 287 | end 288 | 289 | context 'when the type is read only' do 290 | before { type.mutability = :read_only } 291 | 292 | specify { expect(subject).to be_renderable } 293 | end 294 | 295 | context 'when the type is write only' do 296 | before { type.mutability = :write_only } 297 | 298 | specify { expect(subject).not_to be_renderable } 299 | end 300 | 301 | context 'when returned type is `never`' do 302 | before { type.returned = :never } 303 | 304 | specify { expect(subject).not_to be_renderable } 305 | end 306 | end 307 | 308 | context 'when the resource is in client mode' do 309 | let(:type) { Scim::Kit::V2::AttributeType.new(name: 'userName', type: :string) } 310 | let(:resource) { instance_double(Scim::Kit::V2::Resource) } 311 | 312 | before do 313 | allow(resource).to receive(:mode?).with(:server).and_return(false) 314 | allow(resource).to receive(:mode?).with(:client).and_return(true) 315 | end 316 | 317 | context 'when the type is read only' do 318 | before { type.mutability = :read_only } 319 | 320 | specify { expect(subject).not_to be_renderable } 321 | end 322 | 323 | context 'when the type is write only' do 324 | before { type.mutability = :write_only } 325 | 326 | specify do 327 | subject._value = 'hello' 328 | expect(subject).to be_renderable 329 | end 330 | 331 | specify do 332 | subject._value = nil 333 | expect(subject).not_to be_renderable 334 | end 335 | end 336 | end 337 | end 338 | -------------------------------------------------------------------------------- /spec/scim/kit/v2/attribute_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit::V2::AttributeType do 4 | let(:image) { Base64.strict_encode64(raw_image) } 5 | let(:raw_image) { IO.read('./spec/fixtures/avatar.png', mode: 'rb') } 6 | 7 | specify { expect { described_class.new(name: 'displayName', type: :string) }.not_to raise_error } 8 | specify { expect { described_class.new(name: 'primary', type: :boolean) }.not_to raise_error } 9 | specify { expect { described_class.new(name: 'salary', type: :decimal) }.not_to raise_error } 10 | specify { expect { described_class.new(name: 'age', type: :integer) }.not_to raise_error } 11 | specify { expect { described_class.new(name: 'birthdate', type: :datetime) }.not_to raise_error } 12 | specify { expect { described_class.new(name: '$ref', type: :reference) }.not_to raise_error } 13 | specify { expect { described_class.new(name: 'emails', type: :complex) }.not_to raise_error } 14 | specify { expect { described_class.new(name: 'photo', type: :binary) }.not_to raise_error } 15 | specify { expect { described_class.new(name: 'invalid', type: :invalid) }.to raise_error(ArgumentError) } 16 | 17 | describe 'with a symbolic name' do 18 | subject { described_class.new(name: :display_name) } 19 | 20 | specify { expect(subject.to_h[:name]).to eql('displayName') } 21 | specify { expect(subject.to_h[:description]).to eql('displayName') } 22 | end 23 | 24 | describe 'String Attribute' do 25 | describe 'defaults' do 26 | subject { described_class.new(name: 'displayName') } 27 | 28 | specify { expect(subject.name).to eql('display_name') } 29 | specify { expect(subject.type).to be(:string) } 30 | specify { expect(subject.to_h[:name]).to eql('displayName') } 31 | specify { expect(subject.to_h[:type]).to eql('string') } 32 | specify { expect(subject.to_h[:multiValued]).to be(false) } 33 | specify { expect(subject.to_h[:description]).to eql('displayName') } 34 | specify { expect(subject.to_h[:required]).to be(false) } 35 | specify { expect(subject.to_h[:caseExact]).to be(false) } 36 | specify { expect(subject.to_h[:mutability]).to eql('readWrite') } 37 | specify { expect(subject.to_h[:returned]).to eql('default') } 38 | specify { expect(subject.to_h[:uniqueness]).to eql('none') } 39 | end 40 | 41 | describe 'overrides' do 42 | def build(overrides) 43 | x = described_class.new(name: 'displayName') 44 | overrides.each do |(key, value)| 45 | x.public_send("#{key}=".to_sym, value) 46 | end 47 | x 48 | end 49 | 50 | specify { expect(build(multi_valued: true).to_h[:multiValued]).to be(true) } 51 | specify { expect(build(description: 'hello').to_h[:description]).to eq('hello') } 52 | specify { expect(build(required: true).to_h[:required]).to be(true) } 53 | specify { expect(build(case_exact: true).to_h[:caseExact]).to be(true) } 54 | 55 | specify { expect(build(mutability: :read_only).to_h[:mutability]).to eql('readOnly') } 56 | specify { expect(build(mutability: :read_write).to_h[:mutability]).to eql('readWrite') } 57 | specify { expect(build(mutability: :immutable).to_h[:mutability]).to eql('immutable') } 58 | specify { expect(build(mutability: :write_only).to_h[:mutability]).to eql('writeOnly') } 59 | specify { expect { build(mutability: :invalid) }.to raise_error(ArgumentError) } 60 | 61 | specify { expect(build(returned: :always).to_h[:returned]).to eql('always') } 62 | specify { expect(build(returned: :never).to_h[:returned]).to eql('never') } 63 | specify { expect(build(returned: :default).to_h[:returned]).to eql('default') } 64 | specify { expect(build(returned: :request).to_h[:returned]).to eql('request') } 65 | specify { expect { build(returned: :invalid) }.to raise_error(ArgumentError) } 66 | 67 | specify { expect(build(uniqueness: :none).to_h[:uniqueness]).to eql('none') } 68 | specify { expect(build(uniqueness: :server).to_h[:uniqueness]).to eql('server') } 69 | specify { expect(build(uniqueness: :global).to_h[:uniqueness]).to eql('global') } 70 | specify { expect { build(uniqueness: :invalid) }.to raise_error(ArgumentError) } 71 | 72 | specify { expect(build(reference_types: %w[User Group]).to_h[:referenceTypes]).to match_array(%w[User Group]) } 73 | specify { expect(build(canonical_values: %w[User Group]).to_h[:canonicalValues]).to match_array(%w[User Group]) } 74 | end 75 | end 76 | 77 | describe '#valid?' do 78 | specify { expect(described_class.new(name: :x, type: :binary)).to be_valid(image) } 79 | specify { expect(described_class.new(name: :x, type: :binary)).not_to be_valid('hello') } 80 | specify { expect(described_class.new(name: :x, type: :binary)).not_to be_valid(1) } 81 | specify { expect(described_class.new(name: :x, type: :binary)).not_to be_valid([1]) } 82 | 83 | specify { expect(described_class.new(name: :x, type: :boolean)).to be_valid(true) } 84 | specify { expect(described_class.new(name: :x, type: :boolean)).to be_valid(false) } 85 | specify { expect(described_class.new(name: :x, type: :boolean)).not_to be_valid('false') } 86 | specify { expect(described_class.new(name: :x, type: :boolean)).not_to be_valid(1) } 87 | 88 | specify { expect(described_class.new(name: :x, type: :datetime)).to be_valid(DateTime.now) } 89 | specify { expect(described_class.new(name: :x, type: :datetime)).not_to be_valid(DateTime.now.iso8601) } 90 | specify { expect(described_class.new(name: :x, type: :datetime)).not_to be_valid(Time.now.to_i) } 91 | specify { expect(described_class.new(name: :x, type: :datetime)).not_to be_valid(Time.now) } 92 | 93 | specify { expect(described_class.new(name: :x, type: :decimal)).to be_valid(1.0) } 94 | specify { expect(described_class.new(name: :x, type: :decimal)).not_to be_valid(1) } 95 | specify { expect(described_class.new(name: :x, type: :decimal)).not_to be_valid('1.0') } 96 | 97 | specify { expect(described_class.new(name: :x, type: :integer)).to be_valid(1) } 98 | specify { expect(described_class.new(name: :x, type: :integer)).to be_valid(1_000) } 99 | specify { expect(described_class.new(name: :x, type: :integer)).not_to be_valid(10.0) } 100 | specify { expect(described_class.new(name: :x, type: :integer)).not_to be_valid('10') } 101 | specify { expect(described_class.new(name: :x, type: :integer)).not_to be_valid([]) } 102 | 103 | specify { expect(described_class.new(name: :x, type: :reference)).to be_valid(FFaker::Internet.uri('https')) } 104 | specify { expect(described_class.new(name: :x, type: :reference)).not_to be_valid('hello') } 105 | specify { expect(described_class.new(name: :x, type: :reference)).not_to be_valid(1) } 106 | specify { expect(described_class.new(name: :x, type: :reference)).not_to be_valid(['hello']) } 107 | 108 | specify { expect(described_class.new(name: :x, type: :string)).to be_valid('name') } 109 | specify { expect(described_class.new(name: :x, type: :string)).not_to be_valid(1) } 110 | specify { expect(described_class.new(name: :x, type: :string)).not_to be_valid(['string']) } 111 | 112 | context 'when multi valued string type' do 113 | subject { described_class.new(name: :emails, type: :string) } 114 | 115 | let(:email) { FFaker::Internet.email } 116 | 117 | before do 118 | subject.multi_valued = true 119 | end 120 | 121 | specify { expect(subject).to be_valid([email]) } 122 | specify { expect(subject).not_to be_valid([1]) } 123 | specify { expect(subject).not_to be_valid(email) } 124 | end 125 | 126 | context 'when single valued complex type' do 127 | subject { described_class.new(name: :location, type: :complex) } 128 | 129 | before do 130 | subject.multi_valued = false 131 | subject.add_attribute(name: :name, type: :string) 132 | subject.add_attribute(name: :latitude, type: :integer) 133 | subject.add_attribute(name: :longitude, type: :integer) 134 | end 135 | 136 | specify { expect(subject).to be_valid(name: 'work', latitude: 100, longitude: 100) } 137 | specify { expect(subject).not_to be_valid([name: 'work', latitude: 100, longitude: 100]) } 138 | specify { expect(subject).not_to be_valid(name: 'work', latitude: 'wrong', longitude: 100) } 139 | end 140 | 141 | context 'when multi valued complex type' do 142 | subject { described_class.new(name: :emails, type: :complex) } 143 | 144 | let(:email) { FFaker::Internet.email } 145 | let(:other_email) { FFaker::Internet.email } 146 | 147 | before do 148 | subject.multi_valued = true 149 | subject.add_attribute(name: 'value', type: :string) 150 | subject.add_attribute(name: 'primary', type: :boolean) 151 | end 152 | 153 | specify { expect(subject).to be_valid([value: email, primary: true]) } 154 | specify { expect(subject).to be_valid([{ value: email, primary: true }, { value: other_email, primary: false }]) } 155 | specify { expect(subject).not_to be_valid(email) } 156 | specify { expect(subject).not_to be_valid([email]) } 157 | specify { expect(subject).not_to be_valid([value: 1, primary: true]) } 158 | specify { expect(subject).not_to be_valid([value: email, primary: 'true']) } 159 | end 160 | end 161 | 162 | describe '#coerce' do 163 | let(:now) { DateTime.now } 164 | let(:uri) { FFaker::Internet.uri('https') } 165 | 166 | specify { expect(described_class.new(name: :x, type: :binary).coerce(raw_image)).to eql(image) } 167 | specify { expect(described_class.new(name: :x, type: :binary).coerce(image)).to eql(image) } 168 | 169 | specify { expect(described_class.new(name: :x, type: :boolean).coerce(true)).to be(true) } 170 | specify { expect(described_class.new(name: :x, type: :boolean).coerce('true')).to be(true) } 171 | specify { expect(described_class.new(name: :x, type: :boolean).coerce(false)).to be(false) } 172 | specify { expect(described_class.new(name: :x, type: :boolean).coerce('false')).to be(false) } 173 | specify { expect(described_class.new(name: :x, type: :boolean).coerce('invalid')).to eql('invalid') } 174 | 175 | specify { expect(described_class.new(name: :x, type: :datetime).coerce(now)).to eql(now) } 176 | specify { expect(described_class.new(name: :x, type: :datetime).coerce(now.iso8601)).to be_within(1).of(now) } 177 | 178 | specify { expect(described_class.new(name: :x, type: :decimal).coerce(1.0)).to be(1.0) } 179 | specify { expect(described_class.new(name: :x, type: :decimal).coerce(1)).to be(1.0) } 180 | specify { expect(described_class.new(name: :x, type: :decimal).coerce('1.0')).to be(1.0) } 181 | specify { expect(described_class.new(name: :x, type: :decimal).coerce('1')).to be(1.0) } 182 | 183 | specify { expect(described_class.new(name: :x, type: :integer).coerce(1)).to be(1) } 184 | specify { expect(described_class.new(name: :x, type: :integer).coerce(1.0)).to be(1) } 185 | specify { expect(described_class.new(name: :x, type: :integer).coerce('1.0')).to be(1) } 186 | specify { expect(described_class.new(name: :x, type: :integer).coerce('1')).to be(1) } 187 | 188 | specify { expect(described_class.new(name: :x, type: :reference).coerce(uri)).to eql(uri) } 189 | specify { expect(described_class.new(name: :x, type: :reference).coerce('hello')).to eql('hello') } 190 | 191 | specify { expect(described_class.new(name: :x, type: :string).coerce('name')).to eql('name') } 192 | specify { expect(described_class.new(name: :x, type: :string).coerce(1)).to eql('1') } 193 | 194 | context 'when multi valued string type' do 195 | subject do 196 | x = described_class.new(name: :x, type: :string) 197 | x.multi_valued = true 198 | x 199 | end 200 | 201 | specify { expect(subject.coerce(['1'])).to match_array(['1']) } 202 | specify { expect(subject.coerce([1])).to match_array(['1']) } 203 | end 204 | 205 | context 'when single valued complex type' do 206 | subject { described_class.new(name: :location, type: :complex) } 207 | 208 | before do 209 | subject.multi_valued = false 210 | subject.add_attribute(name: :name, type: :string) 211 | subject.add_attribute(name: :latitude, type: :integer) 212 | subject.add_attribute(name: :longitude, type: :integer) 213 | end 214 | 215 | specify { expect(subject.coerce(name: 'work', latitude: 100, longitude: 100)).to eql(name: 'work', latitude: 100, longitude: 100) } 216 | end 217 | 218 | context 'when multi valued complex type' do 219 | subject { described_class.new(name: :emails, type: :complex) } 220 | 221 | let(:email) { FFaker::Internet.email } 222 | let(:other_email) { FFaker::Internet.email } 223 | 224 | before do 225 | subject.multi_valued = true 226 | subject.add_attribute(name: 'value', type: :string) 227 | subject.add_attribute(name: 'primary', type: :boolean) 228 | end 229 | 230 | specify { expect(subject.coerce([value: email, primary: true])).to match_array([value: email, primary: true]) } 231 | specify { expect(subject.coerce([{ value: email, primary: true }, { value: other_email, primary: false }])).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) } 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /spec/scim/kit/v2/resource_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Scim::Kit::V2::Resource do 4 | subject { described_class.new(schemas: schemas, location: resource_location) } 5 | 6 | let(:schemas) { [schema] } 7 | let(:schema) { Scim::Kit::V2::Schema.new(id: Scim::Kit::V2::Schemas::USER, name: 'User', location: FFaker::Internet.uri('https')) } 8 | let(:resource_location) { FFaker::Internet.uri('https') } 9 | 10 | context 'when the schemas is empty' do 11 | let(:schemas) { [] } 12 | 13 | specify { expect { subject }.not_to raise_error } 14 | specify { expect(subject.meta.resource_type).to eql('Unknown') } 15 | end 16 | 17 | context 'with common attributes' do 18 | let(:id) { SecureRandom.uuid } 19 | let(:external_id) { SecureRandom.uuid } 20 | let(:created_at) { Time.now } 21 | let(:updated_at) { Time.now } 22 | let(:version) { SecureRandom.uuid } 23 | 24 | before do 25 | subject.id = id 26 | subject.external_id = external_id 27 | subject.meta.created = created_at 28 | subject.meta.last_modified = updated_at 29 | subject.meta.version = version 30 | end 31 | 32 | specify { expect(subject.id).to eql(id) } 33 | specify { expect(subject.external_id).to eql(external_id) } 34 | specify { expect(subject.meta.resource_type).to eql('User') } 35 | specify { expect(subject.meta.location).to eql(resource_location) } 36 | specify { expect(subject.meta.created).to eql(created_at) } 37 | specify { expect(subject.meta.last_modified).to eql(updated_at) } 38 | specify { expect(subject.meta.version).to eql(version) } 39 | 40 | describe '#as_json' do 41 | specify { expect(subject.as_json[:schemas]).to match_array([schema.id]) } 42 | specify { expect(subject.as_json[:id]).to eql(id) } 43 | specify { expect(subject.as_json[:externalId]).to be_nil } # only render in client mode 44 | specify { expect(subject.as_json[:meta][:resourceType]).to eql('User') } 45 | specify { expect(subject.as_json[:meta][:location]).to eql(resource_location) } 46 | specify { expect(subject.as_json[:meta][:created]).to eql(created_at.iso8601) } 47 | specify { expect(subject.as_json[:meta][:lastModified]).to eql(updated_at.iso8601) } 48 | specify { expect(subject.as_json[:meta][:version]).to eql(version) } 49 | end 50 | end 51 | 52 | context 'with attribute named "members"' do 53 | before do 54 | schema.add_attribute(name: 'members') do |attribute| 55 | attribute.mutability = :read_only 56 | attribute.multi_valued = true 57 | attribute.add_attribute(name: 'value') do |z| 58 | z.mutability = :immutable 59 | end 60 | attribute.add_attribute(name: '$ref') do |z| 61 | z.reference_types = %w[User Group] 62 | z.mutability = :immutable 63 | end 64 | attribute.add_attribute(name: 'type') do |z| 65 | z.canonical_values = %w[User Group] 66 | z.mutability = :immutable 67 | end 68 | end 69 | subject.members << { value: SecureRandom.uuid, '$ref' => FFaker::Internet.uri('https'), type: 'User' } 70 | end 71 | 72 | specify { expect(subject.members[0][:type]).to eql('User') } 73 | specify { expect(subject.as_json[:members][0][:type]).to eql('User') } 74 | specify { expect(subject.to_h[:members][0][:type]).to eql('User') } 75 | end 76 | 77 | context 'with custom string attribute' do 78 | let(:user_name) { FFaker::Internet.user_name } 79 | 80 | before do 81 | schema.add_attribute(name: 'userName') 82 | subject.user_name = user_name 83 | end 84 | 85 | specify { expect(subject.user_name).to eql(user_name) } 86 | end 87 | 88 | context 'with attribute named "type"' do 89 | before do 90 | schema.add_attribute(name: 'type') 91 | subject.type = 'User' 92 | end 93 | 94 | specify { expect(subject.type).to eql('User') } 95 | specify { expect(subject.as_json[:type]).to eql('User') } 96 | specify { expect(subject.send(:attribute_for, :type)._type).to be_instance_of(Scim::Kit::V2::AttributeType) } 97 | end 98 | 99 | context 'with attribute named $ref' do 100 | before do 101 | schema.add_attribute(name: '$ref') 102 | subject.write_attribute('$ref', 'User') 103 | end 104 | 105 | specify { expect(subject.read_attribute('$ref')).to eql('User') } 106 | specify { expect(subject.as_json['$ref']).to eql('User') } 107 | specify { expect(subject.send(:attribute_for, '$ref')._type).to be_instance_of(Scim::Kit::V2::AttributeType) } 108 | end 109 | 110 | context 'with a complex attribute' do 111 | before do 112 | schema.add_attribute(name: 'name') do |x| 113 | x.add_attribute(name: 'familyName') 114 | x.add_attribute(name: 'givenName') 115 | end 116 | subject.name.family_name = 'Garrett' 117 | subject.name.given_name = 'Tsuyoshi' 118 | end 119 | 120 | specify { expect(subject.name.family_name).to eql('Garrett') } 121 | specify { expect(subject.name.given_name).to eql('Tsuyoshi') } 122 | 123 | describe '#as_json' do 124 | specify { expect(subject.as_json[:name][:familyName]).to eql('Garrett') } 125 | specify { expect(subject.as_json[:name][:givenName]).to eql('Tsuyoshi') } 126 | end 127 | end 128 | 129 | context 'with a complex multi valued attribute' do 130 | let(:email) { FFaker::Internet.email } 131 | let(:other_email) { FFaker::Internet.email } 132 | 133 | before do 134 | schema.add_attribute(name: 'emails', type: :complex) do |x| 135 | x.multi_valued = true 136 | x.add_attribute(name: 'value') do |y| 137 | y.required = true 138 | end 139 | x.add_attribute(name: 'primary', type: :boolean) 140 | end 141 | subject.emails = [ 142 | { value: email, primary: true }, 143 | { value: other_email, primary: false } 144 | ] 145 | end 146 | 147 | specify { expect(subject.emails).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) } 148 | specify { expect(subject.as_json[:emails]).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) } 149 | 150 | context 'when one attribute has an invalid type' do 151 | before do 152 | subject.emails = [{ value: email, primary: 'q' }] 153 | subject.valid? 154 | end 155 | 156 | specify { expect(subject).not_to be_valid } 157 | specify { expect(subject.errors[:primary]).to be_present } 158 | end 159 | 160 | context 'when a required attribute is missing' do 161 | before do 162 | subject.emails = [{ primary: true }] 163 | subject.valid? 164 | end 165 | 166 | specify { expect(subject).not_to be_valid } 167 | specify { expect(subject.errors[:value]).to be_present } 168 | end 169 | end 170 | 171 | context 'with multiple schemas' do 172 | let(:schemas) { [schema, extension] } 173 | let(:extension) { Scim::Kit::V2::Schema.new(id: extension_id, name: 'Extension', location: FFaker::Internet.uri('https')) } 174 | let(:extension_id) { 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' } 175 | 176 | before do 177 | schema.add_attribute(name: :country) 178 | extension.add_attribute(name: :province) 179 | end 180 | 181 | context 'without any collisions' do 182 | before do 183 | subject.country = 'canada' 184 | subject.province = 'alberta' 185 | end 186 | 187 | specify { expect(subject.country).to eql('canada') } 188 | specify { expect(subject.province).to eql('alberta') } 189 | specify { expect(subject.as_json[:country]).to eql('canada') } 190 | specify { expect(subject.as_json[extension_id][:province]).to eql('alberta') } 191 | end 192 | 193 | context 'with an extension attribute with the same name as a core attribute' do 194 | before do 195 | extension.add_attribute(name: :country) 196 | 197 | subject.country = 'canada' 198 | subject.write_attribute("#{extension_id}#country", 'usa') 199 | end 200 | 201 | specify { expect(subject.country).to eql('canada') } 202 | specify { expect(subject.read_attribute("#{extension_id}#country")).to eql('usa') } 203 | specify { expect(subject.as_json[:country]).to eql('canada') } 204 | specify { expect(subject.as_json[extension_id][:country]).to eql('usa') } 205 | end 206 | end 207 | 208 | describe '#valid?' do 209 | context 'when valid' do 210 | before { subject.id = SecureRandom.uuid } 211 | 212 | specify { expect(subject).to be_valid } 213 | end 214 | 215 | context 'when a required simple attribute is blank' do 216 | before do 217 | schema.add_attribute(name: 'userName') do |x| 218 | x.required = true 219 | end 220 | subject.id = SecureRandom.uuid 221 | subject.valid? 222 | end 223 | 224 | specify { expect(subject).not_to be_valid } 225 | specify { expect(subject.errors[:user_name]).to be_present } 226 | end 227 | 228 | context 'when not matching a canonical value' do 229 | before do 230 | schema.add_attribute(name: 'hero') do |x| 231 | x.canonical_values = %w[batman robin] 232 | end 233 | subject.id = SecureRandom.uuid 234 | subject.hero = 'spiderman' 235 | subject.valid? 236 | end 237 | 238 | specify { expect(subject).not_to be_valid } 239 | specify { expect(subject.errors[:hero]).to be_present } 240 | end 241 | 242 | context 'when validating a complex type' do 243 | before do 244 | schema.add_attribute(name: :manager, type: :complex) do |x| 245 | x.multi_valued = false 246 | x.required = false 247 | x.mutability = :read_write 248 | x.returned = :default 249 | x.add_attribute(name: :value, type: :string) do |y| 250 | y.multi_valued = false 251 | y.required = false 252 | y.case_exact = false 253 | y.mutability = :read_write 254 | y.returned = :default 255 | y.uniqueness = :none 256 | end 257 | x.add_attribute(name: '$ref', type: :reference) do |y| 258 | y.multi_valued = false 259 | y.required = false 260 | y.case_exact = false 261 | y.mutability = :read_write 262 | y.returned = :default 263 | y.uniqueness = :none 264 | end 265 | x.add_attribute(name: :display_name, type: :string) do |y| 266 | y.multi_valued = false 267 | y.required = true 268 | y.case_exact = false 269 | y.mutability = :read_only 270 | y.returned = :default 271 | y.uniqueness = :none 272 | end 273 | end 274 | end 275 | 276 | context 'when valid' do 277 | before do 278 | subject.manager.value = SecureRandom.uuid 279 | subject.manager.write_attribute('$ref', FFaker::Internet.uri('https')) 280 | subject.manager.display_name = SecureRandom.uuid 281 | end 282 | 283 | specify { expect(subject).to be_valid } 284 | end 285 | 286 | context 'when invalid' do 287 | before do 288 | subject.manager.value = SecureRandom.uuid 289 | subject.manager.write_attribute('$ref', SecureRandom.uuid) 290 | subject.manager.display_name = nil 291 | subject.valid? 292 | end 293 | 294 | specify { expect(subject).not_to be_valid } 295 | specify { expect(subject.errors[:display_name]).to be_present } 296 | end 297 | end 298 | end 299 | 300 | context 'when building a new resource' do 301 | subject { described_class.new(schemas: schemas) } 302 | 303 | before do 304 | schema.add_attribute(name: 'userName') do |attribute| 305 | attribute.required = true 306 | attribute.uniqueness = :server 307 | end 308 | schema.add_attribute(name: 'name') do |attribute| 309 | attribute.add_attribute(name: 'formatted') do |x| 310 | x.mutability = :read_only 311 | end 312 | attribute.add_attribute(name: 'familyName') 313 | attribute.add_attribute(name: 'givenName') 314 | end 315 | schema.add_attribute(name: 'displayName') do |attribute| 316 | attribute.mutability = :read_only 317 | end 318 | schema.add_attribute(name: 'locale') 319 | schema.add_attribute(name: 'timezone') 320 | schema.add_attribute(name: 'active', type: :boolean) 321 | schema.add_attribute(name: 'password') do |attribute| 322 | attribute.mutability = :write_only 323 | attribute.returned = :never 324 | end 325 | schema.add_attribute(name: 'emails') do |attribute| 326 | attribute.multi_valued = true 327 | attribute.add_attribute(name: 'value') 328 | attribute.add_attribute(name: 'primary', type: :boolean) 329 | end 330 | schema.add_attribute(name: 'groups') do |attribute| 331 | attribute.multi_valued = true 332 | attribute.mutability = :read_only 333 | attribute.add_attribute(name: 'value') do |x| 334 | x.mutability = :read_only 335 | end 336 | attribute.add_attribute(name: '$ref') do |x| 337 | x.reference_types = %w[User Group] 338 | x.mutability = :read_only 339 | end 340 | attribute.add_attribute(name: 'display') do |x| 341 | x.mutability = :read_only 342 | end 343 | end 344 | end 345 | 346 | specify { expect(subject.as_json.key?(:meta)).to be(false) } 347 | specify { expect(subject.as_json.key?(:id)).to be(false) } 348 | specify { expect(subject.as_json.key?(:externalId)).to be(false) } 349 | 350 | context 'when using a simplified API' do 351 | let(:user_name) { FFaker::Internet.user_name } 352 | let(:resource) do 353 | described_class.new(schemas: schemas) do |x| 354 | x.user_name = user_name 355 | x.name.given_name = 'Barbara' 356 | x.name.family_name = 'Jensen' 357 | x.emails = [ 358 | { value: FFaker::Internet.email, primary: true }, 359 | { value: FFaker::Internet.email, primary: false } 360 | ] 361 | x.locale = 'en' 362 | x.timezone = 'Etc/UTC' 363 | end 364 | end 365 | 366 | specify { expect(resource.user_name).to eql(user_name) } 367 | specify { expect(resource.name.given_name).to eql('Barbara') } 368 | specify { expect(resource.name.family_name).to eql('Jensen') } 369 | specify { expect(resource.emails[0][:value]).to be_present } 370 | specify { expect(resource.emails[0][:primary]).to be(true) } 371 | specify { expect(resource.emails[1][:value]).to be_present } 372 | specify { expect(resource.emails[1][:primary]).to be(false) } 373 | specify { expect(resource.locale).to eql('en') } 374 | specify { expect(resource.timezone).to eql('Etc/UTC') } 375 | 376 | specify { expect(resource.to_h[:userName]).to eql(user_name) } 377 | specify { expect(resource.to_h[:name][:givenName]).to eql('Barbara') } 378 | specify { expect(resource.to_h[:name][:familyName]).to eql('Jensen') } 379 | specify { expect(resource.to_h[:emails][0][:value]).to be_present } 380 | specify { expect(resource.to_h[:emails][0][:primary]).to be(true) } 381 | specify { expect(resource.to_h[:emails][1][:value]).to be_present } 382 | specify { expect(resource.to_h[:emails][1][:primary]).to be(false) } 383 | specify { expect(resource.to_h[:locale]).to eql('en') } 384 | specify { expect(resource.to_h[:timezone]).to eql('Etc/UTC') } 385 | specify { expect(resource.to_h.key?(:meta)).to be(false) } 386 | specify { expect(resource.to_h.key?(:id)).to be(false) } 387 | specify { expect(resource.to_h.key?(:external_id)).to be(false) } 388 | end 389 | 390 | context 'when building in client mode' do 391 | subject { described_class.new(schemas: schemas) } 392 | 393 | let(:external_id) { SecureRandom.uuid } 394 | 395 | before do 396 | subject.password = FFaker::Internet.password 397 | subject.external_id = external_id 398 | end 399 | 400 | specify { expect(subject.to_h.key?(:id)).to be(false) } 401 | specify { expect(subject.to_h.key?(:externalId)).to be(true) } 402 | specify { expect(subject.to_h[:externalId]).to eql(external_id) } 403 | specify { expect(subject.to_h.key?(:meta)).to be(false) } 404 | specify { expect(subject.to_h.key?(:userName)).to be(true) } 405 | specify { expect(subject.to_h[:name].key?(:formatted)).to be(false) } 406 | specify { expect(subject.to_h[:name].key?(:familyName)).to be(true) } 407 | specify { expect(subject.to_h[:name].key?(:givenName)).to be(true) } 408 | specify { expect(subject.to_h.key?(:displayName)).to be(false) } 409 | specify { expect(subject.to_h.key?(:locale)).to be(true) } 410 | specify { expect(subject.to_h.key?(:timezone)).to be(true) } 411 | specify { expect(subject.to_h.key?(:active)).to be(true) } 412 | specify { expect(subject.to_h.key?(:password)).to be(true) } 413 | specify { expect(subject.to_h.key?(:emails)).to be(true) } 414 | specify { expect(subject.to_h.key?(:groups)).to be(false) } 415 | end 416 | 417 | context 'when building in server mode' do 418 | subject { described_class.new(schemas: schemas, location: resource_location) } 419 | 420 | before do 421 | subject.external_id = SecureRandom.uuid 422 | end 423 | 424 | specify { expect(subject.to_h.key?(:id)).to be(true) } 425 | specify { expect(subject.to_h.key?(:externalId)).to be(false) } 426 | specify { expect(subject.to_h.key?(:meta)).to be(true) } 427 | specify { expect(subject.to_h.key?(:userName)).to be(true) } 428 | specify { expect(subject.to_h[:name].key?(:formatted)).to be(true) } 429 | specify { expect(subject.to_h[:name].key?(:familyName)).to be(true) } 430 | specify { expect(subject.to_h[:name].key?(:givenName)).to be(true) } 431 | specify { expect(subject.to_h.key?(:displayName)).to be(true) } 432 | specify { expect(subject.to_h.key?(:locale)).to be(true) } 433 | specify { expect(subject.to_h.key?(:timezone)).to be(true) } 434 | specify { expect(subject.to_h.key?(:active)).to be(true) } 435 | specify { expect(subject.to_h.key?(:password)).to be(false) } 436 | specify { expect(subject.to_h.key?(:emails)).to be(true) } 437 | specify { expect(subject.to_h.key?(:groups)).to be(true) } 438 | end 439 | end 440 | 441 | describe '#mode?' do 442 | context 'when server mode' do 443 | subject { described_class.new(schemas: schemas, location: resource_location) } 444 | 445 | specify { expect(subject).to be_mode(:server) } 446 | specify { expect(subject).not_to be_mode(:client) } 447 | end 448 | 449 | context 'when client mode' do 450 | subject { described_class.new(schemas: schemas) } 451 | 452 | specify { expect(subject).not_to be_mode(:server) } 453 | specify { expect(subject).to be_mode(:client) } 454 | end 455 | end 456 | 457 | describe '#assign_attributes' do 458 | context 'with a simple string attribute' do 459 | let(:user_name) { FFaker::Internet.user_name } 460 | 461 | before do 462 | schema.add_attribute(name: 'userName') 463 | subject.assign_attributes('schemas' => schemas.map(&:id), userName: user_name) 464 | end 465 | 466 | specify { expect(subject.user_name).to eql(user_name) } 467 | end 468 | 469 | context 'with a simple integer attribute' do 470 | before do 471 | schema.add_attribute(name: 'age', type: :integer) 472 | subject.assign_attributes(schemas: schemas.map(&:id), age: 34) 473 | end 474 | 475 | specify { expect(subject.age).to be(34) } 476 | end 477 | 478 | context 'with a multi-valued simple string attribute' do 479 | before do 480 | schema.add_attribute(name: 'colours', type: :string) do |x| 481 | x.multi_valued = true 482 | end 483 | subject.assign_attributes(schemas: schemas.map(&:id), colours: ['red', 'green', :blue]) 484 | end 485 | 486 | specify { expect(subject.colours).to match_array(%w[red green blue]) } 487 | end 488 | 489 | context 'with a single complex attribute' do 490 | before do 491 | schema.add_attribute(name: :name) do |x| 492 | x.add_attribute(name: :given_name) 493 | x.add_attribute(name: :family_name) 494 | end 495 | subject.assign_attributes(schemas: schemas.map(&:id), name: { givenName: 'Tsuyoshi', familyName: 'Garrett' }) 496 | end 497 | 498 | specify { expect(subject.name.given_name).to eql('Tsuyoshi') } 499 | specify { expect(subject.name.family_name).to eql('Garrett') } 500 | end 501 | 502 | context 'with a multi-valued complex attribute' do 503 | let(:email) { FFaker::Internet.email } 504 | let(:other_email) { FFaker::Internet.email } 505 | 506 | before do 507 | schema.add_attribute(name: :emails) do |x| 508 | x.multi_valued = true 509 | x.add_attribute(name: :value) 510 | x.add_attribute(name: :primary, type: :boolean) 511 | end 512 | subject.assign_attributes(schemas: schemas.map(&:id), emails: [ 513 | { value: email, primary: true }, 514 | { value: other_email, primary: false } 515 | ]) 516 | end 517 | 518 | specify do 519 | expect(subject.emails).to match_array([ 520 | { value: email, primary: true }, 521 | { value: other_email, primary: false } 522 | ]) 523 | end 524 | 525 | specify { expect(subject.emails[0][:value]).to eql(email) } 526 | specify { expect(subject.emails[0][:primary]).to be(true) } 527 | specify { expect(subject.emails[1][:value]).to eql(other_email) } 528 | specify { expect(subject.emails[1][:primary]).to be(false) } 529 | end 530 | 531 | context 'with an extension schema' do 532 | let(:schemas) { [schema, extension] } 533 | let(:extension) { Scim::Kit::V2::Schema.new(id: extension_id, name: 'Extension', location: FFaker::Internet.uri('https')) } 534 | let(:extension_id) { Scim::Kit::V2::Schemas::ENTERPRISE_USER } 535 | 536 | before do 537 | extension.add_attribute(name: :preferred_name) 538 | subject.assign_attributes( 539 | schemas: schemas.map(&:id), 540 | extension_id => { preferredName: 'hunk' } 541 | ) 542 | end 543 | 544 | specify { expect(subject.preferred_name).to eql('hunk') } 545 | end 546 | 547 | context 'when initializing the resource with attributes' do 548 | subject { described_class.new(schemas: schemas, attributes: attributes) } 549 | 550 | let(:user_name) { FFaker::Internet.user_name } 551 | let(:email) { FFaker::Internet.email } 552 | let(:attributes) do 553 | { 554 | schemas: schemas.map(&:id), 555 | userName: user_name, 556 | age: 34, 557 | colours: %w[red green blue], 558 | name: { given_name: 'Tsuyoshi', family_name: 'Garrett' }, 559 | emails: [{ value: email, primary: true }] 560 | } 561 | end 562 | 563 | before do 564 | schema.add_attribute(name: :user_name) 565 | schema.add_attribute(name: :age, type: :integer) 566 | schema.add_attribute(name: :colours, type: :string) do |x| 567 | x.multi_valued = true 568 | end 569 | schema.add_attribute(name: :name) do |x| 570 | x.add_attribute(name: :given_name) 571 | x.add_attribute(name: :family_name) 572 | end 573 | schema.add_attribute(name: :emails) do |x| 574 | x.multi_valued = true 575 | x.add_attribute(name: :value) 576 | x.add_attribute(name: :primary, type: :boolean) 577 | end 578 | end 579 | 580 | specify { expect(subject.raw_attributes).to eql(attributes) } 581 | specify { expect(subject.user_name).to eql(user_name) } 582 | specify { expect(subject.age).to be(34) } 583 | specify { expect(subject.colours).to match_array(%w[red green blue]) } 584 | specify { expect(subject.name.given_name).to eql('Tsuyoshi') } 585 | specify { expect(subject.name.family_name).to eql('Garrett') } 586 | specify { expect(subject.emails[0][:value]).to eql(email) } 587 | specify { expect(subject.emails[0][:primary]).to be(true) } 588 | 589 | specify do 590 | attributes = { schemas: schemas.map(&:id), unknown: 'unknown' } 591 | expect do 592 | described_class.new(schemas: schemas, attributes: attributes) 593 | end.to raise_error(Scim::Kit::UnknownAttributeError) 594 | end 595 | end 596 | end 597 | 598 | describe 'Errors' do 599 | subject { described_class.new(schemas: schemas) } 600 | 601 | let(:schemas) { [Scim::Kit::V2::Error.default_schema] } 602 | 603 | before do 604 | subject.scim_type = :invalidSyntax 605 | subject.detail = 'error' 606 | subject.status = 400 607 | end 608 | 609 | specify { expect(subject.to_h[:schemas]).to match_array([Scim::Kit::V2::Messages::ERROR]) } 610 | specify { expect(subject.to_h[:scimType]).to eql('invalidSyntax') } 611 | specify { expect(subject.to_h[:detail]).to eql('error') } 612 | specify { expect(subject.to_h[:status]).to eql('400') } 613 | end 614 | end 615 | --------------------------------------------------------------------------------