idnio 2.3.2b
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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/lib/idnio.rb +295 -0
- data/lib/idnio/crypto.rb +34 -0
- data/lib/idnio/idnapi.rb +158 -0
- data/lib/idnio/markdown.rb +345 -0
- data/lib/idnio/program.rb +153 -0
- data/lib/idnio/timer.rb +57 -0
- data/lib/idnio/version.rb +4 -0
- data/lib/objects/access-profiles.rb +107 -0
- data/lib/objects/access-request-config.rb +90 -0
- data/lib/objects/account-profiles.rb +167 -0
- data/lib/objects/account-schemas.rb +341 -0
- data/lib/objects/applications.rb +145 -0
- data/lib/objects/attribute-sync-config.rb +122 -0
- data/lib/objects/branding.rb +49 -0
- data/lib/objects/campaign-filters.rb +61 -0
- data/lib/objects/connectors.rb +291 -0
- data/lib/objects/email-templates.rb +226 -0
- data/lib/objects/identity-attributes.rb +136 -0
- data/lib/objects/identity-profiles.rb +206 -0
- data/lib/objects/integrations.rb +149 -0
- data/lib/objects/lifecycle-states.rb +86 -0
- data/lib/objects/password-policies.rb +107 -0
- data/lib/objects/password-sync-groups.rb +100 -0
- data/lib/objects/public-identities-config.rb +78 -0
- data/lib/objects/reference-resolver.rb +137 -0
- data/lib/objects/roles.rb +117 -0
- data/lib/objects/rules.rb +198 -0
- data/lib/objects/sources.rb +217 -0
- data/lib/objects/system-settings.rb +185 -0
- data/lib/objects/transforms.rb +157 -0
- metadata +124 -0
- metadata.gz.sig +0 -0
    
        data/lib/idnio/timer.rb
    ADDED
    
    | @@ -0,0 +1,57 @@ | |
| 1 | 
            +
            #
         | 
| 2 | 
            +
            # Time Utility
         | 
| 3 | 
            +
            #
         | 
| 4 | 
            +
            module Timer
         | 
| 5 | 
            +
              @@timeStart = nil
         | 
| 6 | 
            +
              @@timeEnd = nil
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              #
         | 
| 9 | 
            +
              # Starts the timer
         | 
| 10 | 
            +
              #
         | 
| 11 | 
            +
              def self.start
         | 
| 12 | 
            +
                @@timeStart = Process.clock_gettime( Process::CLOCK_MONOTONIC )
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              #
         | 
| 16 | 
            +
              # Stops the timer
         | 
| 17 | 
            +
              #
         | 
| 18 | 
            +
              def self.stop
         | 
| 19 | 
            +
                @@timeEnd = Process.clock_gettime( Process::CLOCK_MONOTONIC )
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              #
         | 
| 23 | 
            +
              # Gets elapsed time
         | 
| 24 | 
            +
              #
         | 
| 25 | 
            +
              def self.elapsed
         | 
| 26 | 
            +
                unless @@timeEnd.nil? || @@timeStart.nil?
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  elapsed = @@timeEnd.to_i - @@timeStart.to_i # distance between t1 and t2 in seconds
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  resolution = if elapsed > 29030400 # seconds in a year
         | 
| 31 | 
            +
                    [(elapsed/29030400), 'years']
         | 
| 32 | 
            +
                  elsif elapsed > 2419200
         | 
| 33 | 
            +
                    [(elapsed/2419200), 'months']
         | 
| 34 | 
            +
                  elsif elapsed > 604800
         | 
| 35 | 
            +
                    [(elapsed/604800), 'weeks']
         | 
| 36 | 
            +
                  elsif elapsed > 86400
         | 
| 37 | 
            +
                    [(elapsed/86400), 'days']
         | 
| 38 | 
            +
                  elsif elapsed > 3600 # seconds in an hour
         | 
| 39 | 
            +
                    [(elapsed/3600), 'hours']
         | 
| 40 | 
            +
                  elsif elapsed > 60
         | 
| 41 | 
            +
                    [(elapsed/60), 'minutes']
         | 
| 42 | 
            +
                  else
         | 
| 43 | 
            +
                    [elapsed, 'seconds']
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  if resolution[0] == 1
         | 
| 47 | 
            +
                    return resolution.join(' ')[0...-1]
         | 
| 48 | 
            +
                  else
         | 
| 49 | 
            +
                    return resolution.join(' ')
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                else
         | 
| 53 | 
            +
                  return nil
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
              end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            end
         | 
| @@ -0,0 +1,107 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            require "json"
         | 
| 3 | 
            +
            require "idnio/idnapi"
         | 
| 4 | 
            +
            require "idnio/program"
         | 
| 5 | 
            +
            require "idnio/markdown"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module AccessProfiles
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              #
         | 
| 10 | 
            +
              # Converts Entitlement IDs to Entitlement Names
         | 
| 11 | 
            +
              #
         | 
| 12 | 
            +
              def self.getEntitlements( entitlement_ids )
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                entitlementNames = ""
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                entitlement_ids.each do |entitlement_id|
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  response = IDNAPI.get( "#{$url}/v2/search/entitlements?query='id=#{entitlement_id}''", $token )
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  case response
         | 
| 21 | 
            +
                  when Net::HTTPSuccess
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    entitlements = JSON.parse( response.body )
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    entitlements.each do |entitlement|
         | 
| 26 | 
            +
                      if (entitlementNames != "")
         | 
| 27 | 
            +
                        entitlementNames << ";"
         | 
| 28 | 
            +
                      end
         | 
| 29 | 
            +
                      entitlementNames << entitlement['name']
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                return entitlementNames
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
              #
         | 
| 39 | 
            +
              # Exports Access Profile configurations.
         | 
| 40 | 
            +
              #
         | 
| 41 | 
            +
              def self.export( directory )
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                response = IDNAPI.get( "#{$url}/cc/api/accessProfile/list", $token )
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                case response
         | 
| 46 | 
            +
                when Net::HTTPSuccess
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  accessProfiles = JSON.parse( response.body )
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  $log.info "\tDetected #{accessProfiles['count']} access profiles."
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  accessProfiles['items'].each do |accessProfile|
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    $log.info "\tAccess Profile: #{accessProfile["name"]}"
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    accessProfile['entitlementNames'] = AccessProfiles.getEntitlements( accessProfile['entitlements'] )
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    Program.write_file( "#{directory}/access-profiles/", "Access Profile - #{accessProfile["name"]}.json", JSON.pretty_generate( accessProfile ) )
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  end # transforms["items"].each do |transform|
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                else
         | 
| 63 | 
            +
                  $log.error "\tError: Unable to fetch access profiles."
         | 
| 64 | 
            +
                end # case response
         | 
| 65 | 
            +
             | 
| 66 | 
            +
              end # def self.export( directory )
         | 
| 67 | 
            +
             | 
| 68 | 
            +
              #
         | 
| 69 | 
            +
              # Imports Access Profile configurations.
         | 
| 70 | 
            +
              #
         | 
| 71 | 
            +
              def self.import( directory )
         | 
| 72 | 
            +
                $log.warn "\tImport for object type access-profiles is not supported at this time."
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
              #
         | 
| 76 | 
            +
              # Documents Access Profile configurations.
         | 
| 77 | 
            +
              #
         | 
| 78 | 
            +
              def self.doc
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                response = IDNAPI.get( "#{$url}/cc/api/accessProfile/list", $token )
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                case response
         | 
| 83 | 
            +
                when Net::HTTPSuccess
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  accessProfiles = JSON.parse( response.body )
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  $log.info "\tDetected #{accessProfiles['count']} access profiles."
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  Markdown.h2( "Access Profiles" )
         | 
| 90 | 
            +
                  Markdown.text( "| Name | Description | Entitlements | Requestable |\n")
         | 
| 91 | 
            +
                  Markdown.text( "|------|-------------|--------------|-------------|\n")
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  accessProfiles['items'].each do |accessProfile|
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                    $log.info "\tAccess Profile: #{accessProfile["name"]}"
         | 
| 96 | 
            +
                    Markdown.text( "|#{accessProfile["name"]}|#{accessProfile["description"]}|#{accessProfile["sourceName"]}: #{AccessProfiles.getEntitlements(accessProfile["entitlements"])}|#{accessProfile["requestable"]}|\n")
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  end # transforms["items"].each do |transform|
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                else
         | 
| 101 | 
            +
                  $log.error "\tError: Unable to fetch access profiles."
         | 
| 102 | 
            +
                end # case response
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                Markdown.write
         | 
| 105 | 
            +
              end # def self.doc
         | 
| 106 | 
            +
             | 
| 107 | 
            +
            end
         | 
| @@ -0,0 +1,90 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            require "json"
         | 
| 3 | 
            +
            require "uri"
         | 
| 4 | 
            +
            require "idnio/idnapi"
         | 
| 5 | 
            +
            require "idnio/program"
         | 
| 6 | 
            +
            require "idnio/markdown"
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module AccessRequestConfig
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              #
         | 
| 11 | 
            +
              # Exports Access Request Config
         | 
| 12 | 
            +
              #
         | 
| 13 | 
            +
              def self.export( directory )
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                response = IDNAPI.get( "#{$url}/beta/access-request-config/", $token )
         | 
| 16 | 
            +
                case response
         | 
| 17 | 
            +
                when Net::HTTPSuccess
         | 
| 18 | 
            +
                  $log.info "\tRetreived configuration."
         | 
| 19 | 
            +
                  config = JSON.parse( response.body )
         | 
