Commit ec016c43 by source_reader

refactor joule modules to data apps and added support for nginx app proxy

parent 8201bc3b
......@@ -10,8 +10,9 @@ module Joule
db_service = UpdateDb.new(nilm.db)
result = StubService.new
result.absorb_status(db_service.run(@backend.dbinfo, @backend.db_schema))
module_service = UpdateModules.new(nilm)
result.absorb_status(module_service.run(@backend.module_schemas))
return result unless result.success?
app_service = UpdateApps.new(nilm)
result.absorb_status(app_service.run(@backend.app_schemas))
result
end
......
......@@ -62,6 +62,29 @@ module Joule
resp.parsed_response.deep_symbolize_keys
end
def app_schemas
begin
resp = self.class.get("#{@url}/app.json")
return nil unless resp.success?
items = resp.parsed_response
# if the site exists but is not a joule server...
required_keys = %w(name id)
items.each do |item|
unless item.respond_to?(:has_key?) &&
required_keys.all? {|s| item.key? s}
Rails.logger.warn "Error #{@url} is not a Joule node"
return nil
end
item.symbolize_keys!
end
rescue StandardError => e
Rails.logger.warn "Error retrieving app_schemas for #{@url}: [#{e}]"
return nil
end
items
end
def module_schemas
begin
resp = self.class.get("#{@url}/modules.json?statistics=1")
......@@ -85,6 +108,7 @@ module Joule
items
end
def module_interface(joule_module, req)
self.class.get("#{@url}/interface/#{joule_module.joule_id}/#{req}")
end
......
# frozen_string_literal: true
module Joule
# Handles construction of database objects
class UpdateApps
include ServiceStatus
def initialize(nilm)
super()
@nilm = nilm
end
def run(app_schemas)
if app_schemas.nil?
add_error("unable to retrieve app information")
return self
end
#remove the previous modules
@nilm.data_apps.destroy_all
app_schemas.each do |schema|
@nilm.data_apps << DataApp.new(name: schema[:name],
joule_id: schema[:id])
end
set_notice("refreshed apps")
self
end
end
end
\ No newline at end of file
......@@ -13,7 +13,7 @@ module Joule
# check to make sure dbinfo and schema are set
# if either is nil, the database is not available
if dbinfo.nil? || schema.nil?
add_error("cannot contact database at #{@db.url}")
add_error("cannot contact node at #{@db.url}")
@db.update_attributes(available: false)
return self
end
......
class DataAppController < ApplicationController
before_action :authenticate_user!
def show
@app = DataApp.find(params[:id])
@nilm = @app.nilm
head :unauthorized and return unless current_user.views_nilm?(@nilm)
token = InterfaceAuthToken.create(data_app: @app,
user: current_user, expiration: 5.minutes.from_now)
@auth_url = _app_auth_url(token)
end
private
def _app_auth_url(token)
#urls = Rails.application.config_for(:urls)
#eg: http://3.interfaces.wattsworth.net/authenticate?token=1234
Rails.configuration.app_auth_url.call(
token.data_app.id)+"?token="+token.value
end
def authenticate_interface_user
@current_user = User.find_by_id(session[:user_id])
@app = DataApp.find_by_id(session[:interface_id])
if @current_user.nil? || @app.nil?
return false
end
true
end
end
class InterfacesController < ActionController::Base
before_action :authenticate_interface_user!, except: [:authenticate]
before_action :create_adapter, only: [:get, :put, :post, :delete]
skip_before_action :verify_authenticity_token
after_action :allow_wattsworth_iframe
#GET /authenticate
def authenticate
#if the user is already authenticated just destroy the token and redirect
if _authenticate_interface_user
token = InterfaceAuthToken.find_by_value(params[:token])
token.destroy unless token.nil?
redirect_to Rails.configuration.interface_url_template.call(@joule_module.id)
return
end
# otherwise log them in
reset_session
token = InterfaceAuthToken.find_by_value(params[:token])
render :unauthorized and return if token.nil?
render :unauthorized and return if token.expiration < Time.now
token.destroy
session[:user_id]=token.user.id
session[:interface_id]=token.joule_module.id
redirect_to Rails.configuration.interface_url_template.call(token.joule_module.id)
end
#GET /logout
def logout
reset_session
redirect_to Rails.configuration.interface_url_template.call(token.joule_module.id)
end
#everything else is proxied
def get
path = create_proxy_path(request.fullpath, @joule_module.id)
proxied_response = @node_adapter.module_interface(@joule_module,path)
render plain: proxied_response.body
proxied_response.headers.each do |key,value|
response.headers[key] = value
end
end
def put
end
def post
path = create_proxy_path(request.fullpath, @joule_module.id)
proxied_response = @node_adapter.module_post_interface(@joule_module,path, request.raw_post)
render plain: proxied_response.body
proxied_response.headers.each do |key,value|
response.headers[key] = value
end
end
def delete
end
private
def authenticate_interface_user!
render :unauthorized unless _authenticate_interface_user
#verify the session matches the URL
#verify the user has permissions on this module
end
def _authenticate_interface_user
@current_user = User.find_by_id(session[:user_id])
@joule_module = JouleModule.find_by_id(session[:interface_id])
if @current_user.nil? || @joule_module.nil?
return false
end
#@role = @current_user.module_role(@joule_module)
#return false if @role.nil?
true
end
def allow_wattsworth_iframe
urls = Rails.application.config_for(:urls)
# TODO: check if this does anything...
response.headers['X-Frame-Options'] = "" #ALLOW-FROM #{urls['frontend']}"
end
def create_adapter
nilm = @joule_module.nilm
@node_adapter = NodeAdapterFactory.from_nilm(nilm)
if @node_adapter.nil?
@service = StubService.new
@service.add_error("Cannot contact installation")
render 'helpers/empty_response', status: :unprocessable_entity
end
if @node_adapter.node_type != 'joule'
render 'helpers/empty_response', status: :unprocessable_entity
end
end
def create_proxy_path(path, module_id)
chunks = path.split('/')
id_chunk = chunks.find_index(module_id.to_s)
chunks.drop(id_chunk+1).join('/')
end
end
......@@ -90,7 +90,7 @@ class NilmsController < ApplicationController
params.permit(:name, :description, :url)
end
def db_params
params.permit(:max_points_per_plot)
params.permit(:max_points_per_plot, :url)
end
#authorization based on nilms
......
class ProxyController < ActionController::Base
skip_before_action :verify_authenticity_token
# /app/id.json is DataApp#show
# /app/id/auth is authenticate
# /proxy/id is proxied by nginx
#GET /app/:id/auth
def authenticate
#if the user is already authenticated return the proxy url
if params[:token].nil?
if authenticate_interface_user
response.set_header('X-PROXY-URL', @app.url)
response.set_header('X-JOULE-KEY', @nilm.key)
head :ok and return
else
head :forbidden and return
end
end
# otherwise log them in and redirect to /proxy
#reset_session
token = InterfaceAuthToken.find_by_value(params[:token])
render :unauthorized and return if token.nil?
render :unauthorized and return if token.expiration < Time.now
token.destroy
session[:user_id]=token.user.id
response.set_header('X-JOULE-KEY', token.data_app.nilm.key)
redirect_to _app_proxy_url(token) and return
end
private
def _app_proxy_url(token)
#urls = Rails.application.config_for(:urls)
#eg: http://3.interfaces.wattsworth.net/authenticate?token=1234
Rails.configuration.app_proxy_url.call(token.data_app.id)
end
def authenticate_interface_user
@current_user = User.find_by_id(session[:user_id])
@app = DataApp.find_by_id(params[:id])
@nilm = @app.nilm
return false if @current_user.nil? || @app.nil?
return false unless @current_user.views_nilm?(@nilm)
true
end
end
class DataApp < ApplicationRecord
belongs_to :nilm
def url
"#{nilm.url}/app/#{joule_id}/"
end
end
class InterfaceAuthToken < ApplicationRecord
belongs_to :joule_module
belongs_to :data_app
belongs_to :user
after_initialize do |auth_token|
......
......@@ -12,4 +12,8 @@ class JouleModule < ApplicationRecord
[:id, :name, :description, :web_interface, :exec_cmd,
:status, :pid, :joule_id]
end
def url
"#{nilm.url}/app/#{joule_id}"
end
end
......@@ -11,6 +11,7 @@ class Nilm < ApplicationRecord
has_many :data_views_nilms
has_many :data_views, through: :data_views_nilms
has_many :joule_modules, dependent: :destroy
has_many :data_apps, dependent: :destroy
#---Validations-----
validates :name, presence: true, uniqueness: true
validates :url, presence: true, uniqueness: true
......
json.id @app.id
json.name @app.name
json.nilm_id @nilm.id
json.url @auth_url
json.extract! @joule_module, *JouleModule.json_keys
json.nilm_id @nilm.id
if @joule_module.web_interface
json.url @module_url
end
......@@ -13,9 +13,10 @@ json.data do
end
end
end
json.jouleModules(@nilm.joule_modules) do |m|
json.extract! m, *JouleModule.json_keys
json.url Rails.configuration.interface_url_template.call(m.id)
json.data_apps(@nilm.data_apps) do |app|
json.id app.id
json.name app.name
json.url Rails.configuration.app_proxy_url.call(app.id)
json.nilm_id @nilm.id
end
end
......
......@@ -31,7 +31,7 @@ module ControlPanel
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
# but still support cookies for interfaces
# but still support cookies for data_app
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
# Add folders under the services and adapters directory
......
......@@ -106,7 +106,7 @@ Rails.application.configure do
# NOTE: this requires a DNS server
return "https://#{id}.module.cloud.wattsworth.net"
#return "/api/interfaces/#{id}/"
#return "/api/data_app/#{id}/"
end
......
......@@ -78,12 +78,20 @@ Rails.application.configure do
#
config.send_emails = true
config.interface_url_template = lambda do |id|
config.app_auth_url = lambda do |id|
# change to subdomains for additional security
# NOTE: this requires a DNS server
# return "http://#{id}.interfaces.wattsworth.local"
# return "http://#{id}.data_app.wattsworth.local"
#
return "http://localhost:3000/interfaces/#{id}/"
return "http://127.0.0.1:3001/api/app/#{id}/auth"
end
config.app_proxy_url = lambda do |id|
# change to subdomains for additional security
# NOTE: this requires a DNS server
# return "http://#{id}.data_app.wattsworth.local"
#
return "http://127.0.0.1:3001/app/#{id}/"
end
end
......@@ -98,9 +98,9 @@ Rails.application.configure do
config.interface_url_template = lambda do |id|
# change to subdomains for additional security
# NOTE: this requires a DNS server
# return "http://#{id}.interfaces.wattsworth.local"
# return "http://#{id}.data_app.wattsworth.local"
#
return "/api/interfaces/#{id}/"
return "/api/data_app/#{id}/"
end
end
......@@ -41,8 +41,19 @@ Rails.application.configure do
# config.action_view.raise_on_missing_translations = true
# set up interface subdomain
config.interface_url_template = lambda do |id|
return "http://#{id}.interfaces.wattsworth.local"
end
config.app_auth_url = lambda do |id|
# change to subdomains for additional security
# NOTE: this requires a DNS server
# return "http://#{id}.data_app.wattsworth.local"
#
return "http://127.0.0.1:3001/api/app/#{id}/auth"
end
config.app_proxy_url = lambda do |id|
# change to subdomains for additional security
# NOTE: this requires a DNS server
# return "http://#{id}.data_app.wattsworth.local"
#
return "http://127.0.0.1:3001/app/#{id}/"
end
end
......@@ -54,10 +54,12 @@ Rails.application.routes.draw do
end
end
get 'interfaces/:id/authenticate', to: 'interfaces#authenticate'
get 'interfaces/:id', to: 'interfaces#get'
get 'interfaces/:id/*path', to: 'interfaces#get'
post 'interfaces/:id/*path', to: 'interfaces#post'
get 'app/:id/auth', to: 'proxy#authenticate'
get 'app/:id.json', to: 'data_app#show'
#get 'data_app/:id', to: 'data_app#get'
#get 'data_app/:id/*path', to: 'data_app#get'
#post 'data_app/:id/*path', to: 'data_app#post'
get 'index', to: 'home#index'
root to: 'home#index'
......
test:
api: http://wattsworth.local/api
interfaces: http://XX.interfaces.wattsworth.local
interfaces: http://XX.data_app.wattsworth.local
development:
api: http://wattsworth.local/api
interfaces: http://XX.interfaces.wattsworth.local
interfaces: http://XX.data_app.wattsworth.local
frontend: http://wattsworth.net
aws:
api: https://wattsworth.net/api
interfaces: https://XX.interfaces.wattsworth.net
interfaces: https://XX.data_app.wattsworth.net
class CreateDataApps < ActiveRecord::Migration[5.2]
def change
create_table :data_apps do |t|
t.string :name
t.string :url
t.belongs_to :nilm, index: true
t.timestamps
end
end
end
class RefactorJouleApp < ActiveRecord::Migration[5.2]
def change
rename_column :interface_auth_tokens, :joule_module_id, :data_app_id
rename_column :data_apps, :url, :joule_id
end
end
......@@ -10,7 +10,16 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_05_05_175223) do
ActiveRecord::Schema.define(version: 2019_08_20_005311) do
create_table "data_apps", force: :cascade do |t|
t.string "name"
t.string "joule_id"
t.integer "nilm_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["nilm_id"], name: "index_data_apps_on_nilm_id"
end
create_table "data_views", force: :cascade do |t|
t.integer "user_id"
......@@ -112,12 +121,12 @@ ActiveRecord::Schema.define(version: 2019_05_05_175223) do
create_table "interface_auth_tokens", force: :cascade do |t|
t.integer "user_id"
t.integer "joule_module_id"
t.integer "data_app_id"
t.string "value"
t.datetime "expiration"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["joule_module_id"], name: "index_interface_auth_tokens_on_joule_module_id"
t.index ["data_app_id"], name: "index_interface_auth_tokens_on_data_app_id"
t.index ["user_id"], name: "index_interface_auth_tokens_on_user_id"
end
......
require 'rails_helper'
RSpec.describe InterfacesController, type: :controller do
RSpec.describe DataAppController, type: :controller do
end
......@@ -125,15 +125,15 @@ RSpec.describe NilmsController, type: :request do
expect(body['data']['root_folder']['name']).to_not be_empty
end
end
it 'returns joule modules as json' do
test_module = create(:joule_module, name: 'test', description: 'sample')
john_nilm.joule_modules << test_module
it 'returns data apps as json' do
test_app = create(:data_app, name: 'test')
john_nilm.data_apps << test_app
get "/nilms/#{john_nilm.id}.json",
headers: john.create_new_auth_token
body = JSON.parse(response.body)
expect(body['data']['jouleModules'][0]['name']).to eq(test_module.name)
expect(body['data']['data_apps'][0]['name']).to eq(test_app.name)
# TODO: figure out a configuration for subdomains
#expect(body['data']['jouleModules'][0]['url']).to start_with("http://#{test_module.joule_id}.interfaces")
#expect(body['data']['jouleModules'][0]['url']).to start_with("http://#{test_module.joule_id}.data_app")
end
it 'refreshes nilm data when requested' do
@auth_headers = john.create_new_auth_token
......
FactoryBot.define do
factory :data_app do
end
end
require 'rails_helper'
RSpec.describe DataApp, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment