ecoportal-scim 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a7ad495de878dc33d4bc202d34b37b8409a42acf1ab47777864e325cdfebcb29
4
+ data.tar.gz: 25369ed1936d1e264cd8cf2098fd706f576980fad5b4ca8ebfa92b9184459042
5
+ SHA512:
6
+ metadata.gz: 057dc11619874eb6c4b8e625095e5997a5f6c620f7faa1a8409a9c45c2690299ea734d60357ca305a056c1c4f11e7582db9455beb06adf9b67cf80e409edf46a
7
+ data.tar.gz: 668f1f294873a9307b61de0ee7bcaf4d4badd44dda945813c108283ed8bdded477e322781b8996749cbf078f8f4222db7b9d98f41ea562e071eb3d86d37a3ef3
data/CHANGELOG.md ADDED
@@ -0,0 +1,46 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-11-01
8
+
9
+ ### Added
10
+
11
+ - First commit.
12
+ - `Groups` endpoint (list, get, patch), mirroring the `Users` endpoint architecture.
13
+ Supports the SCIM `PatchOp` membership flip (`add_members`/`remove_members`) needed
14
+ by the permission-verification test suite.
15
+ - `Users` endpoint (list, get) -- read-only for now. `#each` enumerates the SCIM
16
+ `ListResponse` `Resources` the same way as `Groups`. `User` resource stays a Hash-shape
17
+ placeholder (see `Groups::Group`) pending the attribute model needed for the
18
+ APIv0-People adapter.
19
+ - `Users::User` attribute readers (`id`, `user_name`, `name`/`given_name`/`family_name`,
20
+ `active?`, `external_id`, `emails`, `phone_numbers`), as an optional read-only wrapper
21
+ over a SCIM `User` Hash (`Users::User.new(scim.users.get(id))`). `Users#list`/`#get`
22
+ keep returning plain Hashes -- no change to the endpoint's public interface.
23
+ - `Users::User#manager`/`#manager_id`/`#enterprise_extension` readers for the SCIM
24
+ enterprise-user extension's `manager` attribute, verified against the ecoPortal backend
25
+ (internal code spec `scim-users`): namespaced under
26
+ `urn:ietf:params:scim:schemas:extension:enterprise:2.0:User`, shape narrowed to `{value}`
27
+ only (no `$ref`/`displayName`), `value` is the supervisor's ecoPortal-internal id.
28
+ - README: documented the Users resource semantics (`active` is the enabled account, not the
29
+ archived flag; 500-per-page cap and paging; rate-limit guidance; no Bulk support) and how to
30
+ read the ecoPortal `requiredfields`/`schemafields` extension attributes from a `Users#list`
31
+ response.
32
+
33
+ ### Changed
34
+
35
+ ### Fixed
36
+
37
+ - `Ecoportal::API::Scim#initialize` passed `scim_key:` to `Common::Scim::Client`, whose
38
+ keyword is `api_key:` -- the top-level entry point raised `ArgumentError` for every
39
+ endpoint (`users`/`groups`/`schemas`).
40
+
41
+ - **Packaging allowlist hardened from root directory to full path.** `spec.files` allowlisted
42
+ `lib/` and `exe/` by directory, which says nothing about what a file under those roots IS --
43
+ only `.rb` now ships out of `lib/`. Preemptive: this gem has not leaked anything (confirmed
44
+ no non-`.rb` file is tracked under `lib/`), but the same directory-keyed allowlist shape had
45
+ already caused a near-miss in this gem before its first release, and a real incident in
46
+ ecoportal-api-graphql (2.2.0 shipped five `CLAUDE.md` files that lived under its `lib/`).
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ecoPortal
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Ecoportal::API::Scim
2
+
3
+ This is a basic collection of helpers that can be used to interact with the
4
+ [ecoPortal](https://www.ecoportal.com) SCIM integration.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'ecoportal-scim', require: %w[ecoportal/scim]
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install ecoportal-scim
21
+
22
+ ## Endpoints
23
+
24
+ `Ecoportal::API::Scim` exposes four SCIM resources, each reachable from the top-level client:
25
+
26
+ - `Groups` -- list, get and patch SCIM groups, including member add/remove.
27
+ - `Users` -- list and get SCIM users (read-only).
28
+ - `Schemas` -- list the SCIM schemas the api advertises.
29
+ - `ServiceProviderConfig` -- fetch the api's SCIM service provider configuration.
30
+
31
+ ```ruby
32
+ scim = Ecoportal::API::Scim.new(scim_id, scim_key)
33
+
34
+ scim.users.get(user_id) # => Hash, the SCIM User resource
35
+ scim.groups.list # => Hash, a SCIM ListResponse
36
+ scim.schemas.list
37
+ scim.service_provider_config.get
38
+ ```
39
+
40
+ ### Groups
41
+
42
+ `Groups#add_members`/`#remove_members` send a SCIM `PatchOp` `add`/`remove` operation against
43
+ the group's `members` path (RFC 7644 section 3.5.2).
44
+
45
+ **Hazard -- `remove_members(id, [])` or `remove_members(id, nil)`:** both send
46
+ `{"op"=>"remove", "path"=>"members", "value"=>[]}` -- a `remove` with a `path` and an empty
47
+ `value` array, not an absent `value`. Per RFC 7644 section 3.5.2.2, a `remove` with a `path`
48
+ and **no** `value` means "remove everything at that path"; whether the ecoPortal SCIM server
49
+ treats "empty `value` array" the same way as "no `value`" is **unverified**. If it does,
50
+ passing an empty or nil list -- an easy thing to do by accident, e.g. forwarding an empty
51
+ filter result straight into `remove_members` -- could empty the entire group instead of being
52
+ a no-op. **Guard the empty/nil case yourself** (skip the call, or assert non-empty) until this
53
+ is confirmed against the api.
54
+
55
+ ### Users
56
+
57
+ `Users#list` and `Users#get` return the api's plain parsed Hash bodies -- that return contract
58
+ is unchanged and does not require anything below.
59
+
60
+ `Users::User` is an optional, read-only wrapper for when you want typed accessors instead of
61
+ raw Hash keys:
62
+
63
+ ```ruby
64
+ user = Ecoportal::API::Scim::Users::User.new(scim.users.get(user_id))
65
+
66
+ user.id # => the SCIM `id`
67
+ user.user_name # => the SCIM `userName`
68
+ user.name # => { 'givenName' => ..., 'familyName' => ... }
69
+ user.given_name
70
+ user.family_name
71
+ user.active? # => true only when the api reports `active: true`
72
+ user.external_id
73
+ user.emails # => the `emails` multi-valued attribute, [] if absent
74
+ user.phone_numbers # => the `phoneNumbers` multi-valued attribute, [] if absent
75
+ user.doc # => the raw Hash it wraps
76
+ user['userName'] # => raw Hash access, for attributes without a reader
77
+ ```
78
+
79
+ #### `manager` / supervisor
80
+
81
+ The supervisor relationship is exposed under the SCIM enterprise-user extension, not the core
82
+ `User` schema:
83
+
84
+ ```ruby
85
+ user.enterprise_extension # => the Hash keyed by the enterprise extension URI, {} if absent
86
+ user.manager # => the raw `manager` complex attribute, {} if absent
87
+ user.manager_id # => manager['value'], nil if absent
88
+ ```
89
+
90
+ A few things worth knowing before relying on this:
91
+
92
+ - **Narrower than stock SCIM.** ecoPortal's `manager` only ever carries `value` -- there is no
93
+ `$ref` and no `displayName`, unlike the generic SCIM enterprise-user schema (RFC 7643 4.3).
94
+ Code written against the RFC shape will look for fields that never arrive.
95
+ - **Read/write asymmetry.** On read, `manager_id` is always the supervisor's ecoPortal-internal
96
+ id, never their `externalId`. On write (directly against the api, not yet exposed by this
97
+ gem), ecoPortal accepts either the internal id or the `externalId` and resolves it server-side.
98
+ - **Absent means omitted, not null.** When a user has no supervisor, ecoPortal leaves `manager`
99
+ out of the enterprise extension entirely -- it is never emitted as `null` or an empty object.
100
+ This gem's `#manager` reader normalizes that absence to `{}` (and `#manager_id` to `nil`);
101
+ `#enterprise_extension` itself returns `{}` only if the api omitted the extension namespace.
102
+
103
+ The behaviour described above is what the `Users::User` readers are written against. If ecoPortal changes it, those readers need to change with it.
104
+
105
+ ## Changelog
106
+
107
+ See {file:CHANGELOG.md} for a list of changes.
@@ -0,0 +1,28 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Concerns
5
+ module EndPoint
6
+ class << self
7
+ private
8
+
9
+ def included(base)
10
+ super
11
+
12
+ base.extend Ecoportal::API::Common::BaseClass
13
+ base.send :include, Ecoportal::API::Common::Logging
14
+ end
15
+ end
16
+
17
+ attr_reader :client
18
+
19
+ # @param client [Common::Client] a `Common::Client` object that holds the configuration of the api connection.
20
+ # @return [Users] an instance object ready to make user Scim interactions.
21
+ def initialize(client)
22
+ @client = client
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,10 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Concerns
5
+ end
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative 'concerns/end_point'
@@ -0,0 +1,58 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Scim
5
+ # @see Ecoportal::API::Common::Client
6
+ class Client < Ecoportal::API::Common::Client
7
+ MAIN_END_POINT = 'scim'.freeze
8
+
9
+ attr_accessor :logger
10
+
11
+ # @note the `scim_key` will be automatically added as parameter
12
+ # `Authentication: Bearer` in the header of the http requests.
13
+ def initialize(
14
+ api_key:,
15
+ scim_id:,
16
+ version: 'v2',
17
+ host: 'live.ecoportal.com',
18
+ logger: nil
19
+ )
20
+ @scim_id = scim_id
21
+
22
+ super(api_key: api_key, version: version, host: host, logger: logger)
23
+
24
+ if host.match(/^localhost|^127\.0\.0\.1/)
25
+ @base_uri = "http://#{host}/scim/"
26
+ else
27
+ @base_uri = "https://#{host}/scim/"
28
+ end
29
+ end
30
+
31
+ def delete(_path)
32
+ raise "DELETE operation does not have integration for api #{@version}"
33
+ end
34
+
35
+ # Full URl builder of the request
36
+ # @param path [String] the tail that completes the url of the request.
37
+ # @return [String] the final url.
38
+ def url_for(path)
39
+ "#{super('')}/#{scim_id}#{path}"
40
+ end
41
+
42
+ # Creates a HTTP object adding the `Authorization: Bearer KEY` param to the header.
43
+ # @note It configures HTTP so it only allows body data in json+scim format.
44
+ # @return [HTTP] HTTP object.
45
+ def base_request
46
+ @base_request ||= HTTP.headers(
47
+ 'Authorization' => "Bearer #{api_key}"
48
+ ).accept(:'json+scim')
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :scim_id
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,16 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Scim
5
+ module Includer
6
+ def include_missing(base, target)
7
+ return base if base <= target
8
+
9
+ base.send(:include, target)
10
+ base
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Scim
5
+ end
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative 'scim/includer'
11
+ require_relative 'scim/client'
@@ -0,0 +1,13 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ unless Ecoportal::API.const_defined?(:SCIM_VERSION)
5
+ msg = "Correct way to require this gem is `require 'ecoportal/scim`"
6
+ raise LoadError, msg
7
+ end
8
+ end
9
+ end
10
+ end
11
+
12
+ require_relative 'common/concerns'
13
+ require_relative 'common/scim'
@@ -0,0 +1,17 @@
1
+ module Ecoportal
2
+ module API
3
+ class Scim
4
+ class Groups
5
+ # SCIM Group resource, per RFC 7643 section 4.2.
6
+ # @note kept as a placeholder: `Groups` works against the plain parsed Hash
7
+ # bodies returned by the api (`id`, `displayName`, `members` -- an array
8
+ # of `{value, display}`). When it gains attribute readers, mirror the
9
+ # Hash-wrapper shape of `Users::User`.
10
+ class Group
11
+ # SCIM core schema URI for `Group`, per RFC 7643 section 4.2.
12
+ SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:Group'.freeze
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,107 @@
1
+ module Ecoportal
2
+ module API
3
+ class Scim
4
+ # @attr_reader client [Common::Client] a `Common::Client` object that
5
+ # holds the configuration of the api connection.
6
+ class Groups
7
+ include Ecoportal::API::Common::Concerns::EndPoint
8
+ include Enumerable
9
+
10
+ class_resolver :group_class, 'Ecoportal::API::Scim::Groups::Group'
11
+
12
+ END_POINT = '/Groups'.freeze
13
+ PATCH_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:PatchOp'.freeze
14
+
15
+ # Lists the groups of the organization.
16
+ # @note per RFC 7644 section 3.4.2, the response is a SCIM `ListResponse`
17
+ # (`totalResults`, `startIndex`, `itemsPerPage`, `Resources`).
18
+ # @param filter [String] a SCIM filter expression.
19
+ # @param start_index [Integer] the 1-based index of the first result (SCIM `startIndex`).
20
+ # @param count [Integer] the maximum number of results (SCIM `count`).
21
+ # @return [Hash] the SCIM `ListResponse`.
22
+ def list(filter: nil, start_index: nil, count: nil)
23
+ params = {}
24
+ params[:filter] = filter if filter
25
+ params[:startIndex] = start_index if start_index
26
+ params[:count] = count if count
27
+
28
+ response = client.get(END_POINT, params: params)
29
+ body = response.body
30
+ return body if response.success?
31
+
32
+ raise "Could not list groups - Error #{response.status}: #{body}"
33
+ end
34
+
35
+ # Iterates all the groups returned by `list`.
36
+ # @param filter [String] a SCIM filter expression.
37
+ # @yield [group] does something with the group resource.
38
+ # @yieldparam group [Hash]
39
+ def each(filter: nil, &block)
40
+ return to_enum(:each, filter: filter) unless block_given?
41
+
42
+ list(filter: filter).fetch('Resources', []).each(&block)
43
+ end
44
+
45
+ # Gets a group via api.
46
+ # @param id [String] the `id` of the target group.
47
+ # @return [Hash] the group resource.
48
+ def get(id)
49
+ response = client.get("#{END_POINT}/#{CGI.escape(id)}")
50
+ body = response.body
51
+ return body if response.success?
52
+
53
+ raise "Could not get group #{id} - Error #{response.status}: #{body}"
54
+ end
55
+
56
+ # Sends a raw SCIM `PatchOp` request against a group.
57
+ # @note per RFC 7644 section 3.5.2.
58
+ # @param id [String] the `id` of the target group.
59
+ # @param operations [Array<Hash>] the SCIM `PatchOp` `Operations`.
60
+ # @return [Hash] the updated group resource.
61
+ def patch(id, operations:)
62
+ data = {
63
+ schemas: [PATCH_SCHEMA],
64
+ Operations: operations
65
+ }
66
+
67
+ response = client.patch("#{END_POINT}/#{CGI.escape(id)}", data: data)
68
+ body = response.body
69
+ return body if response.success?
70
+
71
+ raise "Could not patch group #{id} - Error #{response.status}: #{body}"
72
+ end
73
+
74
+ # Adds members to a group.
75
+ # @param id [String] the `id` of the target group.
76
+ # @param user_ids [Array<String>, String] the `id`s of the users to add.
77
+ # @return [Hash] the updated group resource.
78
+ def add_members(id, user_ids)
79
+ patch(id, operations: [member_operation('add', user_ids)])
80
+ end
81
+
82
+ # Removes members from a group.
83
+ # @param id [String] the `id` of the target group.
84
+ # @param user_ids [Array<String>, String] the `id`s of the users to remove.
85
+ # @return [Hash] the updated group resource.
86
+ def remove_members(id, user_ids)
87
+ patch(id, operations: [member_operation('remove', user_ids)])
88
+ end
89
+
90
+ private
91
+
92
+ # @param op_name [String] `add` or `remove`.
93
+ # @param user_ids [Array<String>, String] the `id`s of the target users.
94
+ # @return [Hash] a single SCIM `PatchOp` operation targeting `members`.
95
+ def member_operation(op_name, user_ids)
96
+ {
97
+ 'op' => op_name,
98
+ 'path' => 'members',
99
+ 'value' => Array(user_ids).map { |user_id| { 'value' => user_id } }
100
+ }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ require_relative 'groups/group'
@@ -0,0 +1,11 @@
1
+ module Ecoportal
2
+ module API
3
+ class Scim
4
+ class Schemas
5
+ class Schema
6
+ # Pending: model over the /Schemas resource (scaffold stub).
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module Ecoportal
2
+ module API
3
+ class Scim
4
+ class Schemas
5
+ include Ecoportal::API::Common::Concerns::EndPoint
6
+ include Enumerable
7
+
8
+ class_resolver :user_class, 'Ecoportal::API::Scim::Schemas::Schema'
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ require_relative 'schemas/schema'
@@ -0,0 +1,9 @@
1
+ module Ecoportal
2
+ module API
3
+ class Scim
4
+ class ServiceProviderConfig
5
+ include Ecoportal::API::Common::Concerns::EndPoint
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,119 @@
1
+ module Ecoportal
2
+ module API
3
+ class Scim
4
+ class Users
5
+ # SCIM User resource, per RFC 7643 section 4.1.
6
+ # @note the `Users` endpoint keeps returning the plain parsed Hash bodies of
7
+ # the api (its public interface is unchanged): this class is an optional,
8
+ # read-only wrapper over one of those Hashes, offering snake_case readers
9
+ # for the attributes this api actually returns.
10
+ # @example
11
+ # user = Ecoportal::API::Scim::Users::User.new(scim.users.get(id))
12
+ # user.user_name # => 'alice.example@example.com'
13
+ class User
14
+ # SCIM core schema URI for `User`, per RFC 7643 section 4.1.
15
+ SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User'.freeze
16
+
17
+ # @return [Hash] the raw SCIM resource Hash this object wraps.
18
+ attr_reader :doc
19
+
20
+ # @param doc [Hash] a SCIM `User` resource, as returned by `Users#get`
21
+ # or by one entry of `Users#list`'s `Resources`.
22
+ def initialize(doc = {})
23
+ @doc = doc || {}
24
+ end
25
+
26
+ # @return [String, nil] the SCIM `id` (the api-side identifier).
27
+ def id
28
+ doc['id']
29
+ end
30
+
31
+ # @return [String, nil] the SCIM `userName`.
32
+ def user_name
33
+ doc['userName']
34
+ end
35
+
36
+ # @return [Hash] the SCIM `name` complex attribute (`givenName`, `familyName`).
37
+ def name
38
+ doc['name'] || {}
39
+ end
40
+
41
+ # @return [String, nil] the SCIM `name.givenName`.
42
+ def given_name
43
+ name['givenName']
44
+ end
45
+
46
+ # @return [String, nil] the SCIM `name.familyName`.
47
+ def family_name
48
+ name['familyName']
49
+ end
50
+
51
+ # @note only `true` when the api reports the boolean `true`: a missing
52
+ # `active` attribute is **not** treated as active.
53
+ # @return [Boolean] the SCIM `active` flag.
54
+ def active?
55
+ doc['active'] == true
56
+ end
57
+
58
+ # @return [String, nil] the SCIM `externalId`.
59
+ def external_id
60
+ doc['externalId']
61
+ end
62
+
63
+ # @return [Array<Hash>] the SCIM `emails` multi-valued attribute
64
+ # (each entry a `{value, primary, ...}` Hash). Empty when absent.
65
+ def emails
66
+ doc['emails'] || []
67
+ end
68
+
69
+ # @return [Array<Hash>] the SCIM `phoneNumbers` multi-valued attribute
70
+ # (each entry a `{value, primary, ...}` Hash). Empty when absent.
71
+ def phone_numbers
72
+ doc['phoneNumbers'] || []
73
+ end
74
+
75
+ # SCIM enterprise-user extension schema URI, per RFC 7643 section 4.3.
76
+ # ecoPortal namespaces `manager` under this URI (see `#manager`).
77
+ ENTERPRISE_SCHEMA = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'.freeze
78
+
79
+ # @return [Hash] the SCIM enterprise-user extension namespace Hash,
80
+ # keyed by `ENTERPRISE_SCHEMA` at the top level of the resource.
81
+ # Empty when the api did not include the extension.
82
+ def enterprise_extension
83
+ doc[ENTERPRISE_SCHEMA] || {}
84
+ end
85
+
86
+ # @note ecoPortal's `manager` complex type carries only `value` -- no
87
+ # `$ref` or `displayName` (unlike the generic SCIM enterprise-user
88
+ # schema).
89
+ # @return [Hash] the SCIM `manager` complex attribute, namespaced
90
+ # under `ENTERPRISE_SCHEMA`. Empty when the api did not include a
91
+ # supervisor (ecoPortal omits the attribute entirely rather than
92
+ # emitting a null/empty one).
93
+ def manager
94
+ enterprise_extension['manager'] || {}
95
+ end
96
+
97
+ # @note this is the supervisor's ecoPortal-internal id, not their
98
+ # `externalId` -- the two are accepted interchangeably on writes
99
+ # but only the internal id is ever returned on reads.
100
+ # @return [String, nil] the SCIM `manager.value`.
101
+ def manager_id
102
+ manager['value']
103
+ end
104
+
105
+ # @param key [String] a raw SCIM attribute name (i.e. `'userName'`).
106
+ # @return [Object, nil] the raw value, for attributes without a reader.
107
+ def [](key)
108
+ doc[key]
109
+ end
110
+
111
+ # @return [Hash] the raw SCIM resource Hash.
112
+ def to_h
113
+ doc
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,59 @@
1
+ module Ecoportal
2
+ module API
3
+ class Scim
4
+ # @attr_reader client [Common::Client] a `Common::Client` object that
5
+ # holds the configuration of the api connection.
6
+ class Users
7
+ include Ecoportal::API::Common::Concerns::EndPoint
8
+ include Enumerable
9
+
10
+ class_resolver :user_class, 'Ecoportal::API::Scim::Users::User'
11
+
12
+ END_POINT = '/Users'.freeze
13
+
14
+ # Lists the users of the organization.
15
+ # @note per RFC 7644 section 3.4.2, the response is a SCIM `ListResponse`
16
+ # (`totalResults`, `startIndex`, `itemsPerPage`, `Resources`).
17
+ # @param filter [String] a SCIM filter expression.
18
+ # @param start_index [Integer] the 1-based index of the first result (SCIM `startIndex`).
19
+ # @param count [Integer] the maximum number of results (SCIM `count`).
20
+ # @return [Hash] the SCIM `ListResponse`.
21
+ def list(filter: nil, start_index: nil, count: nil)
22
+ params = {}
23
+ params[:filter] = filter if filter
24
+ params[:startIndex] = start_index if start_index
25
+ params[:count] = count if count
26
+
27
+ response = client.get(END_POINT, params: params)
28
+ body = response.body
29
+ return body if response.success?
30
+
31
+ raise "Could not list users - Error #{response.status}: #{body}"
32
+ end
33
+
34
+ # Iterates all the users returned by `list`.
35
+ # @param filter [String] a SCIM filter expression.
36
+ # @yield [user] does something with the user resource.
37
+ # @yieldparam user [Hash]
38
+ def each(filter: nil, &block)
39
+ return to_enum(:each, filter: filter) unless block_given?
40
+
41
+ list(filter: filter).fetch('Resources', []).each(&block)
42
+ end
43
+
44
+ # Gets a user via api.
45
+ # @param id [String] the `id` of the target user.
46
+ # @return [Hash] the user resource.
47
+ def get(id)
48
+ response = client.get("#{END_POINT}/#{CGI.escape(id)}")
49
+ body = response.body
50
+ return body if response.success?
51
+
52
+ raise "Could not get user #{id} - Error #{response.status}: #{body}"
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ require_relative 'users/user'
@@ -0,0 +1,80 @@
1
+ module Ecoportal
2
+ module API
3
+ # @attr_reader client [Common::Client] a `Common::Client` object that holds the configuration of the api connection.
4
+ # @attr_reader logger [Logger] the logger.
5
+ class Scim
6
+ unless Ecoportal::API.const_defined?(:SCIM_VERSION)
7
+ msg = "Correct way to require this gem is `require 'ecoportal/scim`"
8
+ raise LoadError, msg
9
+ end
10
+
11
+ extend Ecoportal::API::Common::BaseClass
12
+ include Ecoportal::API::Common::Logging
13
+
14
+ VERSION = 'v2'.freeze
15
+
16
+ class_resolver :users_class, 'Ecoportal::API::Scim::Users'
17
+ class_resolver :groups_class, 'Ecoportal::API::Scim::Groups'
18
+ class_resolver :schemas_class, 'Ecoportal::API::Scim::Schemas'
19
+ class_resolver :service_provider_config_class, 'Ecoportal::API::Scim::ServiceProviderConfig'
20
+
21
+ # Creates an `V2` object to scope version specific api requests.
22
+ # @note
23
+ # - You should use either `api_key` or `user_key` and `org_key`
24
+ # - The const `VERSION` determineds the api version that client will query against.
25
+ # - This means that each subclass of `V2` should define their own `VERSION` constant.
26
+ # @param api_key [String] the key version to stablish the api connection.
27
+ # @param user_key [String] the user key used for the api connection (requires `org_key`).
28
+ # @param org_key [String] the org key used for the api connection (requires `user_key`).
29
+ # @param host [String] api server domain.
30
+ # @param logger [Logger] an object with `Logger` interface to generate logs.
31
+ def initialize(
32
+ scim_id,
33
+ scim_key,
34
+ host: 'live.ecoportal.com',
35
+ logger: default_logger
36
+ )
37
+ @logger = logger
38
+ @client = Common::Scim::Client.new(
39
+ api_key: scim_key,
40
+ scim_id: scim_id,
41
+ host: host,
42
+ version: self.class::VERSION,
43
+ logger: @logger
44
+ )
45
+ end
46
+
47
+ # @return [Common::Scim::Client] the low-level SCIM http client.
48
+ attr_reader :client
49
+
50
+ # Obtain specific object for User resource.
51
+ # @return [User] an instance object ready to make User Scim interactions.
52
+ def users
53
+ users_class.new(client)
54
+ end
55
+
56
+ # Obtain specific object for Group resource.
57
+ # @return [Groups] an instance object ready to make Group Scim interactions.
58
+ def groups
59
+ groups_class.new(client)
60
+ end
61
+
62
+ # Obtain specific object for Schemas resource.
63
+ # @return [Schemas] an instance object ready to make Schemas Scim interactions.
64
+ def schemas
65
+ schemas_class.new(client)
66
+ end
67
+
68
+ # Obtain specific object for Schemas resource.
69
+ # @return [ServiceProviderConfig] an instance object ready to make
70
+ # ServiceProviderConfig Scim interactions.
71
+ def service_provider_config
72
+ service_provider_config_class.new(client)
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ require_relative 'scim/schemas'
79
+ require_relative 'scim/users'
80
+ require_relative 'scim/groups'
@@ -0,0 +1,5 @@
1
+ module Ecoportal
2
+ module API
3
+ SCIM_VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ require 'ecoportal/api'
2
+ require_relative 'api/scim_version'
3
+
4
+ module Ecoportal
5
+ module API
6
+ end
7
+ end
8
+
9
+ require_relative 'api/common.scim'
10
+ require_relative 'api/scim'
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ecoportal-scim
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Oscar Segura
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 13.0.3
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '14'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 13.0.3
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '14'
32
+ - !ruby/object:Gem::Dependency
33
+ name: redcarpet
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 3.6.0
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '4'
42
+ type: :development
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 3.6.0
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '4'
52
+ - !ruby/object:Gem::Dependency
53
+ name: rspec
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.12.0
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.12.0
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '4'
72
+ - !ruby/object:Gem::Dependency
73
+ name: rubocop
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - "~>"
77
+ - !ruby/object:Gem::Version
78
+ version: '1'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '1'
86
+ - !ruby/object:Gem::Dependency
87
+ name: rubocop-rake
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: webmock
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - "~>"
105
+ - !ruby/object:Gem::Version
106
+ version: '3'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '3'
114
+ - !ruby/object:Gem::Dependency
115
+ name: yard
116
+ requirement: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: '0.9'
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - "~>"
126
+ - !ruby/object:Gem::Version
127
+ version: '0.9'
128
+ - !ruby/object:Gem::Dependency
129
+ name: ecoportal-api
130
+ requirement: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - "~>"
133
+ - !ruby/object:Gem::Version
134
+ version: '0.10'
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: 0.10.12
138
+ type: :runtime
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '0.10'
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: 0.10.12
148
+ email:
149
+ - oscar@ecoportal.co.nz
150
+ executables: []
151
+ extensions: []
152
+ extra_rdoc_files: []
153
+ files:
154
+ - CHANGELOG.md
155
+ - LICENSE
156
+ - README.md
157
+ - lib/ecoportal/api/common.scim.rb
158
+ - lib/ecoportal/api/common/concerns.rb
159
+ - lib/ecoportal/api/common/concerns/end_point.rb
160
+ - lib/ecoportal/api/common/scim.rb
161
+ - lib/ecoportal/api/common/scim/client.rb
162
+ - lib/ecoportal/api/common/scim/includer.rb
163
+ - lib/ecoportal/api/scim.rb
164
+ - lib/ecoportal/api/scim/groups.rb
165
+ - lib/ecoportal/api/scim/groups/group.rb
166
+ - lib/ecoportal/api/scim/schemas.rb
167
+ - lib/ecoportal/api/scim/schemas/schema.rb
168
+ - lib/ecoportal/api/scim/service_provider_config.rb
169
+ - lib/ecoportal/api/scim/users.rb
170
+ - lib/ecoportal/api/scim/users/user.rb
171
+ - lib/ecoportal/api/scim_version.rb
172
+ - lib/ecoportal/scim.rb
173
+ homepage: https://www.ecoportal.com
174
+ licenses:
175
+ - MIT
176
+ metadata:
177
+ rubygems_mfa_required: 'true'
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: 3.2.2
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ requirements: []
192
+ rubygems_version: 4.0.8
193
+ specification_version: 4
194
+ summary: Ruby client implementation for ecoPortal SCIM
195
+ test_files: []