opennebula 7.0.0 → 7.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 235d927119b2e53f0e2968d09b2020f3bed88c988537d80483d36099f69713b2
4
- data.tar.gz: 830c3859007c69370867b2770f4f6df6e683a585f6edae3e445698ea59364a83
3
+ metadata.gz: 1edcccf0b9e0d503d1c4d28cd64205a58eb85d0f0ab6d62d2f7d6c229b27d841
4
+ data.tar.gz: 47ee96dcfcb9d7b92243ea88eb6f9630866537539ef7bfc1cd7ea7fc698c28b3
5
5
  SHA512:
6
- metadata.gz: 35a2957b7e63e3f8c4ac747f5aab913c4c439f1f14554ed064571a7b585de62db436a0f41a7ca484b3ba9c8e33299bc0cf42f1958107ec24677f061664324d83
7
- data.tar.gz: b2c227239cdbbd486e9101cea6c671338cc52bc58e73695d39e91511b2b3d616e4d0ad3f6281d173b42c05ac4de8215d6563e1ea1994c79be3f79380d8809262
6
+ metadata.gz: 3f94e3f828c2e31c71e8bdf12c5dfc6a4d4a72c92c4333f5a71c85fb7a35e8bdb108fd9b5834e8849d79431888c4402cb047ce688371db6c1feeb29368600c6c
7
+ data.tar.gz: 1c1969f628aea70d2fc5702fc8103ffd9e9a4fb22a01f46003c32994829acc3e049be89a85a93c635dcc911cdbaf6f10d1ed45743a25e122bf7e8dacbdb88f02
@@ -51,7 +51,7 @@ end
51
51
  module CloudClient
52
52
 
53
53
  # OpenNebula version
54
- VERSION = '7.0.0'
54
+ VERSION = '7.0.1'
55
55
 
56
56
  # #########################################################################
57
57
  # Default location for the authentication file
@@ -42,9 +42,10 @@ class Hash
42
42
 
43
43
  target[key] =
44
44
  if value.is_a?(Hash) && current.is_a?(Hash)
45
- current.deep_merge(value)
46
- elsif (value.is_a?(Array) && current.is_a?(Array)) && merge_array
47
- current + value
45
+ current.deep_merge(value, merge_array)
46
+ elsif value.is_a?(Array) && current.is_a?(Array) && merge_array
47
+ merged = current + value
48
+ merged.all? {|el| el.is_a?(Hash) } ? merged.uniq : merged
48
49
  else
49
50
  value
50
51
  end
@@ -42,6 +42,7 @@ module OpenNebula
42
42
  # The default view for group and group admins, must be defined in
43
43
  # sunstone_views.yaml
44
44
  GROUP_ADMIN_SUNSTONE_VIEWS = "groupadmin"
45
+ GROUP_SUNSTONE_VIEWS = "cloud"
45
46
 
46
47
  # Creates a Group description with just its identifier
47
48
  # this method should be used to create plain Group objects.
@@ -144,10 +145,14 @@ module OpenNebula
144
145
  # Add Sunstone views for the group
145
146
  if group_hash[:views]
146
147
  sunstone_attrs << "VIEWS=\"#{group_hash[:views].join(",")}\""
148
+ else
149
+ sunstone_attrs << "VIEWS=\"#{GROUP_SUNSTONE_VIEWS}\""
147
150
  end
148
151
 
149
152
  if group_hash[:default_view]
150
153
  sunstone_attrs << "DEFAULT_VIEW=\"#{group_hash[:default_view]}\""
154
+ else
155
+ sunstone_attrs << "DEFAULT_VIEW=\"#{GROUP_SUNSTONE_VIEWS}\""
151
156
  end
152
157
 
153
158
  # And the admin views
@@ -168,7 +173,7 @@ module OpenNebula
168
173
  if sunstone_attrs.length > 0
169
174
  do_update = true
170
175
 
171
- update_str = "SUNSTONE=[#{sunstone_attrs.join(",\n")}]\n"
176
+ update_str = "FIREEDGE=[#{sunstone_attrs.join(",\n")}]\n"
172
177
  end
173
178
 
174
179
  opennebula_attrs = []
@@ -0,0 +1,254 @@
1
+ # ---------------------------------------------------------------------------- #
2
+ # Copyright 2002-2024, OpenNebula Project, OpenNebula Systems #
3
+ # #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may #
5
+ # not use this file except in compliance with the License. You may obtain #
6
+ # a copy of the License at #
7
+ # #
8
+ # http://www.apache.org/licenses/LICENSE-2.0 #
9
+ # #
10
+ # Unless required by applicable law or agreed to in writing, software #
11
+ # distributed under the License is distributed on an "AS IS" BASIS, #
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
13
+ # See the License for the specific language governing permissions and #
14
+ # limitations under the License. #
15
+ # ---------------------------------------------------------------------------- #
16
+
17
+ require 'rubygems'
18
+ require 'opennebula/xml_utils'
19
+ require 'opennebula/client'
20
+ require 'opennebula/group_pool'
21
+ require 'yaml'
22
+ require 'onelogin/ruby-saml'
23
+
24
+ if !defined?(ONE_LOCATION)
25
+ ONE_LOCATION=ENV['ONE_LOCATION']
26
+ end
27
+
28
+ if !ONE_LOCATION
29
+ VAR_LOCATION='/var/lib/one/'
30
+ else
31
+ VAR_LOCATION=ONE_LOCATION+'/var/'
32
+ end
33
+
34
+ # A top-level module for OpenNebula related classes.
35
+ module OpenNebula
36
+
37
+ # This class handles SAML authentication responses and group mapping for OpenNebula.
38
+ class SamlAuth
39
+
40
+ def initialize(provider, config)
41
+ @options={
42
+ :issuer => nil,
43
+ :idp_cert => nil,
44
+ :user_field => 'NameID',
45
+ :group_field => 'memberOf',
46
+ :group_required => nil,
47
+ :mapping_generate => true,
48
+ :mapping_key => 'SAML_GROUP',
49
+ :mapping_mode => 'strict',
50
+ :mapping_timeout => 300,
51
+ :mapping_filename => 'saml_groups_1.yaml',
52
+ :mapping_default => 1,
53
+ :group_admin_name => 'cloud-admins'
54
+ }.merge(provider)
55
+
56
+ @options[:mapping_file_path] = VAR_LOCATION + @options[:mapping_filename]
57
+
58
+ @options[:sp_entity_id] = config[:sp_entity_id]
59
+ @options[:acs_url] = config[:acs_url]
60
+
61
+ if !options_ok?
62
+ raise StandardError,
63
+ 'Identity Provider configured options are not correct.' \
64
+ ' Please, configure a valid Identity Provider certificate.'
65
+ end
66
+
67
+ generate_mapping if @options[:mapping_generate]
68
+
69
+ load_mapping
70
+ end
71
+
72
+ def options_ok?
73
+ required_keys = [:issuer, :idp_cert, :sp_entity_id, :group_field]
74
+ return false unless required_keys.all? {|key| @options.key?(key) }
75
+
76
+ # Avoid XPath injection towards the assertion
77
+ !@options[:group_field].include?("'")
78
+ end
79
+
80
+ # Validates SAML assertion using the ruby-saml library.
81
+ # Returns true if the assertion is valid.
82
+ # If not valid, returns an array of errors with the failed validations.
83
+ #
84
+ # The following validations are performed:
85
+ # validations = [
86
+ # :validate_version, # SAML 2.0 is used
87
+ # :validate_id, # assertion contains an ID
88
+ # :validate_success_status, # status of the assertion
89
+ # :validate_num_assertion, # only a single assertion is contained
90
+ # :validate_signed_elements, # only valid elements are signed
91
+ # :validate_structure, # assertion against a specific schema
92
+ # :validate_no_duplicated_attributes, # duplicated attributes
93
+ # :validate_in_response_to, # provided request_id == inResponseTo
94
+ # :validate_one_conditions, # saml:Conditions exist
95
+ # :validate_conditions, # assertion is not expired
96
+ # :validate_one_authnstatement, # saml:AuthnStatement exists
97
+ # :validate_audience, # Audience == sp_entity_id
98
+ # :validate_destination, # destination == acs_url
99
+ # :validate_issuer, # assertion issuer matches the configured one
100
+ # :validate_session_expiration, # expiration (SessionNotOnOrAfter)
101
+ # :validate_subject_confirmation, # subject confirmation is correct
102
+ # :validate_name_id, # NameID element is present
103
+ # :validate_signature # assertion signature
104
+ # ]
105
+
106
+ # Source: https://github.com/SAML-Toolkits/ruby-saml/blob/fbbedc978300deb9355a8e505849666974ef2e67/lib/onelogin/ruby-saml/response.rb#L399
107
+
108
+ def validate_assertion(assertion_text)
109
+ saml_settings = OneLogin::RubySaml::Settings.new
110
+
111
+ saml_settings.idp_cert = @options[:idp_cert]
112
+ saml_settings.issuer = @options[:issuer]
113
+
114
+ saml_settings.sp_entity_id = @options[:sp_entity_id]
115
+ saml_settings.assertion_consumer_service_url = @options[:acs_url]
116
+
117
+ assertion = OneLogin::RubySaml::Response.new(assertion_text, :settings => saml_settings)
118
+
119
+ return if assertion.is_valid?
120
+
121
+ assertion.errors
122
+ end
123
+
124
+ def generate_mapping
125
+ file = @options[:mapping_file_path]
126
+
127
+ File.open(file, File::RDWR | File::CREAT) do |f|
128
+ # Shared lock for reading the file
129
+ f.flock(File::LOCK_SH)
130
+
131
+ stat = f.stat
132
+ age = Time.now.to_i - stat.mtime.to_i
133
+
134
+ break if age <= @options[:mapping_timeout]
135
+
136
+ # Switch to exclusive lock for writing
137
+ f.flock(File::LOCK_UN)
138
+ f.flock(File::LOCK_EX)
139
+
140
+ # Check stat again, it might have changed while we were waiting for the lock
141
+ stat = f.stat
142
+ age = Time.now.to_i - stat.mtime.to_i
143
+
144
+ break if age <= @options[:mapping_timeout]
145
+
146
+ client = OpenNebula::Client.new
147
+ group_pool = OpenNebula::GroupPool.new(client)
148
+
149
+ rc = group_pool.info
150
+
151
+ raise StandardError, rc.message if OpenNebula.is_error?(rc)
152
+
153
+ groups = [group_pool.get_hash['GROUP_POOL']['GROUP']].flatten
154
+ yaml = {}
155
+
156
+ groups.each do |group|
157
+ if group['TEMPLATE'] && group['TEMPLATE'][@options[:mapping_key]]
158
+ yaml[group['TEMPLATE'][@options[:mapping_key]]] = group['ID']
159
+ end
160
+ end
161
+
162
+ f.truncate(0)
163
+ f.rewind
164
+ f.write(yaml.to_yaml)
165
+ ensure
166
+ f.flock(File::LOCK_UN)
167
+ end
168
+ end
169
+
170
+ def load_mapping
171
+ file=@options[:mapping_file_path]
172
+
173
+ @mapping = {}
174
+
175
+ if File.exist?(file)
176
+ @mapping = YAML.safe_load(File.read(file))
177
+ end
178
+
179
+ @mapping = {} unless @mapping.class == Hash
180
+ end
181
+
182
+ def validate_required_group(idp_groups)
183
+ required = @options[:group_required]
184
+ return if required.nil?
185
+
186
+ return if idp_groups.include?(required) || idp_groups.include?("/#{required}")
187
+
188
+ raise StandardError,
189
+ 'The user does not belong to the required group.' \
190
+ " Groups reported by the IdP: #{idp_groups}" \
191
+ " Configured required group: #{required} ( /#{required} if using Keycloak )"
192
+ end
193
+
194
+ def get_groups(idp_groups)
195
+ is_admin = false
196
+ case @options[:mapping_mode]
197
+ # Direct mapping of SAML group names to ONE group IDs
198
+ when 'strict'
199
+ valid_mappings = idp_groups.map {|group| @mapping[group] }.compact
200
+
201
+ g_admin = @options[:group_admin_name]
202
+ is_admin = g_admin && idp_groups.include?(g_admin)
203
+ # Keycloak-specific group syntax and group nesting support (e.g. /group1/subgroup1)
204
+ when 'keycloak'
205
+ valid_mappings = []
206
+
207
+ idp_groups.each do |idp_group|
208
+ group_parts = idp_group.split('/')
209
+ group_parts.reject!(&:empty?)
210
+
211
+ # Build all possible parent group paths
212
+ (1..group_parts.length).each do |i|
213
+ # Create group path with leading slash (Keycloak format)
214
+ group_path = '/' + group_parts[0...i].join('/')
215
+
216
+ is_admin = true if group_path == @options[:group_admin_name]
217
+
218
+ # Check direct mapping first
219
+ if @mapping[group_path]
220
+ valid_mappings << @mapping[group_path]
221
+ elsif i == 1
222
+ # Try without the leading slash for single group parts
223
+ # E.g. in the mapping file "/group1" should be the same as "group1"
224
+ group_path_no_slash = group_parts[0]
225
+
226
+ is_admin = true if group_path_no_slash == @options[:group_admin_name]
227
+
228
+ if @mapping[group_path_no_slash]
229
+ valid_mappings << @mapping[group_path_no_slash]
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ valid_mappings.compact!
236
+ valid_mappings.uniq!
237
+ else
238
+ raise StandardError,
239
+ "Unsupported group mapping mode: #{@options[:mapping_mode]}." \
240
+ " Supported modes are 'strict' and 'keycloak'."
241
+ end
242
+
243
+ # Return the default group if no mapping is found
244
+ valid_mappings = [@options[:mapping_default].to_s] if valid_mappings.empty?
245
+
246
+ # Handle group admin case. Group admin can NOT be a nested group
247
+ valid_mappings = valid_mappings.map {|id| "*#{id}" } if is_admin
248
+
249
+ return valid_mappings.join(' ')
250
+ end
251
+
252
+ end
253
+
254
+ end
data/lib/opennebula.rb CHANGED
@@ -79,5 +79,5 @@ require 'opennebula/backupjob_pool'
79
79
  module OpenNebula
80
80
 
81
81
  # OpenNebula version
82
- VERSION = '7.0.0'
82
+ VERSION = '7.0.1'
83
83
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opennebula
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0
4
+ version: 7.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenNebula
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-02 00:00:00.000000000 Z
11
+ date: 2025-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -150,6 +150,7 @@ files:
150
150
  - lib/opennebula/oneflow_client.rb
151
151
  - lib/opennebula/pool.rb
152
152
  - lib/opennebula/pool_element.rb
153
+ - lib/opennebula/saml_auth.rb
153
154
  - lib/opennebula/security_group.rb
154
155
  - lib/opennebula/security_group_pool.rb
155
156
  - lib/opennebula/server_cipher_auth.rb