| 20 | 
            +
                  Program.write_file( "#{directory}/access-request-config/", "access-request-config.json", JSON.pretty_generate( config ) )
         | 
| 21 | 
            +
                else
         | 
| 22 | 
            +
                  $log.error "\tUnable to retreive configuration."
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              #
         | 
| 28 | 
            +
              # Imports Access Request Config
         | 
| 29 | 
            +
              #
         | 
| 30 | 
            +
              def self.import( directory )
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                # Read from the file system to determine how many configs we have.  We should really only have one.
         | 
| 33 | 
            +
                configs = Program.read_directory( "#{directory}/access-request-config/" )
         | 
| 34 | 
            +
                $log.info "\tRetreived configuration."
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                # Iterate through each transform.
         | 
| 37 | 
            +
                configs.each do |config|
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  $log.debug "\tResolving fallback approver..."
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  unless config['approvalReminderAndEscalationConfig']['fallbackApproverRef'].nil? || config['approvalReminderAndEscalationConfig']['fallbackApproverRef']['name'].nil?
         | 
| 42 | 
            +
                    config['approvalReminderAndEscalationConfig']['fallbackApproverRef'] = ReferenceResolver.get_identity_ref( config['approvalReminderAndEscalationConfig']['fallbackApproverRef']['name'] )
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  $log.debug "\tImporting access request config..."
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  response = IDNAPI.put_json( "#{$url}/beta/access-request-config/", $token, JSON.parse( config ) )
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  case response
         | 
| 50 | 
            +
                  when Net::HTTPSuccess
         | 
| 51 | 
            +
                    $log.info "\tUpdated access request config."
         | 
| 52 | 
            +
                  else
         | 
| 53 | 
            +
                    $log.error "\tUnable to import access request config."
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
              #
         | 
| 59 | 
            +
              # Documents Access Request Config
         | 
| 60 | 
            +
              #
         | 
| 61 | 
            +
              def self.doc
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                Markdown.h2( "Access Request Config" )
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                response = IDNAPI.get( "#{$url}/beta/access-request-config/", $token )
         | 
| 66 | 
            +
                case response
         | 
| 67 | 
            +
                when Net::HTTPSuccess
         | 
| 68 | 
            +
                  $log.info "\tRetreived configuration."
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  config = JSON.parse( response.body )
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  Markdown.h3( "Request Settings" )
         | 
| 73 | 
            +
                  Markdown.text "- Allow requests on behalf of anyone, by anyone: #{config['requestOnBehalfOfConfig']['allowRequestOnBehalfOfAnyoneByAnyone']}\n"
         | 
| 74 | 
            +
                  Markdown.text "- Allow requests on behalf of employees, by manager: #{config['requestOnBehalfOfConfig']['allowRequestOnBehalfOfEmployeeByManager']}\n"
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  Markdown.h3( "Approval and Reminders" )
         | 
| 77 | 
            +
                  Markdown.text "- Days until escalation: #{config['approvalReminderAndEscalationConfig']['daysUntilEscalation']}\n"
         | 
| 78 | 
            +
                  Markdown.text "- Days between reminders: #{config['approvalReminderAndEscalationConfig']['daysBetweenReminders']}\n"
         | 
| 79 | 
            +
                  Markdown.text "- Maximum reminders: #{config['approvalReminderAndEscalationConfig']['maxReminders']}\n"
         | 
| 80 | 
            +
                  unless config['approvalReminderAndEscalationConfig']['fallbackApproverRef'].nil?
         | 
| 81 | 
            +
                    Markdown.text "- Fallback approver: #{config['approvalReminderAndEscalationConfig']['fallbackApproverRef']['name']}\n"
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                else
         | 
| 85 | 
            +
                  $log.error "\tUnable to retreive configuration."
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                Markdown.write
         | 
| 89 | 
            +
              end
         | 
| 90 | 
            +
            end
         | 
| @@ -0,0 +1,167 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            require "json"
         | 
| 3 | 
            +
            require "idnio/idnapi"
         | 
| 4 | 
            +
            require "idnio/program"
         | 
| 5 | 
            +
            require "idnio/markdown"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module AccountProfiles
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              #
         | 
| 10 | 
            +
              # Exports Account Profile configurations.
         | 
| 11 | 
            +
              #
         | 
| 12 | 
            +
              def self.export( directory )
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                response = IDNAPI.get( "#{$url}/cc/api/source/list", $token )
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                case response
         | 
| 17 | 
            +
                when Net::HTTPSuccess
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  sources = JSON.parse( response.body )
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  $log.info "\tDetected #{sources.count} account profiles, across #{sources.count} sources."
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  sources.each do |source|
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    response = IDNAPI.get( "#{$url}/cc/api/accountProfile/list/#{source["id"]}", $token )
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    case response
         | 
| 28 | 
            +
                    when Net::HTTPSuccess
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                      accountProfiles = JSON.parse( response.body )
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                      accountProfiles.each do |accountProfile|
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                        $log.info "\tAccount Profile: #{source["name"]} - #{accountProfile["name"]} - #{accountProfile["usage"]}"
         | 
| 35 | 
            +
                        Program.write_file( "#{directory}/account-profiles/", "Account Profile - #{source["name"]} - #{accountProfile["name"]} - #{accountProfile["usage"]}.json", JSON.pretty_generate( accountProfile ) )
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      end # accountProfiles.each do |accountProfile|
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    else
         | 
| 40 | 
            +
                      $log.error "\tError: Unable to fetch account profile for source #{source["name"]}."
         | 
| 41 | 
            +
                    end # case response
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  end # sources.each do |source|
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                else
         | 
| 46 | 
            +
                  $log.error "\tError: Unable to fetch sources, for account profiles."
         | 
| 47 | 
            +
                end # case response
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              end # def self.export( directory )
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              #
         | 
| 52 | 
            +
              # Imports Account Profile configurations.
         | 
| 53 | 
            +
              #
         | 
| 54 | 
            +
              def self.import( directory )
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                #
         | 
| 57 | 
            +
                # Read from the file system to determine how many account profiles we have.
         | 
| 58 | 
            +
                #
         | 
| 59 | 
            +
                objects = []
         | 
| 60 | 
            +
                Dir.glob("#{directory}/account-profiles/*.json").each do |file|
         | 
| 61 | 
            +
                  object = {
         | 
| 62 | 
            +
                    "file" => file,
         | 
| 63 | 
            +
                    "source" => File.basename( file, ".json" ).split(/\W?-\W?/)[1],
         | 
| 64 | 
            +
                    "name" => File.basename( file, ".json" ).split(/\W?-\W?/)[2],
         | 
| 65 | 
            +
                    "usage" => File.basename( file, ".json" ).split(/\W?-\W?/)[3]
         | 
| 66 | 
            +
                  }
         | 
| 67 | 
            +
                  objects.push( object )
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                $log.info "\tDetected #{objects.count} account profiles."
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                #
         | 
| 73 | 
            +
                # Iterate through each account profile.
         | 
| 74 | 
            +
                #
         | 
| 75 | 
            +
                objects.each do |object|
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  $log.info "\tAccount Profile: #{object['source']} - #{object['name']} - #{object['usage']}"
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  #
         | 
| 80 | 
            +
                  # Get the account profile JSON.
         | 
| 81 | 
            +
                  #
         | 
| 82 | 
            +
                  template_account_profile = JSON.parse( File.read( object['file'] ) )
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  #
         | 
| 85 | 
            +
                  # Lookup the existing source.
         | 
| 86 | 
            +
                  #
         | 
| 87 | 
            +
                  existing_source_id = Sources.get_cc_id( object['source'] )
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  unless existing_source_id.nil? || template_account_profile.nil?
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    $log.debug "\t\tUpdating account profile..."
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                    IDNAPI.post_json( "#{$url}/cc/api/accountProfile/update/#{existing_source_id}?usage=#{object['usage']}", $token, template_account_profile )
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                    $log.info "\t\tUpdate complete."
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  else
         | 
| 98 | 
            +
                    $log.warn "\t\tSkipping account profile creation. Source [#{object['source']}] does not exist.\n"
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
              end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
              #
         | 
| 106 | 
            +
              # Documents Account Profile configurations.
         | 
| 107 | 
            +
              #
         | 
| 108 | 
            +
              def self.doc
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                response = IDNAPI.get( "#{$url}/cc/api/source/list", $token )
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                case response
         | 
| 113 | 
            +
                when Net::HTTPSuccess
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  sources = JSON.parse( response.body )
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                  $log.info "\tDetected #{sources.count} account profiles, across #{sources.count} sources."
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  Markdown.h2( "Account Profiles" )
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  sources.each do |source|
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    response = IDNAPI.get( "#{$url}/cc/api/accountProfile/list/#{source["id"]}", $token )
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                    case response
         | 
| 126 | 
            +
                    when Net::HTTPSuccess
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                      accountProfiles = JSON.parse( response.body )
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                      accountProfiles.each do |accountProfile|
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                        $log.info "\tAccount Profile: #{source["name"]} - #{accountProfile["name"]} - #{accountProfile["usage"]}"
         | 
| 133 | 
            +
                        Markdown.h3 "#{source["name"]} #{accountProfile["usage"]} #{accountProfile["name"]}"
         | 
| 134 | 
            +
                        Markdown.text " - Name: #{accountProfile["name"]}\n"
         | 
| 135 | 
            +
                        Markdown.text " - Description: #{accountProfile["description"]}\n"
         | 
| 136 | 
            +
                        Markdown.text " - Usage: #{accountProfile["usage"]}\n"
         | 
