Experiments with switching of identities.
authorFrantišek Dvořák <valtri@civ.zcu.cz>
Thu, 9 Jun 2016 16:58:52 +0000 (18:58 +0200)
committerFrantišek Dvořák <valtri@civ.zcu.cz>
Thu, 9 Jun 2016 17:00:37 +0000 (19:00 +0200)
.rubocop.yml
README.md
lib/api.rb
lib/nebula.rb
lib/server_cipher_auth.rb [new file with mode: 0644]
swagger.yaml

index e272ac4..f348ad9 100644 (file)
@@ -15,6 +15,11 @@ AllCops:
 Metrics:
   Enabled: false
 
+# Use the return of the conditional for variable assignment and comparison
+# (wtf)
+Style/ConditionalAssignment:
+  Enabled: false
+
 # Favor format over sprintf
 # (easy to read)
 Style/FormatString:
index 70d5fdf..06b8c4b 100644 (file)
--- a/README.md
+++ b/README.md
@@ -3,14 +3,39 @@
 ## Overview
 This is the component to extend OpenNebula network orchestration capabilities.
 
-## Launch
-```
-rackup
-```
+## Admin Usage
+
+At OpenNebula host:
+
+    oneuser create nowadmin --driver server_cipher 'the-best-strongest-password-ever'
+    oneuser chgrp nowadmin oneadmin
+
+At NOW host (configration `/etc/now.yaml`):
+
+    opennebula:
+      admin_user: 'nowadmin'
+      admin_password: 'the-best-strongest-password-ever'
+      endpoint: http://nebula.example.com:2633/RPC2
+
+Launch NOW:
+
+    rackup
+
+## Usage
+
+List networks example:
+
+    curl http://now.example.com:9292/list?user=myuser
 
 ## Development
-```
-make run
-```
 
-It calls `rackup` through bundler.
+Using `make`:
+
+    make check
+    make run
+
+Or directly:
+
+    bundle install
+    bundle exec rubocop
+    bundle exec rackup
index 9396775..7e5c62f 100644 (file)
@@ -22,6 +22,18 @@ module Now
     before do
       # to sinatra request logger point to proper object
       env['rack.logger'] = $logger
+
+      switch_user(params['user'])
+    end
+
+    helpers do
+      def switch_user(user)
+        if user.nil?
+          @nebula.switch_server()
+        else
+          @nebula.switch_user(user)
+        end
+      end
     end
 
     get '/' do
@@ -35,6 +47,7 @@ module Now
         networks = @nebula.list_networks
         JSON.pretty_generate(networks)
       rescue NowError => e
+        logger.error "[HTTP #{e.code}] #{e.message}"
         halt e.code, e.message
       end
     end
@@ -45,6 +58,7 @@ module Now
         network = @nebula.get(params['id'])
         JSON.pretty_generate(network)
       rescue NowError => e
+        logger.error "[HTTP #{e.code}] #{e.message}"
         halt e.code, e.message
       end
     end
index 284a6a7..d0251cb 100644 (file)
@@ -20,44 +20,71 @@ public def deep_merge(second)
 end
 
 module Now
+
+  EXPIRE_LENGTH = 8 * 60 * 60
+
   # NOW core class for communication with OpenNebula
   class Nebula
-    attr_accessor :config, :logger, :client
+    attr_accessor :logger
+    @ctx = nil
+    @server_ctx = nil
+    @user_ctx = nil
 
     def load_config(file)
       c = YAML.load_file(file)
-      @logger.debug "Config file '#{file}' loaded"
+      logger.debug "Config file '#{file}' loaded"
       return c
     rescue Errno::ENOENT
-      @logger.debug "Config file '#{file}' not found"
+      logger.debug "Config file '#{file}' not found"
       return {}
     end
 
     def one_connect(url, credentials)
-      @logger.debug "Connecting to #{url}..."
-      @client = OpenNebula::Client.new(credentials, url)
+      logger.debug "Connecting to #{url} ..."
+      return OpenNebula::Client.new(credentials, url)
+    end
+
+    def switch_user(user)
+      admin_user = @config['opennebula']['admin_user']
+      admin_password = @config['opennebula']['admin_password']
+      logger.debug "Authentication from #{admin_user} to #{user}"
+
+      server_auth = ServerCipherAuth.new(admin_user, admin_password)
+      expiration = Time.now.to_i + EXPIRE_LENGTH
+      user_token = server_auth.login_token(expiration, user)
+
+      @user_ctx = one_connect(@url, user_token)
+      @ctx = @user_ctx
+    end
+
+    def switch_server()
+      admin_user = @config['opennebula']['admin_user']
+      admin_password = @config['opennebula']['admin_password']
+      logger.debug "Authentication to #{admin_user}"
+
+      direct_token = "#{admin_user}:#{admin_password}"
+      @server_ctx = one_connect(@url, direct_token)
+      @ctx = @server_ctx
     end
 
     def initialize()
       @logger = $logger
-      @logger.info "Starting Network Orchestrator Wrapper (NOW #{VERSION})"
+      logger.info "Starting Network Orchestrator Wrapper (NOW #{VERSION})"
       @config = {}
 
       c = load_config(::File.expand_path('../../etc/now.yaml', __FILE__))
       @config = @config.deep_merge(c)
