aaf-mdqt 0.8.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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/codeql-analysis.yml +70 -0
  3. data/.github/workflows/ruby.yml +41 -0
  4. data/.gitignore +25 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +1 -0
  7. data/.rubocop_todo.yml +296 -0
  8. data/.ruby-version +1 -0
  9. data/.tool-versions +1 -0
  10. data/.travis.yml +7 -0
  11. data/CHANGELOG.md +168 -0
  12. data/CODE_OF_CONDUCT.md +74 -0
  13. data/Gemfile +9 -0
  14. data/LICENSE.txt +21 -0
  15. data/Makefile +4 -0
  16. data/README.md +268 -0
  17. data/Rakefile +5 -0
  18. data/aaf-mdqt.gemspec +46 -0
  19. data/bin/console +14 -0
  20. data/bin/setup +8 -0
  21. data/cucumber.yml +2 -0
  22. data/exe/mdqt +174 -0
  23. data/lib/mdqt/cli/base.rb +190 -0
  24. data/lib/mdqt/cli/cache_control.rb +25 -0
  25. data/lib/mdqt/cli/check.rb +78 -0
  26. data/lib/mdqt/cli/compliance.rb +0 -0
  27. data/lib/mdqt/cli/defaults.rb +70 -0
  28. data/lib/mdqt/cli/entities.rb +47 -0
  29. data/lib/mdqt/cli/exists.rb +0 -0
  30. data/lib/mdqt/cli/get.rb +130 -0
  31. data/lib/mdqt/cli/list.rb +65 -0
  32. data/lib/mdqt/cli/ln.rb +81 -0
  33. data/lib/mdqt/cli/ls.rb +54 -0
  34. data/lib/mdqt/cli/rename.rb +75 -0
  35. data/lib/mdqt/cli/reset.rb +27 -0
  36. data/lib/mdqt/cli/services.rb +25 -0
  37. data/lib/mdqt/cli/transform.rb +33 -0
  38. data/lib/mdqt/cli/url.rb +37 -0
  39. data/lib/mdqt/cli/version.rb +17 -0
  40. data/lib/mdqt/cli.rb +24 -0
  41. data/lib/mdqt/client/identifier_utils.rb +51 -0
  42. data/lib/mdqt/client/metadata_file.rb +144 -0
  43. data/lib/mdqt/client/metadata_response.rb +182 -0
  44. data/lib/mdqt/client/metadata_service.rb +194 -0
  45. data/lib/mdqt/client/metadata_validator.rb +81 -0
  46. data/lib/mdqt/client.rb +83 -0
  47. data/lib/mdqt/schema/MetadataExchange.xsd +112 -0
  48. data/lib/mdqt/schema/mdqt_check_schema.xsd +5 -0
  49. data/lib/mdqt/schema/oasis-200401-wss-wssecurity-secext-1.0.xsd +195 -0
  50. data/lib/mdqt/schema/oasis-200401-wss-wssecurity-utility-1.0.xsd +108 -0
  51. data/lib/mdqt/schema/saml-schema-assertion-2.0.xsd +283 -0
  52. data/lib/mdqt/schema/saml-schema-metadata-2.0.xsd +337 -0
  53. data/lib/mdqt/schema/ws-addr.xsd +137 -0
  54. data/lib/mdqt/schema/ws-authorization.xsd +145 -0
  55. data/lib/mdqt/schema/ws-federation.xsd +471 -0
  56. data/lib/mdqt/schema/ws-securitypolicy-1.2.xsd +1205 -0
  57. data/lib/mdqt/schema/xenc-schema.xsd +136 -0
  58. data/lib/mdqt/schema/xml.xsd +287 -0
  59. data/lib/mdqt/schema/xmldsig-core-schema.xsd +309 -0
  60. data/lib/mdqt/version.rb +3 -0
  61. data/lib/mdqt.rb +5 -0
  62. data/lib/tasks/cucumber.rake +8 -0
  63. data/lib/tasks/spec.rake +5 -0
  64. data/lib/tasks/tests.rake +6 -0
  65. data/lib/tasks/yard.rake +6 -0
  66. metadata +332 -0
