noms-command 0.5.0

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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +191 -0
  5. data/README.rst +376 -0
  6. data/ROADMAP.rst +127 -0
  7. data/Rakefile +49 -0
  8. data/TODO.rst +7 -0
  9. data/bin/noms2 +20 -0
  10. data/fixture/dnc.rb +120 -0
  11. data/fixture/identity +5 -0
  12. data/fixture/public/dnc.json +22 -0
  13. data/fixture/public/echo.json +7 -0
  14. data/fixture/public/files/data.json +12 -0
  15. data/fixture/public/files/foo.json +2 -0
  16. data/fixture/public/lib/dnc.js +81 -0
  17. data/fixture/public/lib/noms-args.js +13 -0
  18. data/fixture/public/lib/showopt.js +18 -0
  19. data/fixture/public/location.json +8 -0
  20. data/fixture/public/showopt.json +15 -0
  21. data/fixture/rig2json +21 -0
  22. data/lib/noms/command/application.rb +204 -0
  23. data/lib/noms/command/auth/identity.rb +62 -0
  24. data/lib/noms/command/auth.rb +117 -0
  25. data/lib/noms/command/base.rb +22 -0
  26. data/lib/noms/command/document.rb +59 -0
  27. data/lib/noms/command/error.rb +11 -0
  28. data/lib/noms/command/formatter.rb +178 -0
  29. data/lib/noms/command/urinion/data.rb +63 -0
  30. data/lib/noms/command/urinion.rb +29 -0
  31. data/lib/noms/command/useragent.rb +134 -0
  32. data/lib/noms/command/version.rb +7 -0
  33. data/lib/noms/command/window.rb +95 -0
  34. data/lib/noms/command/xmlhttprequest.rb +181 -0
  35. data/lib/noms/command.rb +107 -0
  36. data/noms-command.gemspec +30 -0
  37. data/spec/01noms-command_spec.rb +30 -0
  38. data/spec/02noms-command.sh +31 -0
  39. data/spec/03application_spec.rb +47 -0
  40. data/spec/04application_spec.rb +61 -0
  41. data/spec/05formatter_spec.rb +195 -0
  42. data/spec/06urinion_data.rb +20 -0
  43. data/spec/07js_spec.rb +87 -0
  44. data/spec/08xhr_spec.rb +209 -0
  45. data/spec/09bookmarks_spec.rb +60 -0
  46. data/spec/10auth_spec.rb +33 -0
  47. data/spec/spec_helper.rb +40 -0
  48. metadata +228 -0
