opennebula 6.99.90.pre → 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 +4 -4
- data/lib/cloud/CloudClient.rb +1 -1
- data/lib/opennebula/flow/validator.rb +4 -3
- data/lib/opennebula/group.rb +6 -1
- data/lib/opennebula/saml_auth.rb +254 -0
- data/lib/opennebula.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1edcccf0b9e0d503d1c4d28cd64205a58eb85d0f0ab6d62d2f7d6c229b27d841
|
|
4
|
+
data.tar.gz: 47ee96dcfcb9d7b92243ea88eb6f9630866537539ef7bfc1cd7ea7fc698c28b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3f94e3f828c2e31c71e8bdf12c5dfc6a4d4a72c92c4333f5a71c85fb7a35e8bdb108fd9b5834e8849d79431888c4402cb047ce688371db6c1feeb29368600c6c
|
|
7
|
+
data.tar.gz: 1c1969f628aea70d2fc5702fc8103ffd9e9a4fb22a01f46003c32994829acc3e049be89a85a93c635dcc911cdbaf6f10d1ed45743a25e122bf7e8dacbdb88f02
|
data/lib/cloud/CloudClient.rb
CHANGED
|
@@ -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
|
|
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
|
data/lib/opennebula/group.rb
CHANGED
|
@@ -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 = "
|
|
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
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:
|
|
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-
|
|
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
|
|
@@ -197,9 +198,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
197
198
|
version: '0'
|
|
198
199
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
199
200
|
requirements:
|
|
200
|
-
- - "
|
|
201
|
+
- - ">="
|
|
201
202
|
- !ruby/object:Gem::Version
|
|
202
|
-
version:
|
|
203
|
+
version: '0'
|
|
203
204
|
requirements: []
|
|
204
205
|
rubygems_version: 3.3.5
|
|
205
206
|
signing_key:
|