Commit 18581b9
Changed files (11)
lib/scim/kit/cli/schemas/resource_types.schema.json
@@ -1,42 +1,53 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "schemas": { "type": "array", "items": { "type": "string" } },
- "id": { "type": "string" },
- "name": { "type": "string" },
- "description": { "type": "string" },
- "endpoint": { "type": "string" },
- "schema": { "type": "string" },
- "schemaExtensions": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "schema": { "type": "string" },
- "required": { "type": "boolean" }
- },
- "required": ["schema", "required"],
- "additionalProperties": false
- }
- },
- "externalId": { "type": "string" },
- "meta": {
+ "type": "object",
+ "properties": {
+ "schemas": { "type": "array", "items": { "type": "string" } },
+ "totalResults": { "type": "integer" },
+ "itemsPerPage": { "type": "integer" },
+ "startIndex": { "type": "integer" },
+ "Resources": {
+ "type": "array",
+ "items": {
"type": "object",
"properties": {
- "resourceType": { "type": "string" },
- "created": { "type": "string", "format": "date-time" },
- "lastModified": { "type": "string", "format": "date-time" },
- "location": { "type": "string" },
- "version": { "type": "string" }
+ "schemas": { "type": "array", "items": { "type": "string" } },
+ "id": { "type": "string" },
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "endpoint": { "type": "string" },
+ "schema": { "type": "string" },
+ "schemaExtensions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "schema": { "type": "string" },
+ "required": { "type": "boolean" }
+ },
+ "required": ["schema", "required"],
+ "additionalProperties": false
+ }
+ },
+ "externalId": { "type": "string" },
+ "meta": {
+ "type": "object",
+ "properties": {
+ "resourceType": { "type": "string" },
+ "created": { "type": "string", "format": "date-time" },
+ "lastModified": { "type": "string", "format": "date-time" },
+ "location": { "type": "string" },
+ "version": { "type": "string" }
+ },
+ "required": [],
+ "additionalProperties": false
+ }
},
- "required": [],
+ "required": ["schemas", "name", "endpoint", "schema"],
"additionalProperties": false
}
- },
- "required": ["schemas", "name", "endpoint", "schema"],
- "additionalProperties": false
- }
+ }
+ },
+ "required": ["schemas", "totalResults", "Resources"],
+ "additionalProperties": false
}
lib/scim/kit/cli/schemas/schemas.schema.json
@@ -1,23 +1,34 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "schemas": { "type": "array", "items": { "type": "string" } },
- "id": { "type": "string" },
- "name": { "type": "string" },
- "description": { "type": "string" },
- "attributes": {
- "type": "array",
- "items": { "$ref": "#/$defs/attribute" }
- },
- "externalId": { "type": "string" },
- "meta": { "$ref": "#/$defs/meta" }
- },
- "required": ["schemas", "id", "attributes"],
- "additionalProperties": false
+ "type": "object",
+ "properties": {
+ "schemas": { "type": "array", "items": { "type": "string" } },
+ "totalResults": { "type": "integer" },
+ "itemsPerPage": { "type": "integer" },
+ "startIndex": { "type": "integer" },
+ "Resources": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "schemas": { "type": "array", "items": { "type": "string" } },
+ "id": { "type": "string" },
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "attributes": {
+ "type": "array",
+ "items": { "$ref": "#/$defs/attribute" }
+ },
+ "externalId": { "type": "string" },
+ "meta": { "$ref": "#/$defs/meta" }
+ },
+ "required": ["schemas", "id", "attributes"],
+ "additionalProperties": false
+ }
+ }
},
+ "required": ["schemas", "totalResults", "Resources"],
+ "additionalProperties": false,
"$defs": {
"attribute": {
"type": "object",
lib/scim/kit/cli/schemas/service_provider_config.schema.json
@@ -58,7 +58,8 @@
"name": { "type": "string" },
"description": { "type": "string" },
"specUri": { "type": "string" },
- "documentationUri": { "type": "string" }
+ "documentationUri": { "type": "string" },
+ "primary": { "type": "boolean" }
},
"required": ["type", "name", "description"],
"additionalProperties": false
lib/scim/kit/cli/resource_schema_resolver.rb
@@ -85,9 +85,10 @@ module Scim
def fetch_schemas(urns)
uri = Cli.join_uri(base_url, 'Schemas')
result = http.fetch(uri, headers: headers)
- return {} unless result.ok? && result.body.is_a?(Array)
+ schemas = result.ok? ? Cli.collection(result.body) : nil
+ return {} if schemas.nil?
- result.body.each_with_object({}) do |schema, hash|
+ schemas.each_with_object({}) do |schema, hash|
hash[schema[:id]] = schema if urns.include?(schema[:id])
end
end
lib/scim/kit/cli/resource_type_resolver.rb
@@ -32,11 +32,13 @@ module Scim
uri = Cli.join_uri(base_url, 'ResourceTypes')
result = http.fetch(uri, headers: headers)
raise RequestFailed, result unless result.ok?
- unless result.body.is_a?(Array)
+
+ types = Cli.collection(result.body)
+ unless types
raise InvalidResponse, 'expected /ResourceTypes to return a list'
end
- result.body
+ types
end
end
end
lib/scim/kit/cli.rb
@@ -42,6 +42,17 @@ module Scim
def self.join_uri(base_url, path)
URI.join("#{base_url.to_s.sub(%r{/+\z}, '')}/", path)
end
+
+ # SCIM collection endpoints (/Schemas, /ResourceTypes) return a
+ # ListResponse per RFC 7644 section 4, but some servers return a
+ # bare array. Return the underlying array for either shape, or nil.
+ def self.collection(body)
+ return body if body.is_a?(Array)
+ return unless body.is_a?(Hash)
+
+ resources = body[:Resources]
+ resources if resources.is_a?(Array)
+ end
end
end
end
spec/scim/kit/cli/app_spec.rb
@@ -127,24 +127,32 @@ RSpec.describe Scim::Kit::Cli::App do
}
end
let(:schemas) do
- [
- {
- id: 'urn:ietf:params:scim:schemas:core:2.0:User',
- schemas: ['urn:ietf:params:scim:schemas:core:2.0:Schema'],
- attributes: [
- { name: 'userName', type: 'string', required: true }
- ]
- }
- ]
+ {
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
+ totalResults: 1,
+ Resources: [
+ {
+ id: 'urn:ietf:params:scim:schemas:core:2.0:User',
+ schemas: ['urn:ietf:params:scim:schemas:core:2.0:Schema'],
+ attributes: [
+ { name: 'userName', type: 'string', required: true }
+ ]
+ }
+ ]
+ }
end
let(:resource_types) do
- [
- {
- schemas: ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
- name: 'User', endpoint: '/Users',
- schema: 'urn:ietf:params:scim:schemas:core:2.0:User'
- }
- ]
+ {
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
+ totalResults: 1,
+ Resources: [
+ {
+ schemas: ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
+ name: 'User', endpoint: '/Users',
+ schema: 'urn:ietf:params:scim:schemas:core:2.0:User'
+ }
+ ]
+ }
end
before do
@@ -165,6 +173,34 @@ RSpec.describe Scim::Kit::Cli::App do
end
end
+ context 'when --validate is set and a collection is a bare array' do
+ let(:schemas) do
+ [
+ {
+ id: 'urn:ietf:params:scim:schemas:core:2.0:User',
+ schemas: ['urn:ietf:params:scim:schemas:core:2.0:Schema'],
+ attributes: [{ name: 'userName', type: 'string' }]
+ }
+ ]
+ end
+
+ before do
+ stub_request(:get, "#{base_url}/ServiceProviderConfig").to_return(
+ status: 200, body: service_provider_configuration.to_json
+ )
+ stub_request(:get, "#{base_url}/Schemas")
+ .to_return(status: 200, body: schemas.to_json)
+ end
+
+ it 'flags the bare array as non-compliant with ListResponse format' do
+ allow($stdout).to receive(:print)
+ instance = app('validate' => true)
+
+ expect { exit_status { instance.discover } }
+ .to output(/root is not of type: object/).to_stderr
+ end
+ end
+
context 'when --validate is set and a document is invalid' do
let(:service_provider_configuration) { { patch: { supported: true } } }
let(:schemas) { [{ id: 'User', name: 'User' }] }
spec/scim/kit/cli/resource_schema_resolver_spec.rb
@@ -130,5 +130,28 @@ RSpec.describe Scim::Kit::Cli::ResourceSchemaResolver do
)
end
end
+
+ context 'when /Schemas returns a ListResponse envelope' do
+ let(:resource_type) do
+ { id: 'User', name: 'User', endpoint: '/Users', schema: core_urn }
+ end
+
+ before do
+ stub_request(:get, "#{base_url}/Schemas").to_return(
+ status: 200,
+ body: {
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
+ totalResults: 1,
+ Resources: [core_schema]
+ }.to_json
+ )
+ end
+
+ it 'composes the schema from the Resources array' do
+ schema = subject.schema_for(resource_type)
+
+ expect(schema['properties']).to include('userName')
+ end
+ end
end
end
spec/scim/kit/cli/resource_type_resolver_spec.rb
@@ -44,6 +44,23 @@ RSpec.describe Scim::Kit::Cli::ResourceTypeResolver do
expect(a_request(:get, "#{base_url}/ResourceTypes").with(headers: headers)).to have_been_made
end
end
+
+ context 'when /ResourceTypes returns a ListResponse envelope' do
+ before do
+ stub_request(:get, "#{base_url}/ResourceTypes").to_return(
+ status: 200,
+ body: {
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
+ totalResults: 1,
+ Resources: [{ id: 'User', name: 'User', endpoint: '/Users' }]
+ }.to_json
+ )
+ end
+
+ it 'resolves the entry from the Resources array' do
+ expect(subject.resource_type_for('User')).to include(endpoint: '/Users')
+ end
+ end
end
context 'when the request fails' do
@@ -54,11 +71,11 @@ RSpec.describe Scim::Kit::Cli::ResourceTypeResolver do
end
end
- context 'when the response body is not a list' do
+ context 'when the response body is neither a list nor a ListResponse' do
before do
stub_request(:get, "#{base_url}/ResourceTypes").to_return(
status: 200,
- body: { Resources: [] }.to_json
+ body: { detail: 'not a collection' }.to_json
)
end
spec/scim/kit/cli/schema_registry_spec.rb
@@ -8,16 +8,60 @@ RSpec.describe Scim::Kit::Cli::SchemaRegistry do
expect(schema['required']).to include('patch', 'bulk')
end
- it 'loads the resource_types schema' do
+ it 'expects resource_types wrapped in a ListResponse envelope' do
schema = described_class.fetch(:resource_types)
- expect(schema['items']['required']).to include('endpoint')
+ expect(schema['required']).to include('totalResults', 'Resources')
end
- it 'loads the schemas schema' do
+ it 'validates resource_type items inside the envelope' do
+ schema = described_class.fetch(:resource_types)
+
+ items = schema['properties']['Resources']['items']
+ expect(items['required']).to include('endpoint')
+ end
+
+ it 'expects schemas wrapped in a ListResponse envelope' do
+ schema = described_class.fetch(:schemas)
+
+ expect(schema['required']).to include('totalResults', 'Resources')
+ end
+
+ it 'validates schema items inside the envelope' do
schema = described_class.fetch(:schemas)
- expect(schema['items']['required']).to include('attributes')
+ items = schema['properties']['Resources']['items']
+ expect(items['required']).to include('attributes')
+ end
+ end
+
+ describe 'service_provider_configuration schema' do
+ let(:config) do
+ {
+ schemas: [
+ 'urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'
+ ],
+ patch: { supported: true },
+ bulk: { supported: false, maxOperations: 0, maxPayloadSize: 0 },
+ filter: { supported: false, maxResults: 0 },
+ changePassword: { supported: false },
+ sort: { supported: false },
+ etag: { supported: false },
+ authenticationSchemes: [
+ {
+ type: 'oauthbearertoken', name: 'OAuth Bearer Token',
+ description: 'desc', primary: true
+ }
+ ]
+ }
+ end
+
+ it 'accepts a primary flag on authentication schemes' do
+ schema = described_class.fetch(:service_provider_configuration)
+
+ errors = Scim::Kit::Cli::Validator.errors_for(schema, config)
+
+ expect(errors).to be_empty
end
end
spec/scim/kit/cli_spec.rb
@@ -20,4 +20,24 @@ RSpec.describe Scim::Kit::Cli do
expect(uri.to_s).to eql('https://example.com/scim/v2/Users')
end
end
+
+ describe '.collection' do
+ it 'returns a bare array unchanged' do
+ expect(described_class.collection([{ id: '1' }])).to eql([{ id: '1' }])
+ end
+
+ it 'unwraps the Resources array from a ListResponse envelope' do
+ body = { totalResults: 1, Resources: [{ id: '1' }] }
+
+ expect(described_class.collection(body)).to eql([{ id: '1' }])
+ end
+
+ it 'returns nil for a hash without a Resources array' do
+ expect(described_class.collection(detail: 'boom')).to be_nil
+ end
+
+ it 'returns nil for a scalar body' do
+ expect(described_class.collection('nope')).to be_nil
+ end
+ end
end