| 137 | 
            +
                        Markdown.text " - Enabled: #{accountProfile["validPolicy"]}\n"
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                        unless accountProfile["usage"].nil? || accountProfile["usage"].empty?
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                          Markdown.text "| Name | Type | Required | Multi-valued | Transform |\n"
         | 
| 142 | 
            +
                          Markdown.text "|------|------|----------|--------------|-----------|\n"
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                          accountProfile["fields"].each do |field|
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                            Markdown.text "|#{field['name']}|#{field['type']}|#{field['isRequired']}|#{field['multi']}|#{field['transform']}|\n"
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                          end # accountProfile["fields"].each do |field|
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                        end # unless accountProfile["usage"].nil? ...
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                      end # accountProfiles.each do |accountProfile|
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                    else
         | 
| 155 | 
            +
                      $log.error "\tError: Unable to fetch account profile for source #{source["name"]}."
         | 
| 156 | 
            +
                    end # case response
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                  end # sources.each do |source|
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                else
         | 
| 161 | 
            +
                  $log.error "\tError: Unable to fetch sources, for account profiles."
         | 
| 162 | 
            +
                end # case response
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                Markdown.write
         | 
| 165 | 
            +
              end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
            end
         | 
| @@ -0,0 +1,341 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            require 'json'
         | 
| 3 | 
            +
            require "idnio/idnapi"
         | 
| 4 | 
            +
            require "idnio/program"
         | 
| 5 | 
            +
            require "idnio/markdown"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module AccountSchemas
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              @@account_schema_cache = {}
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              def self.get_existing_schema_attribute( id, attribute_name )
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                unless id.nil? || attribute_name.nil?
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  existing_account_schema = AccountSchemas.get_by_id( id )
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  unless existing_account_schema.nil?
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    existing_account_schema['attributes'].each do |existing_attribute|
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                      if existing_attribute['name'] == attribute_name
         | 
| 22 | 
            +
                        return existing_attribute
         | 
| 23 | 
            +
                      end # existing_attribute['name'] == attribute_name
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    end # existing_account_schema['attributes'].each do |existing_attribute|
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  end # unless existing_account_schema.nil?
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                end # unless id.nil? || attribute_name.nil?
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                return nil
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              def self.get_by_id( id )
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                unless @@account_schema_cache.key?( id )
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  response = IDNAPI.get( "#{$url}/cc/api/source/getAccountSchema/#{id}", $token )
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  case response
         | 
| 41 | 
            +
                  when Net::HTTPSuccess
         | 
| 42 | 
            +
                    @@account_schema_cache[id] = JSON.parse( response.body )
         | 
| 43 | 
            +
                  else
         | 
| 44 | 
            +
                    @@account_schema_cache[id] = nil
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                return @@account_schema_cache[id]
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              #
         | 
| 53 | 
            +
              # Creates a Schema Attribute
         | 
| 54 | 
            +
              #
         | 
| 55 | 
            +
              def self.create( id, attribute )
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                $log.debug "\t\t\tCreating attribute."
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                form_data = {
         | 
| 60 | 
            +
                  'name' => attribute['name'],
         | 
| 61 | 
            +
                  'description' => attribute['description'],
         | 
| 62 | 
            +
                  'type' => attribute['type'],
         | 
| 63 | 
            +
                  'objectType' => 'account',
         | 
| 64 | 
            +
                  'entitlement' => attribute['entitlement'],
         | 
| 65 | 
            +
                  'identityAttribute' => attribute['identityAttribute'],
         | 
| 66 | 
            +
                  'managed' => attribute['managed'],
         | 
| 67 | 
            +
                  'multi' => attribute['multi'],
         | 
| 68 | 
            +
                  'displayAttribute' => attribute['displayAttribute']
         | 
| 69 | 
            +
                }
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                response = IDNAPI.post_form( "#{$url}/cc/api/source/createSchemaAttribute/#{id}", $token, form_data )
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                case response
         | 
| 74 | 
            +
                when Net::HTTPSuccess
         | 
| 75 | 
            +
                  $log.debug "\t\tAttribute [#{attribute['name']}] successfully created."
         | 
| 76 | 
            +
                else
         | 
| 77 | 
            +
                  $log.debug "\t\tError: Attribute [#{attribute['name']}] could not be successfully created."
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
              end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
              #
         | 
| 82 | 
            +
              # Updates a Schema Attribute
         | 
| 83 | 
            +
              #
         | 
| 84 | 
            +
              def self.update( id, attribute )
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                $log.debug "\t\t\tUpdating attribute."
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                existing_attribute = AccountSchemas.get_existing_schema_attribute( id, attribute['name'] )
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                #
         | 
| 91 | 
            +
                # The API only supports updating certain properties of an attribute.  The descriptions, name, type cannot be updated.
         | 
