Class: SolidObjects::Web
- Inherits:
-
Object
- Object
- SolidObjects::Web
- Defined in:
- lib/solid_objects/web.rb,
lib/solid_objects/web/route.rb,
lib/solid_objects/web/action.rb,
lib/solid_objects/web/router.rb,
lib/solid_objects/web/helpers.rb,
lib/solid_objects/web/paginator.rb,
lib/solid_objects/web/statistics.rb,
lib/solid_objects/web/application.rb,
lib/solid_objects/web/csrf_protection.rb,
sig/generated/lib/solid_objects/web.rbs,
sig/generated/lib/solid_objects/web/route.rbs,
sig/generated/lib/solid_objects/web/action.rbs,
sig/generated/lib/solid_objects/web/router.rbs,
sig/generated/lib/solid_objects/web/helpers.rbs,
sig/generated/lib/solid_objects/web/paginator.rbs,
sig/generated/lib/solid_objects/web/statistics.rbs,
sig/generated/lib/solid_objects/web/application.rbs,
sig/generated/lib/solid_objects/web/csrf_protection.rbs
Overview
An operator dashboard for the actor runtime, served as a Rack application:
Rails.application.routes.draw do
mount SolidObjects::Web => "/solid_objects"
end
It is deliberately not loaded by require "solid_objects". A worker
process must not carry a web stack, and an application that never mounts
the dashboard must not pay for it.
Every page asks configuration.authorize_administration first. That block
denies by default, so a mount alone exposes nothing until an application
states who may read it.
Defined Under Namespace
Modules: Helpers, Router Classes: Action, Application, CsrfProtection, Paginator, Route, Statistics
Constant Summary collapse
- ROOT =
File.("../../web", __dir__)
- VIEWS =
File.join(ROOT, "views")
- ASSETS =
File.join(ROOT, "assets")
- NONCE_KEY =
"solid_objects.content_security_policy_nonce"- CSRF_TOKEN_KEY =
"solid_objects.csrf_token"- NONCE_BYTES =
16- ASSET_CACHE_SECONDS =
86_400- TEMPLATE_NAME =
/\A_?[a-z][a-z0-9_]*\z/- CHART_LIBRARY_URL =
The dashboard charts need a charting library, and this one is fetched from a public CDN with a subresource integrity hash, so a compromised CDN cannot substitute other code. A deployment with no outbound network access should vendor the file and point
chart_library_urlat it, or set that to nil to render the dashboard without charts. "https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js"- CHART_LIBRARY_INTEGRITY =
"sha384-XcdcwHqIPULERb2yDEM4R0XaQKU3YnDsrTmjACBZyfdVVqjh6xQ4/DCMd7XLcA6Y"- DEFAULT_TABS =
{ "Dashboard" => "/", "Instances" => "/instances", "Mailbox" => "/mailbox", "Reminders" => "/reminders", "Effects" => "/effects", "Broadcasts" => "/broadcasts", "Dead letters" => "/dead_letters", "Processes" => "/processes" }.freeze
- LOCK =
Mutex.new
Class Attribute Summary collapse
Instance Attribute Summary collapse
-
#chart_library_integrity ⇒ Object
writeonly
A path below the mount serves a vendored copy; an absolute URL is fetched from that host and is named in the content security policy.
-
#chart_library_url ⇒ Object
writeonly
A path below the mount serves a vendored copy; an absolute URL is fetched from that host and is named in the content security policy.
Class Method Summary collapse
- .application ⇒ Web
- .call(env) ⇒ Array[untyped]
-
.chart_library_origin ⇒ String?
The origin the content security policy has to allow.
- .charts? ⇒ Boolean
- .middlewares ⇒ Array[[ Array[untyped], Proc? ]]
-
.register(extension, tab:, path:, views: nil) ⇒ void
Adds pages to the dashboard.
- .reset! ⇒ void
- .tabs ⇒ Hash[String, String]
- .template(name) ⇒ Object
-
.use(*arguments, &block) ⇒ void
The built stack is memoized, so a middleware added after the first request would otherwise be dropped without a word.
-
.views ⇒ Array[String]
Searched in order, so an extension directory added first wins over the packaged one and an application can replace a single page.
Instance Method Summary collapse
Class Attribute Details
.chart_library_integrity ⇒ String?
81 82 83 |
# File 'lib/solid_objects/web.rb', line 81 def chart_library_integrity defined?(@chart_library_integrity) ? @chart_library_integrity : CHART_LIBRARY_INTEGRITY end |
.chart_library_url ⇒ String?
76 77 78 |
# File 'lib/solid_objects/web.rb', line 76 def chart_library_url defined?(@chart_library_url) ? @chart_library_url : CHART_LIBRARY_URL end |
Instance Attribute Details
#chart_library_integrity=(value) ⇒ Object (writeonly)
A path below the mount serves a vendored copy; an absolute URL is fetched from that host and is named in the content security policy. nil renders the dashboard without charts.
59 60 61 |
# File 'sig/generated/lib/solid_objects/web.rbs', line 59 def chart_library_integrity=(value) @chart_library_integrity = value end |
#chart_library_url=(value) ⇒ Object (writeonly)
A path below the mount serves a vendored copy; an absolute URL is fetched from that host and is named in the content security policy. nil renders the dashboard without charts.
52 53 54 |
# File 'sig/generated/lib/solid_objects/web.rbs', line 52 def chart_library_url=(value) @chart_library_url = value end |
Class Method Details
.application ⇒ Web
170 171 172 |
# File 'lib/solid_objects/web.rb', line 170 def application LOCK.synchronize { @application ||= new } end |
.call(env) ⇒ Array[untyped]
165 166 167 |
# File 'lib/solid_objects/web.rb', line 165 def call(env) application.call(env) end |
.chart_library_origin ⇒ String?
The origin the content security policy has to allow. A vendored copy served from the mount has none, so the policy stays at 'self'.
93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/solid_objects/web.rb', line 93 def chart_library_origin url = chart_library_url return nil unless url&.include?("//") uri = URI.parse(url) return nil unless uri.scheme && uri.host "#{uri.scheme}://#{uri.host}" rescue URI::InvalidURIError nil end |
.charts? ⇒ Boolean
86 87 88 |
# File 'lib/solid_objects/web.rb', line 86 def charts? !chart_library_url.nil? end |
.middlewares ⇒ Array[[ Array[untyped], Proc? ]]
118 119 120 |
# File 'lib/solid_objects/web.rb', line 118 def middlewares @middlewares ||= [] end |
.register(extension, tab:, path:, views: nil) ⇒ void
This method returns an undefined value.
Adds pages to the dashboard. The extension receives the application class and declares its own routes on it, which means its routes carry an authorization policy like every other route.
135 136 137 138 139 140 141 142 143 144 |
# File 'lib/solid_objects/web.rb', line 135 def register(extension, tab:, path:, views: nil) if views self.views.unshift(views) # A template compiled before this call resolved against the old # search path, so a replacement view would never be reached. LOCK.synchronize { @templates = {} } end tabs[tab] = path extension.registered(Application) end |
.reset! ⇒ void
This method returns an undefined value.
154 155 156 157 158 159 160 161 162 |
# File 'lib/solid_objects/web.rb', line 154 def reset! LOCK.synchronize { @templates = {} } @tabs = nil @views = nil @middlewares = nil @application = nil remove_instance_variable(:@chart_library_url) if defined?(@chart_library_url) remove_instance_variable(:@chart_library_integrity) if defined?(@chart_library_integrity) end |
.tabs ⇒ Hash[String, String]
106 107 108 |
# File 'lib/solid_objects/web.rb', line 106 def tabs @tabs ||= DEFAULT_TABS.dup end |
.template(name) ⇒ Object
147 148 149 150 151 |
# File 'lib/solid_objects/web.rb', line 147 def template(name) LOCK.synchronize do templates[name] ||= ERB.new(File.read(template_path(name)), trim_mode: "-") end end |
.use(*arguments, &block) ⇒ void
This method returns an undefined value.
The built stack is memoized, so a middleware added after the first request would otherwise be dropped without a word.
125 126 127 128 |
# File 'lib/solid_objects/web.rb', line 125 def use(*arguments, &block) middlewares << [ arguments, block ] LOCK.synchronize { @application = nil } end |
.views ⇒ Array[String]
Searched in order, so an extension directory added first wins over the packaged one and an application can replace a single page.
113 114 115 |
# File 'lib/solid_objects/web.rb', line 113 def views @views ||= [ VIEWS ] end |
Instance Method Details
#app ⇒ Object
199 200 201 |
# File 'lib/solid_objects/web.rb', line 199 def app @app ||= build end |
#build ⇒ Object
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/solid_objects/web.rb', line 206 def build assets = ASSETS extra = self.class.middlewares ::Rack::Builder.new do use ::Rack::Static, urls: [ "/stylesheets", "/javascripts" ], root: assets, cascade: true, header_rules: [ [ :all, { "cache-control" => "private, max-age=#{ASSET_CACHE_SECONDS}" } ] ] extra.each { |arguments, block| use(*arguments, &block) } use CsrfProtection run Application.new end end |
#call(env) ⇒ Array[untyped]
193 194 195 196 |
# File 'lib/solid_objects/web.rb', line 193 def call(env) env[NONCE_KEY] = SecureRandom.base64(NONCE_BYTES) app.call(env) end |