data/ROADMAP.rst ADDED
@@ -0,0 +1,127 @@
1
+ noms-command Roadmap
2
+ ====================
3
+
4
+ Roadmap
5
+ -------
6
+
7
+ These items are necessary and a fairly well-identified goal.
8
+
9
+ Authentication
10
+ ~~~~~~~~~~~~~~
11
+
12
+ Authentication needs to be handled in a rich and correct way. One of
13
+ the original inspirations for replacing the **noms** v1 thick client
14
+ was the daunting nature of adding authentication code to all the different
15
+ client libraries for the NOMS_ components and then exposing an interface
16
+ to dealing with them to the command-line.
17
+
18
+ Instead, all the web plumbing and dealing with things like how to
19
+ present persistent web sessions to a user are going to be handled by the
20
+ general purpose web application client **noms**, and services like the
21
+ NOMS CMDB, NCC-API, NagUI-API, etc. can provide their own command-line
22
+ implementations. The NOMS_ components are likely to use standard Javascript
23
+ libraries to present a consistent look, feel and implementation, but
24
+ **noms** will provide the ability for a service provider to define a CLI
25
+ that looks and feels any way they want.
26
+
27
+ That means **noms** should be good at command-line web authentication. The
28
+ old v1 client could not do more than Basic authentication, and this necessitated
29
+ the storage of personal credentials in plaintext on the system, mixed with
30
+ other configuration like the ReST enpoint URLs and usage conveniences like
31
+ default values.
32
+
33
+ Instead **noms** will handle many authentication flavors in a way that is
34
+ as secure as possible:
35
+
36
+ ============================ =====================================================
37
+ Authentication Type Description
38
+ ============================ =====================================================
39
+ Username/password (basic, **noms** will prompt the user for credentials when
40
+ digest) receives the authorization required HTTP status, and
41
+ persist the result in an obfuscated, expiring form
42
+ for that origin.
43
+ ---------------------------- -----------------------------------------------------
44
+ Login URL with cookie-based **noms** honors redirects to a login URL in the
45
+ sessions same way as a web browser and stores cookies
46
+ using proper cookie expiration. **noms** doesn't
47
+ do HTML parsing, so authentication on the login
48
+ URL must be of some other type or scriptable
49
+ with Javascript.
50
+
51
+ This implies that Javascript must be able to
52
+ script the authentication dialog, which means
53
+ there must be some way to do prompting; possibly
54
+ with the ``window.prompt()`` interface.
55
+ ---------------------------- -----------------------------------------------------
56
+ Login URL with special token **noms** will offer a way for javascripts to persist
57
+ to be included in request state across requests. This mechanism will allow
58
+ headers a javascript to set special headers that it will
59
+ have access to for later requests.
60
+ ---------------------------- -----------------------------------------------------
61
+ Login URL with special token Similar to above, with a somewhat different request
62
+ to be included in request implementation.
63
+ bodies
64
+ ---------------------------- -----------------------------------------------------
65
+ OAuth **noms** will use httpclient's built-in OAuth
66
+ and prompt the user to authenticate with OAuth, and
67
+ use the OAuth token for subsequent requests.
68
+ ---------------------------- -----------------------------------------------------
69
+ Client certificate Prompt for passphrase or use authentication agent
70
+ to provide client certificate.
71
+ ============================ =====================================================
72
+
73
+ Storage
74
+ ~~~~~~~
75
+
76
+ In order to persist state across requests, **noms** should probably
77
+ have a `Web Storage`_ implementation.
78
+
79
+ .. _`Web Storage`: http://dev.w3.org/html5/webstorage/
80
+
81
+ Caching
82
+ ~~~~~~~
83
+
84
+ HTTP provides a rich way to control caching of resources, and **noms** should
85
+ honor these strictly for efficiency. It will honor ``Cache-control`` headers
86
+ and use ``ETags`` and ``If-Modified`` appropriately to avoid loading
87
+ application documents, scripts and even data unnecessarily. This should reduce
88
+ many of the inefficiencies associated with serving an interface from the server.
89
+
90
+ Crossroads
91
+ ----------
92
+
93
+ This is not a roadmap, but a series of ideas of how **noms** could be enhanced as
94
+ well as unanswered questions about how it should work.
95
+
96
+ I/O
97
+ ~~~
98
+
99
+ Its prototype does no local I/O. Outside of restricted situations like
100
+ `Web Storage`_ this is probably desirable. It's extremely typical of CLIs, even
101
+ those for "remote" data stores, to be able to do some I/O, and here are a couple
102
+ of ideas how:
103
+
104
+ * stdin - Right now there's no way to even read stdin. So if you want to make a
105
+ CLI for uploading batch data you can't even do it--your scripts only have access
106
+ to command-line arguments.
107
+
108
+ Use node.js-style `Readable Stream`_ implementation to have access to stdin.
109
+
110
+ .. _`Readable Stream`: https://nodejs.org/api/stream.html
111
+
112
+ Another thing is the possibility of doing I/O on select named files. **noms** could
113
+ perhaps pre-parse the command line and add the files mentioned on the command line
114
+ to an ACL which would allow downloaded scripts to access them. For example::
115
+
116
+ noms http://cmdb/cmdb.json generate-ansible-inventory --output=inventory.json
117
+
118
+ **noms** could scan the command line and guess at what files the user intends the
119
+ application to have access to, and allow the Javascript to open them using a node.js-like
120
+ stream implementation. Exactly how to do this safely would be a challenge. Are only
121
+ certain options allowed? Files that already exist (otherwise it would be easy to allow
122
+ the script to write to unintended files). No files with '..'? What about -oFile.json
123
+ vs. -onf?
124
+
125
+ Another I/O-related subject is that **noms** is currently completely request/response-
126
+ oriented. It might be nice to be able to stream output data, or input data for file-
127
+ or batch-upload type operations, or wait for events on a websocket.
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :test => [:spec, :testcmd]
7
+
8
+ task :testcmd do
9
+ ENV['RUBYLIB'] = ['lib', ENV['RUBYLIB']].join(':')
10
+ ENV['PATH'] = ['bin', ENV['PATH']].join(':')
11
+ Dir.new('spec').each do |script|
12
+ next unless script =~ /\.sh$/
13
+ puts script
14
+ system(File.join('spec', script))
15
+ end
16
+ end
17
+
18
+ # Start the DNC application web server on port 8787
19
+ task :start do
20
+ FileUtils.rm_r 'test' if File.directory? 'test'
21
+ system 'cp -R fixture test'
22
+ system("sh -c '#{RbConfig.ruby} test/dnc.rb >test/dnc.out 2>&1 &'")
23
+ end
24
+
25
+ task :status do
26
+ begin
27
+ pid = File.read('test/dnc.pid').to_i
28
+ Process.kill 0, File.read('test/dnc.pid').to_i
29
+ puts "Test server running (PID #{pid})"
30
+ rescue Errno::ESRCH
31
+ puts "Test server not running on PID #{pid}"
32
+ rescue Errno::ENOENT
33
+ puts "Test server not running (no pidfile)"
34
+ end
35
+ end
36
+
37
+ task :sync do
38
+ system 'cp -R fixture/* test'
39
+ end
40
+
41
+ task :stop do
42
+ Process.kill 'TERM', File.read('test/dnc.pid').to_i
43
+ FileUtils.rm 'test/dnc.pid'
44
+ end
45
+
46
+ task :clean do
47
+ rm_rf ['pkg', 'test']
48
+ end
49
+
data/TODO.rst ADDED
@@ -0,0 +1,7 @@
1
+ TODO
2
+ ====
3
+
4
+ * Flesh out example application CLI.
5
+ * Auth sessions
6
+ * Caching
7
+ * SSL handling, TOFU for SSL
data/bin/noms2 ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright 2015 Evernote Corporation, all rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'noms/command'
19
+
20
+ Process.exit NOMS::Command.run(ARGV)
data/fixture/dnc.rb ADDED
@@ -0,0 +1,120 @@
1
+ require 'sinatra/base'
2
+ require 'json'
3
+
4
+ # Implement Do Not Call List Example REST application
5
+ # and static file server
6
+ class DNC < Sinatra::Application
7
+
8
+ set :port, 8787
9
+ set :root, File.expand_path("#{File.dirname(__FILE__)}")
10
+ enable :static
11
+
12
+ File.open(File.join(settings.root, 'dnc.pid'), 'w') {|f| f.puts Process.pid }
13
+
14
+ def load_data
15
+ JSON.load(File.open(File.join(settings.root, 'public', 'files', 'data.json')))
16
+ end
17
+
18
+ def write_data(data)
19
+ File.open(File.join(settings.root, 'public', 'files', 'data.json'), 'w') { |fh| fh << data.to_json }
20
+ end
21
+
22
+ helpers do
23
+ def require_auth
24
+ return if authorized?
25
+ headers['WWW-Authenticate'] = 'Basic realm="Authorization Required"'
26
+ halt 401, "Not authorized\n"
27
+ end
28
+
29
+ def authorized?
30
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
31
+ @auth.provided? and @auth.basic? and @auth.credentials and @auth.credentials == ['testuser', 'testpass']
32
+ end
33
+ end
34
+
35
+ get '/readme' do
36
+ redirect 'https://raw.githubusercontent.com/en-jbrinkley/noms-command/master/README.rst', 'README'
37
+ end
38
+
39
+ get '/dnc' do
40
+ data = load_data
41
+ if request.query_string.empty?
42
+ [ 200, { 'Content-type' => 'application/json'},
43
+ JSON.pretty_generate(data) ]
44
+ else
45
+ [ 200, { 'Content-type' => 'application/json' },
46
+ JSON.pretty_generate(
47
+ data.select do |item|
48
+ params.keys.all? { |k| item[k.to_s] && item[k.to_s].to_s === params[k] }
49
+ end)
50
+ ]
51
+ end
52
+ end
53
+
54
+ get '/dnc/:id' do
55
+ data = load_data
56
+ object = data.find { |e| e['id'] == params[:id].to_i }
57
+ if object
58
+ [ 200, { 'Content-type' => 'application/json' },
59
+ JSON.pretty_generate(object) ]
60
+ else
61
+ 404
62
+ end
63
+ end
64
+
65
+ post '/dnc' do
66
+ request.body.rewind
67
+ new_object = JSON.parse request.body.read
68
+
69
+ data = load_data
70
+ # How unsafe is this?
71
+ new_object['id'] = data.map { |e| e['id'] }.max + 1
72
+ data << new_object
73
+ write_data data
74
+
75
+ [ 201, { 'Content-type' => 'application/json' },
76
+ JSON.pretty_generate(new_object) ]
77
+ end
78
+
79
+ put '/dnc/:id' do
80
+ request.body.rewind
81
+ new_object = JSON.parse request.body.read
82
+
83
+ data = load_data
84
+ new_data = data.reject { |e| e['id'] == params[:id].to_i }
85
+ if new_data.size == data.size
86
+ 404
87
+ else
88
+ new_object['id'] = params[:id].to_i
89
+ new_data << new_object
90
+ write_data new_data
91
+
92
+ [ 200, { 'Content-type' => 'application/json' },
93
+ JSON.pretty_generate(new_object) ]
94
+ end
95
+ end
96
+
97
+ delete '/dnc/:id' do
98
+ data = load_data
99
+ new_data = data.reject { |e| e['id'] == params[:id].to_i }
100
+
101
+ if new_data.size == data.size
102
+ 404
103
+ else
104
+ write_data new_data
105
+ 204
106
+ end
107
+ end
108
+
109
+ get '/alt/dnc.json' do
110
+ redirect to('/dnc.json')
111
+ end
112
+
113
+ get '/auth/dnc.json' do
114
+ require_auth
115
+ redirect to('/dnc.json')
116
+ end
117
+
118
+ run! if app_file = $0
119
+
120
+ end
data/fixture/identity ADDED
@@ -0,0 +1,5 @@
1
+ { "id": "Authorization+Required=http://localhost:8787/",
2
+ "realm": "Authorization Required",
3
+ "domain": "http://localhost:8787/",
4
+ "username": "testuser",
5
+ "password": "testpass" }
@@ -0,0 +1,22 @@
1
+ { "$doctype": "noms-v2",
2
+ "$comment": [
3
+ "noms2 http://localhost:8787/dnc.json",
4
+ "noms2 http://localhost:8787/dnc.json list"
5
+ ],
6
+ "$script": [
7
+ { "$source": "https://rawgit.com/jfd/optparse-js/v1.0.5/lib/optparse.js",
8
+ "$comment": "Optparse.js 1.0.3 - https://github.com/jfd/optparse-js; via rawgit.com" },
9
+ { "$source": "https://rawgit.com/douglascrockford/JSON-js/master/json2.js",
10
+ "$comment": "JSON in JavaScript - https://github.com/douglascrockford/JSON-js" },
11
+ { "$source": "lib/noms-args.js" },
12
+ { "$source": "lib/dnc.js" }
13
+ ],
14
+ "$body": [
15
+ "Usage:",
16
+ " noms dnc query <field>=<value>",
17
+ " noms dnc add <field>=<value> [<field>=<value> [...]]",
18
+ " noms dnc remove <id>",
19
+ " noms dnc check { <phone> | <name> }",
20
+ " noms dnc list"
21
+ ]
22
+ }
@@ -0,0 +1,7 @@
1
+ { "$doctype": "noms-v2",
2
+ "$comment": "noms2 http://localhost:8787/echo.json one two three",
3
+ "$body": [],
4
+ "$script": [
5
+ "if (document.argv.length > 1) { document.body = document.argv.slice(1).join(\" \"); } else { line1 = prompt('String to echo: '); line2 = prompt(\"Password to echo (don't use real one): \", false); document.body = [line1, line2]; }"
6
+ ]
7
+ }
@@ -0,0 +1,12 @@
1
+ [
2
+ {"id":1,"name":"Manuela Irwin","street":"427 Maple Ln","city":"Arlington, TX 76010","phone":"(817) 555-0427"},
3
+ {"id":2,"name":"Ronda Sheppard","street":"801 New First Rd","city":"Providence, RI 02940","phone":"(401) 555-0801"},
4
+ {"id":3,"name":"Leonor Foreman","street":"428 Willow Rd","city":"Providence, RI 02940","phone":"(401) 555-0428"},
5
+ {"id":4,"name":"Emma Roman","street":"589 Flanty Terr","city":"Anderson, IN 46018","phone":"(317) 555-0589"},
6
+ {"id":5,"name":"Frieda English","street":"930 Stonehedge Blvd","city":"Chicago, IL 60607","phone":"(312) 555-0930"},
7
+ {"id":6,"name":"Kitty Morton","street":"618 Manchester St","city":"Richmond, VA 23232","phone":"(804) 555-0618"},
8
+ {"id":7,"name":"Kathy Mcleod","street":"52 Wommert Ln","city":"Binghamton, NY 13902","phone":"(607) 555-0052"},
9
+ {"id":8,"name":"Bettie Wolfe","street":"523 Sharon Rd","city":"Coward, SC 29530","phone":"(843) 555-0523"},
10
+ {"id":9,"name":"Vanessa Conway","street":"885 Old Pinbrick Dr","city":"Athens, GA 30601","phone":"(404) 555-0885"},
11
+ {"id":10,"name":"Ian Welch","street":"555 Hamlet St","city":"Arlington, TX 76010","phone":"(817) 555-0555"}
12
+ ]
@@ -0,0 +1,2 @@
1
+ { "$doctype": "noms-v2",
2
+ "$body": ["Test output for foo.json"] }
@@ -0,0 +1,81 @@
1
+ if (document.argv.length > 1) {
2
+ var argv = document.argv;
3
+ var me = argv.shift();
4
+ var command;
5
+ var format;
6
+ var xmlhttp = new XMLHttpRequest();
7
+
8
+ // document attributes can be set
9
+ // but are immutable
10
+ document.body = [ ];
11
+ var output = [ ];
12
+
13
+ var optspec = [
14
+ ["-J", "--json", "Display JSON"],
15
+ ["-Y", "--yaml", "Display YAML"],
16
+ ["-C", "--csv", "Display CSV"],
17
+ ["-v", "--verbose", "Enable verbose output"],
18
+ ["--nofeedback", "Don't print feedback"]
19
+ ];
20
+
21
+ var parser = new optparse.OptionParser(optspec);
22
+ var options = {
23
+ "feedback": true,
24
+ "format": "default",
25
+ "verbose": false
26
+ };
27
+ var args = [ ];
28
+
29
+ parser.on("verbose", function() { options["verbose"] = true });
30
+ parser.on("json", function() {
31
+ options["format"] = "json";
32
+ options["feedback"] = false;
33
+ });
34
+ parser.on("yaml", function() {
35
+ options["format"] = "yaml";
36
+ options["feedback"] = false;
37
+ });
38
+ parser.on("csv", function() {
39
+ options["format"] = "csv";
40
+ options["feedback"] = false;
41
+ });
42
+ parser.on("nofeedback", function() { options["feedback"] = false; });
43
+ parser.on(0, function(arg) { command = arg });
44
+ parser.on(function(arg) { args.push(arg); });
45
+
46
+ parser.parse(argv);
47
+
48
+ switch(command) {
49
+ case "list":
50
+ if (options["format"] === "default") {
51
+ format = "lines";
52
+ } else {
53
+ format = options["format"];
54
+ }
55
+ xmlhttp.open("GET", "/dnc", false);
56
+ xmlhttp.send();
57
+ var records = eval('(' + xmlhttp.responseText + ')');
58
+ output.push(
59
+ {
60
+ '$type': 'object-list',
61
+ '$format': format,
62
+ '$columns': [
63
+ { 'field': 'id', 'width': 3, 'align': 'right' },
64
+ { 'field': 'name', 'width': 20 },
65
+ { 'field': 'phone', 'width': 20 }
66
+ ],
67
+ '$data': records
68
+ });
69
+ if (options["feedback"]) {
70
+ output.push(records.length + " objects");
71
+ }
72
+ break;
73
+ default:
74
+ document.exitcode = 8;
75
+ window.alert(
76
+ me + " error: Unknown command '" + command + "'"
77
+ );
78
+ }
79
+
80
+ document.body = output;
81
+ }
@@ -0,0 +1,13 @@
1
+ var nomsargs = { };
2
+
3
+ (function (self) {
4
+ function kwargs() {
5
+ var result = { };
6
+ for (i = 0; i < arguments.length; i++) {
7
+ pair = arguments[i].split("=", 2);
8
+ result[pair[0]] = pair[1];
9
+ }
10
+ return result;
11
+ }
12
+
13
+ })(nomsargs);
@@ -0,0 +1,18 @@
1
+ var optspec = [
2
+ ["-n", "--dry-run", "Don't change anything"],
3
+ ["-d", "--debug", "Enable debugging output"],
4
+ ["-c", "--config FILE", "Specify configuration file"]
5
+ ];
6
+
7
+ var parser = new optparse.OptionParser(optspec);
8
+ var options = { "dry-run": false,
9
+ "debug": false,
10
+ "config": null }
11
+
12
+ parser.on("dry-run", function () { options["dry-run"] = true })
13
+ parser.on("debug", function() { options["debug"] = true })
14
+ parser.on("config", function(opt, file) { options["config"] = file })
15
+
16
+ parser.parse(document.argv)
17
+
18
+ document.body = options
@@ -0,0 +1,8 @@
1
+ { "$doctype": "noms-v2",
2
+ "$script": ["var out = { };",
3
+ "out.protocol = location.protocol;",
4
+ "out.host = location.host;",
5
+ "out.href = location.href;",
6
+ "document.body = out;"
7
+ ],
8
+ "$body": [] }
@@ -0,0 +1,15 @@
1
+ { "$doctype": "noms-v2",
2
+ "$comment": [
3
+ "noms2 http://localhost:8787/showopt.json",
4
+ "noms2 http://localhost:8787/showopt.json --debug --config /dev/null"
5
+ ],
6
+ "$body": [
7
+ "Usage:",
8
+ " showopt [--dry-run] [--config <s>] [--verbose]"
9
+ ],
10
+ "$script": [
11
+ { "$source": "https://rawgit.com/jfd/optparse-js/v1.0.5/lib/optparse.js",
12
+ "$comment": "Optparse.js 1.0.3 - https://github.com/jfd/optparse-js; via rawgit.com" },
13
+ { "$source": "lib/showopt.js" }
14
+ ]
15
+ }
data/fixture/rig2json ADDED
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+
3
+ echo '['
4
+ id=0
5
+
6
+ while read name
7
+ do
8
+ let id=id+1
9
+ read street
10
+ read city
11
+ read phone
12
+ number=$(echo $street | cut -f1 -d\ )
13
+ number=555-$(printf '%04d' $number)
14
+ phone=$(echo $phone | sed "s/xxx-xxxx/$number/")
15
+ echo -n "{'id':$id,'name':'$name','street':'$street','city':'$city','phone':'$phone'}" | tr "'" '"'
16
+ read blank || break
17
+ echo ,
18
+ done
19
+
20
+ echo
21
+ echo ']'