| 92 | 
            +
                #
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                # Check 'entitlement's
         | 
| 95 | 
            +
                unless ( attribute['entitlement'] == existing_attribute['entitlement'] )
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  form_data = {
         | 
| 98 | 
            +
                    'objectType': 'account',
         | 
| 99 | 
            +
                    'names': existing_attribute['name'],
         | 
| 100 | 
            +
                    'fieldName': 'entitlement',
         | 
| 101 | 
            +
                    'fieldValue': attribute['entitlement']
         | 
| 102 | 
            +
                  }
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  response = IDNAPI.post_form( "#{$url}/cc/api/source/updateSchemaAttributes/#{id}", $token, form_data )
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  case response
         | 
| 107 | 
            +
                  when Net::HTTPSuccess
         | 
| 108 | 
            +
                    $log.debug "\t\t\tAttribute [#{attribute['name']}] updated 'entitlement' property."
         | 
| 109 | 
            +
                  else
         | 
| 110 | 
            +
                    $log.debug "\t\tError: Attribute [#{attribute['name']}] could not update 'entitlement' property."
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                end # unless ( attribute['entitlement'] == existing_attribute['entitlement'] )
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                # Check 'managed'
         | 
| 116 | 
            +
                unless ( attribute['managed'] == existing_attribute['managed'] )
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                  form_data = {
         | 
| 119 | 
            +
                    'objectType': 'account',
         | 
| 120 | 
            +
                    'names': existing_attribute['name'],
         | 
| 121 | 
            +
                    'fieldName': 'managed',
         | 
| 122 | 
            +
                    'fieldValue': attribute['managed']
         | 
| 123 | 
            +
                  }
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                  response = IDNAPI.post_form( "#{$url}/cc/api/source/updateSchemaAttributes/#{id}", $token, form_data )
         | 
| 126 | 
            +
            s
         | 
| 127 | 
            +
                  case response
         | 
| 128 | 
            +
                  when Net::HTTPSuccess
         | 
| 129 | 
            +
                    $log.debug "\t\t\tAttribute [#{attribute['name']}] updated 'managed' property."
         | 
| 130 | 
            +
                  else
         | 
| 131 | 
            +
                    $log.debug "\t\tError: Attribute [#{attribute['name']}] could not update 'managed' property."
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                end # unless ( attribute['managed'] == existing_attribute['managed'] )
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                # Check 'multi'
         | 
| 137 | 
            +
                unless ( attribute['multi'] == existing_attribute['multi'] )
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                  form_data = {
         | 
| 140 | 
            +
                    'objectType': 'account',
         | 
| 141 | 
            +
                    'names': existing_attribute['name'],
         | 
| 142 | 
            +
                    'fieldName': 'multi',
         | 
| 143 | 
            +
                    'fieldValue': attribute['multi']
         | 
| 144 | 
            +
                  }
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                  response = IDNAPI.post_form( "#{$url}/cc/api/source/updateSchemaAttributes/#{id}", $token, form_data )
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                  case response
         | 
| 149 | 
            +
                  when Net::HTTPSuccess
         | 
| 150 | 
            +
                    $log.debug "\t\t\tAttribute [#{attribute['name']}] updated 'multi' property."
         | 
| 151 | 
            +
                  else
         | 
| 152 | 
            +
                    $log.debug "\t\tError: Attribute [#{attribute['name']}] could not update 'multi' property."
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                end # unless ( attribute['multi'] == existing_attribute['multi'] )
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                # Check 'displayAttribute'
         | 
| 158 | 
            +
                unless ( attribute['displayAttribute'] == existing_attribute['displayAttribute'] )
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                  form_data = {
         | 
| 161 | 
            +
                    'objectType': 'account',
         | 
| 162 | 
            +
                    'names': existing_attribute['name'],
         | 
| 163 | 
            +
                    'fieldName': 'displayAttribute',
         | 
| 164 | 
            +
                    'fieldValue': attribute['displayAttribute']
         | 
| 165 | 
            +
                  }
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  response = IDNAPI.post_form( "#{$url}/cc/api/source/updateSchemaAttributes/#{id}", $token, form_data )
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                  case response
         | 
| 170 | 
            +
                  when Net::HTTPSuccess
         | 
| 171 | 
            +
                    $log.debug "\t\t\tAttribute [#{attribute['name']}] updated 'displayAttribute' property."
         | 
| 172 | 
            +
                  else
         | 
| 173 | 
            +
                    $log.debug "\t\tError: Attribute [#{attribute['name']}] could not update 'displayAttribute' property."
         | 
| 174 | 
            +
                  end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                end # unless ( attribute['displayAttribute'] == existing_attribute['displayAttribute'] )
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                # Check 'identityAttribute'
         | 
