Skip to content

Commit 60aadf7

Browse files
committed
Hide sensible node vars
Fixes #344
1 parent 6a42223 commit 60aadf7

File tree

9 files changed

+313
-55
lines changed

9 files changed

+313
-55
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
77
## [Unreleased]
88

99
### Added
10+
- Documentation of the configuration (@robertcheramy)
1011

1112
### Changed
12-
- Update weblibs to the latest version
13+
- Update weblibs to the latest version (@robertcheramy)
14+
- Depend on oxidized 0.34.0 (configuration of oxidized web as an extension) (@robertcheramy)
15+
- Use JSON to format the node metadata in /node/show (@robertcheramy)
1316

1417
### Fixed
1518
- Run puma directly, so that it does not rename the oxidized process. Fixes: 349 (@robertcheramy)
1619
- Display local time correctly and use epoch as timestamps in the URLs. Fixes: #258 and #356 (@robertcheramy)
20+
- Hide node vars when listed in the configuration entry hide_node_vars. Fixes: #344 (@robertcheramy)
1721

1822
## [0.16.0 - 2025-03-25]
1923
This release introduces the possibility for an extended configuration of
@@ -25,7 +29,7 @@ will only work with oxidized-web version 0.16.0 or later.
2529

2630
### Fixed
2731
- the table preferences (pagelength, column visibility, search) are stored in the local browser cache. Fixes #315 #314 #265 #211 (@robertcheramy)
28-
- Update "refresh" and "Upldate node list" to more meaningfull texts (@robertcheramy)
32+
- Update "refresh" and "Update node list" to more meaningful texts (@robertcheramy)
2933