@@ -0,0 +1,54 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class Ls < Base
8
+
9
+ def run
10
+
11
+ options.validate = true
12
+
13
+ advise_on_xml_signing_support
14
+ halt!("Cannot check a metadata file without XML support: please install additional gems") unless MDQT::Client.verification_available?
15
+
16
+ client = MDQT::Client.new(
17
+ options.service,
18
+ verbose: options.verbose,
19
+ explain: options.explain ? true : false,
20
+ )
21
+
22
+ results = []
23
+
24
+ p_args = args.empty? ? Dir.glob("*.xml") : args
25
+
26
+ p_args.each do |filename|
27
+
28
+ file = client.open_metadata(filename)
29
+
30
+ halt!("Cannot access file #{filename}") unless file.readable?
31
+
32
+ #halt!("File #{filename} is a metadata aggregate, cannot create entityID hashed link!") if file.aggregate?
33
+ next if file.aggregate?
34
+ halt!("XML validation failed for #{filename}:\n#{file.validation_error}") unless file.valid?
35
+
36
+ halt!("Cannot find entityID for #{filename}") unless file.entity_id
37
+
38
+ results << {id: file.entity_id, type: file.type, filename: file.basename}
39
+
40
+ end
41
+
42
+ results.sort_by { | r | [r[:id], r[:type]] }.each {|r| puts "#{r[:id]}, #{r[:type]}, #{r[:filename]}" }
43
+
44
+ end
45
+
46
+ end
47
+
48
+ private
49
+
50
+ end
51
+
52
+ end
53
+
54
+
@@ -0,0 +1,75 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class Rename < Base
8
+
9
+ def run
10
+
11
+ options.validate = true
12
+
13
+ advise_on_xml_signing_support
14
+ halt!("Cannot check a metadata file without XML support: please install additional gems") unless MDQT::Client.verification_available?
15
+
16
+ client = MDQT::Client.new(
17
+ options.service,
18
+ verbose: options.verbose,
19
+ explain: options.explain ? true : false,
20
+ )
21
+
22
+ halt!("Please specify a file to rename!") if args.empty?
23
+
24
+ args.each do |filename|
25
+
26
+ next if File.symlink?(filename)
27
+
28
+ file = client.open_metadata(filename)
29
+
30
+ halt!("Cannot access file #{filename}") unless file.readable?
31
+ halt!("File #{filename} is a metadata aggregate, cannot rename to hashed entityID!") if file.aggregate?
32
+ halt!("XML validation failed for #{filename}:\n#{file.validation_error}") unless file.valid?
33
+
34
+ halt!("Cannot find entityID for #{filename}") unless file.entity_id
35
+
36
+ newname = file.linkname # Using the same name as the link, not super-obvious
37
+ next if filename == newname
38
+
39
+ if file.turd?
40
+ hey "Warning: will not process backup/turd files"
41
+ next
42
+ end
43
+
44
+ message = ""
45
+
46
+ if File.exist?(newname)
47
+ if options.force
48
+ File.delete(newname)
49
+ else
50
+ halt!("Cannot rename #{filename} to #{newname} - File exists! Use --force to override")
51
+ next
52
+ end
53
+ end
54
+
55
+ File.rename(filename, newname)
56
+
57
+ if options.link
58
+ File.delete(filename) if options.force && File.exist?(filename)
59
+ File.symlink(newname, filename) unless newname == filename
60
+ end
61
+
62
+ hey("#{filename} renamed to #{newname} [#{file.entity_id}] #{message}") if options.verbose
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+
69
+ private
70
+
71
+ end
72
+
73
+ end
74
+
75
+
@@ -0,0 +1,27 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class Reset < Base
8
+
9
+ def run
10
+
11
+ client = MDQT::Client.new(
12
+ :not_required,
13
+ verbose: false,
14
+ cache_type: :file
15
+ )
16
+
17
+ print "Removing all cached files... "
18
+ client.cache_reset!
19
+ yay "done."
20
+
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+
@@ -0,0 +1,25 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class Services < Base
8
+
9
+ def run
10
+
11
+ puts "\nKnown services:"
12
+ puts
13
+ MDQT::CLI::Defaults.services.each do |service|
14
+ puts "#{service[:alias]}: #{service[:url]}"
15
+ end
16
+ puts
17
+ puts "Specify these in commands using the --service option or MDQ_BASE_URL environment variable"
18
+ puts
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
25
+
@@ -0,0 +1,33 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class Transform < Base
8
+
9
+ IdentifierUtils = MDQT::Client::IdentifierUtils
10
+
11
+ def run
12
+
13
+ halt!("No entityIDs have been specified!") if args.empty?
14
+
15
+ args.each do |arg|
16
+ puts transform(arg)
17
+ end
18
+
19
+ end
20
+
21
+ def transform(arg)
22
+ arg = arg.strip
23
+ return arg if IdentifierUtils.valid_transformed?(arg)
24
+ return IdentifierUtils.correct_lazy_transformed(arg) if IdentifierUtils.lazy_transformed?(arg)
25
+ return IdentifierUtils.correct_fish_transformed(arg) if IdentifierUtils.fish_transformed?(arg)
26
+ IdentifierUtils.transform_uri(arg)
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
33
+
@@ -0,0 +1,37 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+ require 'uri'
7
+
8
+ class URL < Base
9
+
10
+ def run
11
+
12
+ mds = MDQT::Client::MetadataService.new(service_url(options),
13
+ verbose:false,
14
+ cache_type: :none,
15
+ explain: false,
16
+ tls_cert_check: false)
17
+
18
+ if args.empty?
19
+ puts service_url(options)
20
+ else
21
+ args.each do |arg|
22
+ puts build_url(mds, arg)
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ def build_url(mds, entity_id)
29
+
30
+ URI.join(service_url(options), "entities/#{mds.prepare_id(entity_id)}")
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
37
+
@@ -0,0 +1,17 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class Version < Base
8
+
9
+ def run
10
+ puts "MDQT version #{MDQT::VERSION}"
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+ end
17
+
data/lib/mdqt/cli.rb ADDED
@@ -0,0 +1,24 @@
1
+ module MDQT
2
+ class CLI
3
+
4
+ require 'mdqt/client'
5
+
6
+ require 'mdqt/cli/defaults'
7
+ require 'mdqt/cli/cache_control'
8
+
9
+ require 'mdqt/cli/get'
10
+ require 'mdqt/cli/reset'
11
+ require 'mdqt/cli/version'
12
+ require 'mdqt/cli/transform'
13
+ require 'mdqt/cli/check'
14
+ require 'mdqt/cli/entities'
15
+ require 'mdqt/cli/ln'
16
+ require 'mdqt/cli/ls'
17
+ require 'mdqt/cli/list'
18
+ require 'mdqt/cli/services'
19
+ require 'mdqt/cli/rename'
20
+ require 'mdqt/cli/url'
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,51 @@
1
+ module MDQT
2
+ class Client
3
+
4
+ module IdentifierUtils
5
+
6
+ require 'digest'
7
+
8
+ class << self
9
+
10
+ def uri_to_sha1(uri)
11
+ raise "An empty string cannot be transformed" if uri.empty?
12
+ Digest::SHA1.hexdigest(uri.to_s.strip)
13
+ end
14
+
15
+ def transform_uri(uri)
16
+ "{sha1}#{uri_to_sha1(uri)}"
17
+ end
18
+
19
+ def normalize_to_sha1(identifier)
20
+ identifier.start_with?("{sha1}") ?
21
+ identifier.gsub("{sha1}", "") :
22
+ uri_to_sha1(identifier)
23
+ end
24
+
25
+ def valid_transformed?(string)
26
+ (string =~ /^[{]sha1[}][0-9a-f]{40}$/i).nil? ? false : true
27
+ end
28
+
29
+ def lazy_transformed?(string)
30
+ (string =~ /^[\[]sha1[\]][0-9a-f]{40}$/i).nil? ? false : true
31
+ end
32
+
33
+ def fish_transformed?(string)
34
+ (string =~ /^sha1[0-9a-f]{40}$/i).nil? ? false : true
35
+ end
36
+
37
+ def correct_lazy_transformed(lazy)
38
+ lazy.gsub("[sha1]", "{sha1}").downcase
39
+ end
40
+
41
+ def correct_fish_transformed(lazy)
42
+ lazy.gsub("sha1", "{sha1}").downcase
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,144 @@
1
+ module MDQT
2
+ class Client
3
+
4
+ class MetadataFile
5
+
6
+ require 'digest'
7
+
8
+ def initialize(filename, options = {})
9
+ @filename = File.absolute_path(filename)
10
+ @data = nil
11
+ @expires = nil
12
+ @etag = nil
13
+ @last_modified = nil
14
+ @explanation = {}
15
+ end
16
+
17
+ def filename
18
+ @filename
19
+ end
20
+
21
+ def basename
22
+ File.basename(filename)
23
+ end
24
+
25
+ def identifier
26
+ entity_id
27
+ end
28
+
29
+ def entity_id
30
+ raise "Incorrect metadata file type - aggregate" if aggregate?
31
+ @entity_id ||= extract_entity_id
32
+ end
33
+
34
+ def entity_ids
35
+ @entity_ids ||= extract_entity_ids
36
+ end
37
+
38
+ def data
39
+ @data ||= File.read(filename)
40
+ end
41
+
42
+ def readable?
43
+ File.readable?(filename)
44
+ end
45
+
46
+ def turd?
47
+ return true if basename.end_with?("~")
48
+ return true if basename.end_with?(".bak")
49
+ false
50
+ end
51
+
52
+ def type
53
+ @type ||= calculate_type
54
+ end
55
+
56
+ def aggregate?
57
+ type == :aggregate
58
+ end
59
+
60
+ def expires
61
+ @expires
62
+ end
63
+
64
+ def etag
65
+ @etag
66
+ end
67
+
68
+ def last_modified
69
+ @last_modified
70
+ end
71
+
72
+ def ok?
73
+ @ok
74
+ end
75
+
76
+ def signed?
77
+ @data.include? "Signature" # This is... not great
78
+ end
79
+
80
+ def verified_signature?(certs = [], _ = {})
81
+ validator = MetadataValidator.new(certs: [certs].flatten)
82
+ validator.verified_signature?(self)
83
+ end
84
+
85
+ def valid?
86
+ validator = MetadataValidator.new
87
+ validator.valid?(self)
88
+ end
89
+
90
+ def validation_error
91
+ validator = MetadataValidator.new
92
+ validator.validation_error(self)
93
+ end
94
+
95
+ def canonical_filename
96
+ if identifier.empty?
97
+ @filename = "aggregate-#{Digest::SHA1.hexdigest(@service)}.xml"
98
+ else
99
+ @filename ||= identifier.start_with?("{sha1}") ?
100
+ "#{@identifier.gsub("{sha1}", "")}.xml" :
101
+ "#{Digest::SHA1.hexdigest(@identifier)}.xml"
102
+ end
103
+ end
104
+
105
+ def linkname
106
+ if aggregate?
107
+ raise "Not supported for aggregates"
108
+ else
109
+ "#{Digest::SHA1.hexdigest(entity_id)}.xml"
110
+ end
111
+ end
112
+
113
+ def symlink?
114
+ File.symlink?(filename)
115
+ end
116
+
117
+ private
118
+
119
+ def calculate_type
120
+ return :aggregate if data.include?("<EntitiesDescriptor")
121
+ if data.include?("EntityDescriptor")
122
+ return :alias if symlink?
123
+ return :entity
124
+ end
125
+ :unknown
126
+ end
127
+
128
+ def xml_doc
129
+ Nokogiri::XML.parse(data).remove_namespaces!
130
+ end
131
+
132
+ def extract_entity_id
133
+ xml_doc.xpath("/EntityDescriptor/@entityID").text
134
+ end
135
+
136
+ def extract_entity_ids
137
+ xml_doc.xpath("//EntityDescriptor/@entityID").map(&:text)
138
+ end
139
+
140
+ end
141
+
142
+ end
143
+
144
+ end
@@ -0,0 +1,182 @@
1
+ module MDQT
2
+ class Client
3
+
4
+ class MetadataResponse
5
+
6
+ require 'digest'
7
+ require 'uri'
8
+
9
+ def initialize(identifier, service, http_response, options = {})
10
+
11
+ @requested_identitier = identifier
12
+ @identifier = URI.decode_www_form_component(identifier)
13
+ @service = service
14
+ @code = http_response.status || 500
15
+ @data = http_response.body || ""
16
+ @type = nil
17
+ @content_type = http_response.headers['Content-Type']
18
+ @expires = http_response.headers['Expires']
19
+ @etag = http_response.headers['ETag']
20
+ @last_modified = http_response.headers['Last-Modified']
21
+ @ok = http_response.success?
22
+ @explanation = {}
23
+
24
+ if options[:explain]
25
+ @explanation[:url] = http_response.env.url.to_s
26
+ @explanation[:method] = http_response.env.method.to_s.upcase
27
+ @explanation[:status] = http_response.status
28
+ @explanation[:response_headers] = http_response.headers
29
+ @explanation[:request_headers] = http_response.env.request_headers
30
+ end
31
+
32
+ end
33
+
34
+ def identifier
35
+ @identifier
36
+ end
37
+
38
+
39
+ def requested_identifier
40
+ @identifier
41
+ end
42
+
43
+ def entity_id
44
+ raise "Incorrect metadata file type - aggregate" if aggregate?
45
+ @entity_id ||= extract_entity_id
46
+ end
47
+
48
+ def entity_ids
49
+ @entity_ids ||= extract_entity_ids
50
+ end
51
+
52
+
53
+ def service
54
+ @service
55
+ end
56
+
57
+ def code
58
+ @code
59
+ end
60
+
61
+ def data
62
+ @data || ""
63
+ end
64
+
65
+ def type
66
+ @type ||= calculate_type
67
+ end
68
+
69
+ def content_type
70
+ @content_type
71
+ end
72
+
73
+ def expires
74
+ @expires
75
+ end
76
+
77
+ def etag
78
+ @etag
79
+ end
80
+
81
+ def last_modified
82
+ @last_modified
83
+ end
84
+
85
+ def ok?
86
+ @ok
87
+ end
88
+
89
+ def signed?
90
+ @data.include? "Signature" # This is... not great
91
+ end
92
+
93
+ def verified_signature?(certs = [], _ = {})
94
+ return true unless ok? # CHECK ?
95
+ validator = MetadataValidator.new(certs: [certs].flatten)
96
+ validator.verified_signature?(self)
97
+ end
98
+
99
+ def valid?
100
+ validator = MetadataValidator.new
101
+ validator.valid?(self)
102
+ end
103
+
104
+ def validation_error
105
+ validator = MetadataValidator.new
106
+ validator.validation_error(self)
107
+ end
108
+
109
+ def filename
110
+ if identifier.empty?
111
+ @filename = "aggregate-#{Digest::SHA1.hexdigest(@service)}.xml"
112
+ else
113
+ @filename ||= identifier.start_with?("{sha1}") ?
114
+ "#{@identifier.gsub("{sha1}","")}.xml" :
115
+ "#{Digest::SHA1.hexdigest(@identifier)}.xml"
116
+ end
117
+ end
118
+
119
+ def message
120
+ case code
121
+ when 200
122
+ identifier.empty? ? "[200] OK! Data for aggregate has been downloaded from #{service}" :
123
+ "[200] OK! Data for '#{identifier}' has been downloaded from #{service}"
124
+ when 304
125
+ "[304] OK! Data for '#{identifier}' is already available in a local cache"
126
+ when 400
127
+ "[400] The identifier '#{identifier}' ('#{requested_identifier}') is malformed or service URL #{service} is incorrect"
128
+ when 401
129
+ "[401] Credentials are incorrect or missing for '#{identifier}'"
130
+ when 403
131
+ identifier.empty? ? "[403] The MDQ service at #{service} does not support aggregate downloads" :
132
+ "[403] You do not have access rights to '#{identifier}' at #{service}"
133
+ when 404
134
+ identifier.empty? ? "[404] The MDQ service at #{service} is not responding with aggregated metadata or the correct status" :
135
+ "[404] Entity metadata for '#{identifier}' was not found at #{service}"
136
+ when 405
137
+ "[405] The service at #{service} believes the wrong HTTP method was used. We should have used HTTP GET..."
138
+ when 406
139
+ "[406] The requested content type is not available at #{service}"
140
+ when 500
141
+ "[500] An error has occurred at #{service}"
142
+ when 505
143
+ "[505] The service at #{service} claims our request was using pre-1999 web protocols, not HTTP 1.1 or later"
144
+ else
145
+ "[#{code}] Sorry - an unknown error has occurred requesting '#{identifier}' from #{service}.\nPlease report this bug!"
146
+ end
147
+ end
148
+
149
+ def explanation
150
+ @explanation
151
+ end
152
+
153
+ private
154
+
155
+ def calculate_type
156
+
157
+ return :html if data[0,1000].include?('<!doctype html>')
158
+ return :aggregate if data[0,5000].include?("EntitiesDescriptor")
159
+ if data[0,5000].include?("EntityDescriptor")
160
+ return :alias if symlink?
161
+ return :entity
162
+ end
163
+ :unknown
164
+ end
165
+
166
+ def xml_doc
167
+ Nokogiri::XML.parse(data).remove_namespaces!
168
+ end
169
+
170
+ def extract_entity_id
171
+ xml_doc.xpath("/EntityDescriptor/@entityID").text
172
+ end
173
+
174
+ def extract_entity_ids
175
+ xml_doc.xpath("//EntityDescriptor/@entityID").map(&:text)
176
+ end
177
+
178
+ end
179
+
180
+ end
181
+
182
+ end