-      #@logger.debug "Configuration: #{@config}"
+      #logger.debug "Configuration: #{@config}"
 
       c = load_config('/etc/now.yaml')
       @config = @config.deep_merge(c)
-      #@logger.debug "Configuration: #{@config}"
+      #logger.debug "Configuration: #{@config}"
 
-      url = @config['opennebula']['endpoint']
-      credentials = "#{@config['opennebula']['admin_user']}:#{@config['opennebula']['admin_password']}"
-      one_connect(url, credentials)
+      @url = @config['opennebula']['endpoint']
     end
 
     def list_networks()
-      vn_pool = OpenNebula::VirtualNetworkPool.new(client, -1)
+      vn_pool = OpenNebula::VirtualNetworkPool.new(@ctx, -1)
       check(vn_pool.info)
 
       networks = []
@@ -73,12 +100,12 @@ module Now
 
     def get(network_id)
       vn_generic = OpenNebula::VirtualNetwork.build_xml(network_id)
-      vn = OpenNebula::VirtualNetwork.new(vn_generic, @client)
+      vn = OpenNebula::VirtualNetwork.new(vn_generic, @ctx)
       check(vn.info)
 
       id = vn.id
       title = vn.name
-      @logger.debug "OpenNebula get(#{network_id}) ==> #{id}, #{title}"
+      logger.debug "OpenNebula get(#{network_id}) ==> #{id}, #{title}"
       network = Network.new(id: id, title: title)
 
       return network.to_hash
diff --git a/lib/server_cipher_auth.rb b/lib/server_cipher_auth.rb
new file mode 100644 (file)
index 0000000..ba2e796
--- /dev/null
@@ -0,0 +1,91 @@
+# -------------------------------------------------------------------------- #
+# Copyright 2002-2013, OpenNebula Project (OpenNebula.org), C12G Labs        #
+#                                                                            #
+# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
+# not use this file except in compliance with the License. You may obtain    #
+# a copy of the License at                                                   #
+#                                                                            #
+# http://www.apache.org/licenses/LICENSE-2.0                                 #
+#                                                                            #
+# Unless required by applicable law or agreed to in writing, software        #
+# distributed under the License is distributed on an "AS IS" BASIS,          #
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
+# See the License for the specific language governing permissions and        #
+# limitations under the License.                                             #
+#--------------------------------------------------------------------------- #
+
+require 'openssl'
+require 'digest/sha1'
+
+require 'base64'
+require 'fileutils'
+
+module Now
+  # Server authentication class. This method can be used by OpenNebula services
+  # to let access authenticated users by other means. It is based on OpenSSL
+  # symmetric ciphers
+  class ServerCipherAuth
+    ###########################################################################
+    # Constants with paths to relevant files and defaults
+    ###########################################################################
+    CIPHER = 'aes-256-cbc'
+
+    def initialize(srv_user, srv_passwd)
+      @srv_user   = srv_user
+      @srv_passwd = srv_passwd
+
+      if srv_passwd.nil? || srv_passwd.empty?
+        @key = ''
+      else
+        @key = ::Digest::SHA1.hexdigest(@srv_passwd)
+      end
+
+      @cipher = ::OpenSSL::Cipher::Cipher.new(CIPHER)
+    end
+
+    # Creates a ServerCipher for client usage
+    def self.new_client(srv_user, srv_passwd)
+      new(srv_user, srv_passwd)
+    end
+
+    # Generates a login token in the form:
+    #   - server_user:target_user:time_expires
+    # The token is then encrypted with the contents of one_auth
+    def login_token(expire, target_user = nil)
+      target_user ||= @srv_user
+      token_txt = "#{@srv_user}:#{target_user}:#{expire}"
+
+      token   = encrypt(token_txt)
+      token64 = ::Base64.encode64(token).strip.delete("\n")
+
+      "#{@srv_user}:#{target_user}:#{token64}"
+    end
+
+    # Returns a valid password string to create a user using this auth driver
+    def password
+      @srv_passwd
+    end
+
+    private
+
+    def encrypt(data)
+      @cipher.encrypt
+      @cipher.key = @key
+
+      rc = @cipher.update(data)
+      rc << @cipher.final
+
+      rc
+    end
+
+    def decrypt(data)
+      @cipher.decrypt
+      @cipher.key = @key
+
+      rc = @cipher.update(::Base64.decode64(data))
+      rc << @cipher.final
+
+      rc
+    end
+  end
+end
index bb2d3fb..f76e1f4 100644 (file)
@@ -19,7 +19,12 @@ paths:
             type: "string"
   /list:
     get:
-      parameters: []
+      parameters:
+        - in: "query"
+          name: "user"
+          description: "OpenNebula user identity"
+          required: false
+          type: "string"
       responses:
         200:
           description: "List existing networks"
@@ -40,6 +45,11 @@ paths:
           required: true
           type: "integer"
           format: "int64"
+        - in: "query"
+          name: "user"
+          description: "OpenNebula user identity"
+          required: false
+          type: "string"
       responses:
         200:
           description: "Get information about network"