3034
## [0.15.1 – 2025-02-20]
3135
This patch release fixes javascript errors.

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,25 @@
33
[![Build Status](https://github.com/ytti/oxidized-web/actions/workflows/ruby.yml/badge.svg)](https://github.com/ytti/oxidized-web/actions/workflows/ruby.yml)
44
[![Gem Version](https://badge.fury.io/rb/oxidized-web.svg)](http://badge.fury.io/rb/oxidized-web)
55

6-
Web userinterface and RESTful API for Oxidized.
6+
Web user interface and RESTful API for Oxidized.
77

8-
This is not useful independently, see https://github.com/ytti/oxidized for install instructions.
8+
## Installation
9+
This is not useful independently, see https://github.com/ytti/oxidized for
10+
install instructions.
911

12+
Note: because of its dependencies, installing the `oxidized-web` gem will
13+
install `oxidized` automatically.
14+
15+
## Configuration
16+
`oxidized-web` is configured through the configuration of `oxidized`,
17+
in the `extension` section. For details, see
18+
[docs/configuration.md](docs/configuration.md).
19+
20+
## Development
1021
If you wonder how to run oxidized-web from git for development, have a look at
1122
[docs/development.md](docs/development.md).
1223

1324
## License and Copyright
14-
1525
Copyright 2013-2015 Saku Ytti <[email protected]>
1626
2013-2015 Samer Abdel-Hafez <[email protected]>
1727

docs/configuration.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Basic configuration
2+
The RESTful API and web interface are enabled by installing the `oxidized-web`
3+
gem and configuring the `extensions.oxidized-web:` section in the configuration
4+
file:
5+
```yaml
6+
extensions:
7+
oxidized-web:
8+
load: true
9+
# enter your configuration here
10+
```
11+
12+
You can set the following parameter:
13+
- `load`: `true`/`false`: Enables or disables the `oxidized-web` extension
14+
(default: `false`)
15+
- `listen`: Specifies the interface to bind to (default: `127.0.0.1`). Valid
16+
options:
17+
- `127.0.0.1`: Allows IPv4 connections from localhost only
18+
- `'[::1]'`: Allows IPv6 connections from localhost only
19+
- `<IPv4-Address>` or `'[<IPv6-Address>]'`: Binds to a specific interface
20+
- `0.0.0.0`: Binds to any IPv4 interface
21+
- `'[::]'`: Binds to any IPv4 and IPv6 interface
22+
- `port`: Specifies the TCP port to listen to (default: `8888`)
23+
- `url_prefix`: Defines a URL prefix (default: no prefix)
24+
- `vhosts`: A list of virtual hosts to listen to. If not specified, it will
25+
respond to any virtual host.
26+
27+
## Examples
28+
29+
```yaml
30+
# Listen on http://[::1]:8888/
31+
extensions:
32+
oxidized-web:
33+
load: true
34+
listen: '[::1]'
35+
port: 8888
36+
```
37+
38+
```yaml
39+
# Listen on http://127.0.0.1:8888/
40+
extensions:
41+
oxidized-web:
42+
load: true
43+
listen: 127.0.0.1
44+
port: 8888
45+
```
46+
47+
```yaml
48+
# Listen on http://[2001:db8:0:face:b001:0:dead:beaf]:8888/oxidized/
49+
extensions:
50+
oxidized-web:
51+
load: true
52+
listen: '[2001:db8:0:face:b001:0:dead:beaf]'
53+
port: 8888
54+
url_prefix: oxidized
55+
```
56+
57+
```yaml
58+
# Listen on http://10.0.0.1:8000/oxidized/
59+
extensions:
60+
oxidized-web:
61+
load: true
62+
listen: 10.0.0.1
63+
port: 8000
64+
url_prefix: oxidized
65+
```
66+
67+
```yaml
68+
# Listen on any interface to http://oxidized.rocks:8888 and
69+
# http://oxidized:8888
70+
extensions:
71+
oxidized-web:
72+
load: true
73+
listen: '[::]'
74+
url_prefix: oxidized
75+
vhosts:
76+
- oxidized.rocks
77+
- oxidized
78+
```
79+
80+
# Hide node vars
81+
Some node vars (enable, password) can contain sensible data. You can list the
82+
vars to be hidden under `hide_node_vars`:
83+
```yaml
84+
extensions:
85+
oxidized-web:
86+
load: true
87+
hide_node_vars:
88+
- enable
89+
- password
90+
```

lib/oxidized/web.rb

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ class Web
1616

1717
def initialize(nodes, configuration)
1818
require 'oxidized/web/webapp'
19-
@addr, @port, uri_prefix, vhosts = self.class.parse_configuration(configuration)
20-
uri_prefix = "/#{uri_prefix}"
19+
@configuration = self.class.parse_configuration(configuration)
2120
WebApp.set :nodes, nodes
22-
WebApp.set :host_authorization, { permitted_hosts: vhosts }
21+
WebApp.set :configuration, @configuration
22+
WebApp.set :host_authorization, {
23+
permitted_hosts: @configuration[:vhosts]
24+
}
25+
uri_prefix = @configuration[:uri_prefix]
2326
@app = Rack::Builder.new do
2427
map uri_prefix do
2528
run WebApp
@@ -30,8 +33,10 @@ def initialize(nodes, configuration)
3033
def run
3134
@thread = Thread.new do
3235
@server = Puma::Server.new @app
33-
@server.add_tcp_listener @addr, @port
34-
logger.info "Oxidized-web server listening on #{@addr}:#{@port}"
36+
addr = @configuration[:addr]
37+
port = @configuration[:port]
38+
@server.add_tcp_listener addr, port
39+
logger.info "Oxidized-web server listening on #{addr}:#{port}"
3540
@server.run.join
3641
end
3742
end
@@ -46,12 +51,20 @@ def self.parse_configuration(configuration)
4651

4752
# New configuration style: extensions.oxidized-web
4853
def self.parse_new_configuration(configuration)
49-
addr = configuration.listen? || DEFAULT_HOST
50-
port = configuration.port? || DEFAULT_PORT
51-
uri_prefix = configuration.url_prefix? || DEFAULT_URI_PREFIX
52-
vhosts = configuration.vhosts? || []
53-
54-
[addr, port, uri_prefix, vhosts]
54+
hide_node_vars = configuration.hide_node_vars? || []
55+
unless hide_node_vars.is_a?(Array)
56+
logger.error "hide_node_vars must be a list of strings"
57+
hide_node_vars = []
58+
end
59+
hide_node_vars = hide_node_vars.map(&:to_sym)
60+
{
61+
addr: configuration.listen? || DEFAULT_HOST,
62+
port: configuration.port? || DEFAULT_PORT,
63+
uri_prefix: normalize_uri(configuration.url_prefix? ||
64+
DEFAULT_URI_PREFIX),
65+
vhosts: configuration.vhosts? || [],
66+
hide_node_vars: hide_node_vars
67+
}
5568
end
5669

5770
# Legacy configuration style: "rest: 127.0.0.1:8888/prefix"
@@ -62,11 +75,19 @@ def self.parse_legacy_configuration(configuration)
6275
port = addr
6376
addr = nil
6477
end
65-
port = port.to_i
66-
uri_prefix ||= DEFAULT_URI_PREFIX
67-
vhosts = []
78+
{
79+
addr: addr,
80+
port: port.to_i,
81+
uri_prefix: normalize_uri(uri_prefix || DEFAULT_URI_PREFIX),
82+
vhosts: [],
83+
hide_node_vars: []
84+
}
85+
end
86+
87+
def self.normalize_uri(uri_prefix)
88+
return '/' if uri_prefix.empty?
6889

69-
[addr, port, uri_prefix, vhosts]
90+
uri_prefix.start_with?('/') ? uri_prefix : "/#{uri_prefix}"
7091
end
7192
end
7293
end

lib/oxidized/web/views/node.haml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
%a.link-dark.link-underline-opacity-0{title: 'update',
1515
href: url_for("/node/next/#{@data[:full_name]}")}
1616
%i.bi.bi-repeat
17-
- out = '';PP.pp(@data,out)
1817
%pre.bg-body-tertiary.border.border-secondary-subtle.rounded
19-
= preserve "#{out}"
18+
%code
19+
=escape_once(JSON.pretty_generate(@data))
20+
2021

lib/oxidized/web/webapp.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
require 'sinatra/json'
33
require 'sinatra/url_for'
44
require 'tilt/haml'
5-
require 'pp'
65
require 'htmlentities'
76
require 'charlock_holmes'
87
module Oxidized
@@ -128,7 +127,7 @@ class WebApp < Sinatra::Base
128127

129128
get '/node/show/:node' do
130129
node, @json = route_parse :node
131-
@data = nodes.show node
130+
@data = filter_node_vars(nodes.show(node))
132131
out :node
133132
end
134133

@@ -259,7 +258,7 @@ def route_parse(param)
259258
[e.join('.'), json]
260259
end
261260

262-
# give the time enlapsed between now and a date (Time object)
261+
# give the time elapsed between now and a date (Time object)
263262
def time_from_now(date)
264263
return "no time specified" if date.nil?
265264

@@ -328,6 +327,20 @@ def convert_to_utf8(text)
328327
'The text contains binary values - cannot display'
329328
end
330329
end
330+
331+
def filter_node_vars(serialized_node)
332+
# Make a deep copy of the data, so we do not impact oxidized
333+
data = Marshal.load(Marshal.dump(serialized_node))
334+
335+
hide_node_vars = settings.configuration[:hide_node_vars]
336+
if data[:vars].is_a?(Hash) && hide_node_vars&.any?
337+
hide_node_vars.each do |key|
338+
data[:vars][key] = '<hidden>' if data[:vars].has_key?(key)
339+
end
340+
end
341+
342+
data
343+
end
331344
end
332345
end
333346
end

spec/web/node/show_spec.rb

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
require_relative '../../spec_helper'
2+
describe Oxidized::API::WebApp do
3+
include Rack::Test::Methods
4+
5+
def app
6+
Oxidized::API::WebApp
7+
end
8+
9+
before do
10+
@nodes = mock('Oxidized::Nodes')
11+
app.set(:nodes, @nodes)
12+
13+
@serialized_node = {
14+
name: "sw5",
15+
full_name: "sw5.example.com",
16+
ip: "10.42.12.42",
17+
group: nil,
18+
model: "ios",
19+
last: {
20+
start: Time.parse("2025-02-05 19:49:00 +0100"),
21+
end: Time.parse("2025-02-05 19:49:10 +0100"),
22+
status: :no_connection,
23+
time: 10
24+
},
25+
vars: {
26+
oxi: "dized",
27+
enable: "secret_enable",
28+
username: "oxidized",
29+
password: "secret_password"
30+
},
31+
mtime: Time.parse("2025-02-05 19:49:11 +0100")
32+
}
33+
end
34+
35+
describe "get /node/show/:node" do
36+
it "shows the metadata of a node" do
37+
app.set(:configuration, { hide_node_vars: [] })
38+
@nodes.expects(:show).with("sw5").returns(@serialized_node)
39+
40+
get '/node/show/sw5'
41+
_(last_response.ok?).must_equal true
42+
body = last_response.body
43+
44+
_(body).must_match(/secret_enable/)
45+
_(body).must_match(/secret_password/)
46+
_(body.include?(
47+
"<code>{\n &quot;name&quot;: &quot;sw5&quot;," \
48+
"\n &quot;full_name&quot;: &quot;sw5.example.com&quot;," \
49+
"\n &quot;ip&quot;: &quot;10.42.12.42&quot;," \
50+
"\n &quot;group&quot;: null,\n &quot;model&quot;: &quot;ios&quot;," \
51+
"\n &quot;last&quot;: {" \
52+
"\n &quot;start&quot;: &quot;2025-02-05 19:49:00 +0100&quot;," \
53+
"\n &quot;end&quot;: &quot;2025-02-05 19:49:10 +0100&quot;," \
54+
"\n &quot;status&quot;: &quot;no_connection&quot;," \
55+
"\n &quot;time&quot;: 10\n }," \
56+
"\n &quot;vars&quot;: {" \
57+
"\n &quot;oxi&quot;: &quot;dized&quot;," \
58+
"\n &quot;enable&quot;: &quot;secret_enable&quot;," \
59+
"\n &quot;username&quot;: &quot;oxidized&quot;," \
60+
"\n &quot;password&quot;: &quot;secret_password&quot;" \
61+
"\n },\n &quot;mtime&quot;: &quot;2025-02-05 19:49:11 +0100&quot;" \
62+
"\n}</code>"
63+
)).must_equal true
64+
end
65+
66+
it "hides vars in hide_node_vars" do
67+
app.set(:configuration, { hide_node_vars: %i[enable password] })
68+
@nodes.expects(:show).with("sw5").returns(@serialized_node)
69+
70+
get '/node/show/sw5'
71+
_(last_response.ok?).must_equal true
72+
body = last_response.body
73+
74+
_(body).wont_match(/secret_enable/)
75+
_(body).wont_match(/secret_password/)
76+
_(body).must_match(/&lt;hidden&gt;/)
77+
_(body.include?(
78+
"<code>{\n &quot;name&quot;: &quot;sw5&quot;," \
79+
"\n &quot;full_name&quot;: &quot;sw5.example.com&quot;," \
80+
"\n &quot;ip&quot;: &quot;10.42.12.42&quot;," \
81+
"\n &quot;group&quot;: null,\n &quot;model&quot;: &quot;ios&quot;," \
82+
"\n &quot;last&quot;: {" \
83+
"\n &quot;start&quot;: &quot;2025-02-05 19:49:00 +0100&quot;," \
84+
"\n &quot;end&quot;: &quot;2025-02-05 19:49:10 +0100&quot;," \
85+
"\n &quot;status&quot;: &quot;no_connection&quot;," \
86+
"\n &quot;time&quot;: 10\n }," \
87+
"\n &quot;vars&quot;: {" \
88+
"\n &quot;oxi&quot;: &quot;dized&quot;," \
89+
"\n &quot;enable&quot;: &quot;&lt;hidden&gt;&quot;," \
90+
"\n &quot;username&quot;: &quot;oxidized&quot;," \
91+
"\n &quot;password&quot;: &quot;&lt;hidden&gt;&quot;" \
92+
"\n },\n &quot;mtime&quot;: &quot;2025-02-05 19:49:11 +0100&quot;" \
93+
"\n}</code>"
94+
)).must_equal true
95+
96+
# The note data is not changed (deep copy with Marshal)
97+
_(@serialized_node[:vars][:enable]).must_equal "secret_enable"
98+
end
99+
end
100+
end

0 commit comments

Comments
 (0)