| 179 | 
            +
                unless ( attribute['identityAttribute'] == existing_attribute['identityAttribute'] )
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                  form_data = {
         | 
| 182 | 
            +
                    'objectType': 'account',
         | 
| 183 | 
            +
                    'names': existing_attribute['name'],
         | 
| 184 | 
            +
                    'fieldName': 'identityAttribute',
         | 
| 185 | 
            +
                    'fieldValue': attribute['identityAttribute']
         | 
| 186 | 
            +
                  }
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                  response = IDNAPI.post_form( "#{$url}/cc/api/source/updateSchemaAttributes/#{id}", $token, form_data )
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                  case response
         | 
| 191 | 
            +
                  when Net::HTTPSuccess
         | 
| 192 | 
            +
                    $log.debug "\t\t\tAttribute [#{attribute['name']}] updated 'identityAttribute' property."
         | 
| 193 | 
            +
                  else
         | 
| 194 | 
            +
                    $log.debug "\t\tError: Attribute [#{attribute['name']}] could not update 'identityAttribute' property."
         | 
| 195 | 
            +
                  end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                end # unless ( attribute['identityAttribute'] == existing_attribute['identityAttribute'] )
         | 
| 198 | 
            +
             | 
| 199 | 
            +
              end
         | 
| 200 | 
            +
             | 
| 201 | 
            +
              #
         | 
| 202 | 
            +
              # Exports Account Schema configurations.
         | 
| 203 | 
            +
              #
         | 
| 204 | 
            +
              def self.export( directory )
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                response = IDNAPI.get("#{$url}/cc/api/source/list", $token)
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                case response
         | 
| 209 | 
            +
                when Net::HTTPSuccess
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                  sources = JSON.parse( response.body )
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                  unless sources.nil?
         | 
| 214 | 
            +
             | 
| 215 | 
            +
                    $log.info "\tDetected #{sources.count} account schemas, across #{sources.count} sources."
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                    sources.each do |source|
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                      $log.info "\tAccount Schema: #{source['name']}"
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                      response = IDNAPI.get( "#{$url}/cc/api/source/getAccountSchema/#{source['id']}", $token )
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                      case response
         | 
| 224 | 
            +
                      when Net::HTTPSuccess
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                        account_schema = JSON.parse( response.body )
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                        Program.write_file("#{directory}/account-schemas/", "Account Schema - #{source['name']} - #{account_schema['objectType']}.json", JSON.pretty_generate( account_schema ) )
         | 
| 229 | 
            +
             | 
| 230 | 
            +
                      else
         | 
| 231 | 
            +
                        $log.error "\tError: Unable to fetch account schema for source '#{source['name']}'."
         | 
| 232 | 
            +
                      end # case response
         | 
| 233 | 
            +
             | 
| 234 | 
            +
                    end # sources.each do |source|
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                  end # unless sources.nil?
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                else
         | 
| 239 | 
            +
                  $log.error "\tError: Unable to fetch sources."
         | 
| 240 | 
            +
                end # case response
         | 
| 241 | 
            +
             | 
| 242 | 
            +
              end
         | 
| 243 | 
            +
             | 
| 244 | 
            +
              #
         | 
| 245 | 
            +
              # Imports Account Schema configurations.
         | 
| 246 | 
            +
              #
         | 
| 247 | 
            +
              def self.import( directory )
         | 
| 248 | 
            +
             | 
| 249 | 
            +
                # Read from the file system to determine how many account profiles we have.
         | 
| 250 | 
            +
                objects = []
         | 
| 251 | 
            +
                Dir.glob("#{directory}/account-schemas/*.json").each do |file|
         | 
| 252 | 
            +
                  object = {
         | 
| 253 | 
            +
                    "data" => file,
         | 
| 254 | 
            +
                    "source" => File.basename( file, ".json" ).split(" - ")[1],
         | 
| 255 | 
            +
                    "objectType" => File.basename( file, ".json" ).split(" - ")[2]
         | 
| 256 | 
            +
                  }
         | 
| 257 | 
            +
                  objects.push( object )
         | 
| 258 | 
            +
                end
         | 
| 259 | 
            +
             | 
| 260 | 
            +
                $log.info "\tDetected #{objects.count} account schemas."
         | 
| 261 | 
            +
             | 
| 262 | 
            +
                objects.each do |object|
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                  $log.info "\tAccount Schema: #{object['source']} - #{object['objectType']}"
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                  existing_source_id = Sources.get_cc_id( object['source'] )
         | 
| 267 | 
            +
             | 
| 268 | 
            +
                  unless existing_source_id.nil?
         | 
| 269 | 
            +
             | 
| 270 | 
            +
                    data = Program.read_file( object['data'] )
         | 
| 271 | 
            +
             | 
| 272 | 
            +
                    unless data.nil?
         | 
| 273 | 
            +
             | 
| 274 | 
            +
                      account_schema = JSON.parse( data )
         | 
| 275 | 
            +
             | 
| 276 | 
            +
                      account_schema['attributes'].each do |attribute|
         | 
| 277 | 
            +
             | 
| 278 | 
            +
                        $log.debug "\t\tAnalyzing attribute [#{attribute['name']}]."
         | 
| 279 | 
            +
             | 
| 280 | 
            +
                        existing_attribute = AccountSchemas.get_existing_schema_attribute( existing_source_id, attribute['name'] ) # Get the existing attribute
         | 
| 281 | 
            +
             | 
| 282 | 
            +
                        unless attribute == existing_attribute
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                          unless existing_attribute.nil?
         | 
| 285 | 
            +
                            AccountSchemas.update( existing_source_id, attribute ) # If the existing attribute is not-null, we'll update.
         | 
| 286 | 
            +
                          else
         | 
| 287 | 
            +
                            AccountSchemas.create( existing_source_id, attribute ) # If the existing attribute is null, we'll create.
         | 
| 288 | 
            +
                          end # unless existing_attribute.nil?
         | 
| 289 | 
            +
             | 
| 290 | 
            +
                        else
         | 
| 291 | 
            +
                          $log.debug "\t\t\tNo action to perform."
         | 
| 292 | 
            +
                        end # attribute == existing_attribute
         | 
| 293 | 
            +
             | 
| 294 | 
            +
                      end # account_schema['attributes'].each do |attribute|
         | 
| 295 | 
            +
             | 
| 296 | 
            +
                    end # unless data.nil?
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                  end # unless existing_source_id.nil?
         | 
| 299 | 
            +
             | 
| 300 | 
            +
                end # objects.each do |object|
         | 
| 301 | 
            +
             | 
| 302 | 
            +
              end
         | 
| 303 | 
            +
             | 
| 304 | 
            +
              #
         | 
| 305 | 
            +
              # Documents Account Schema configurations.
         | 
| 306 | 
            +
              #
         | 
| 307 | 
            +
              def self.doc
         | 
| 308 | 
            +
                Markdown.h2( "Account Schemas" )
         | 
| 309 | 
            +
             | 
| 310 | 
            +
                response = IDNAPI.get( "#{$url}/cc/api/source/list", $token )
         | 
| 311 | 
            +
                unless response.nil?
         | 
| 312 | 
            +
                  sources = JSON.parse( response.body )
         | 
| 313 | 
            +
             | 
| 314 | 
            +
                  $log.info "\tDetected #{sources.count} account schemas, across #{sources.count} sources."
         | 
| 315 | 
            +
             | 
| 316 | 
            +
                  sources.each do |source|
         | 
| 317 | 
            +
             | 
| 318 | 
            +
                    accountSchema = IDNAPI.get( "#{$url}/cc/api/source/getAccountSchema/#{source["id"]}", $token )
         | 
| 319 | 
            +
                    unless accountSchema.nil?
         | 
| 320 | 
            +
                      accountSchema = JSON.parse( accountSchema.body )
         | 
| 321 | 
            +
                      $log.info "\tAccount Schema: #{source["name"]} - #{accountSchema["objectType"]}"
         | 
| 322 | 
            +
             | 
| 323 | 
            +
                      unless ( accountSchema["attributes"].nil? || accountSchema["attributes"].empty? )
         | 
| 324 | 
            +
                        Markdown.h3( "#{source["name"]}" )
         | 
| 325 | 
            +
                        Markdown.text( " - Object Type: #{accountSchema["objectType"]}\n" )
         | 
| 326 | 
            +
                        Markdown.text( " - Account ID: #{accountSchema["identityAttribute"]}\n" )
         | 
| 327 | 
            +
                        Markdown.text( " - Account Name: #{accountSchema["displayAttribute"]}\n" )
         | 
| 328 | 
            +
                        Markdown.text( " - Group Attribute: #{accountSchema["groupAttribute"]}\n\n" )
         | 
| 329 | 
            +
                        Markdown.text( "| Name | Description | Type | Multi-valued | Entitlement |\n" )
         | 
| 330 | 
            +
                        Markdown.text( "|------|-------------|------|--------------|-------------|\n" )
         | 
| 331 | 
            +
                        accountSchema["attributes"].each do |attribute|
         | 
| 332 | 
            +
                          Markdown.text( "|#{attribute["name"]}|#{attribute["description"]}|#{attribute["type"]}|#{attribute["entitlement"]}|#{attribute["multi"]}|\n" )
         | 
| 333 | 
            +
                        end
         | 
| 334 | 
            +
                      end
         | 
| 335 | 
            +
                    end
         | 
| 336 | 
            +
                  end
         | 
| 337 | 
            +
                end
         | 
| 338 | 
            +
             | 
| 339 | 
            +
                Markdown.write
         | 
| 340 | 
            +
              end
         | 
| 341 | 
            +
            end
         |