fis 0.1.1
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.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.envrc.example +1 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.rubocop.yml +86 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +159 -0
- data/LICENSE.txt +20 -0
- data/README.md +214 -0
- data/Rakefile +18 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/cucumber.yml +7 -0
- data/exe/fis +18 -0
- data/fis.gemspec +40 -0
- data/lib/fis.rb +55 -0
- data/lib/fis/auth.rb +12 -0
- data/lib/fis/auth/client.rb +44 -0
- data/lib/fis/auth/runner.rb +21 -0
- data/lib/fis/auth/server.rb +35 -0
- data/lib/fis/cli.rb +29 -0
- data/lib/fis/cli/auth.rb +53 -0
- data/lib/fis/cli/commands/login.rb +57 -0
- data/lib/fis/cli/commands/logout.rb +21 -0
- data/lib/fis/cli/commands/whoami.rb +35 -0
- data/lib/fis/client/base.rb +50 -0
- data/lib/fis/client/user.rb +15 -0
- data/lib/fis/command.rb +120 -0
- data/lib/fis/configuration.rb +88 -0
- data/lib/fis/ui.rb +57 -0
- data/lib/fis/version.rb +5 -0
- metadata +221 -0
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aruba/platform'
|
4
|
+
require 'bundler/gem_tasks'
|
5
|
+
require 'cucumber/rake/task'
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
9
|
+
|
10
|
+
Cucumber::Rake::Task.new do |t|
|
11
|
+
t.profile = 'default'
|
12
|
+
end
|
13
|
+
|
14
|
+
Cucumber::Rake::Task.new('cucumber:wip', 'Run Cucumber features which are "WIP" and are allowed to fail') do |t|
|
15
|
+
t.profile = 'wip'
|
16
|
+
end
|
17
|
+
|
18
|
+
task default: :spec
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'fis'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/cucumber.yml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
<%
|
2
|
+
std_opts = "--format pretty --color --exclude features/fixtures --require features --tags 'not @unsupported-on' --publish-quiet"
|
3
|
+
ignore_opts = "--tags 'not @ignore'"
|
4
|
+
%>
|
5
|
+
|
6
|
+
default: <%= std_opts %> --tags 'not @wip' <%= ignore_opts %>
|
7
|
+
wip: <%= std_opts %> --wip --tags @wip:5 <%= ignore_opts %>
|
data/exe/fis
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib_path = File.expand_path('../lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
6
|
+
require 'fis'
|
7
|
+
|
8
|
+
Signal.trap('INT') do
|
9
|
+
warn("\n#{caller.join("\n")}: interrupted")
|
10
|
+
exit(1)
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
FIS::CLI::Base.start
|
15
|
+
rescue FIS::CLI::Base::Error => e
|
16
|
+
puts "ERROR: #{e.message}"
|
17
|
+
exit 1
|
18
|
+
end
|
data/fis.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/fis/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'fis'
|
7
|
+
spec.license = 'MIT'
|
8
|
+
spec.version = FIS::VERSION
|
9
|
+
spec.authors = ['Tom Milewski']
|
10
|
+
spec.email = ['tmilewski@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'The command line interface to Flatiron School services.'
|
13
|
+
spec.description = spec.summary
|
14
|
+
spec.homepage = 'https://portal.flatironschool.com'
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
|
16
|
+
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/tmilewski/fis'
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/tmilewski/fis/releases'
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.bindir = 'exe'
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ['lib']
|
29
|
+
|
30
|
+
spec.add_runtime_dependency 'faraday', '~> 1.1.0'
|
31
|
+
spec.add_runtime_dependency 'faraday_middleware', '~> 1.0.0'
|
32
|
+
spec.add_runtime_dependency 'oauth2', '~> 1.4.4'
|
33
|
+
spec.add_runtime_dependency 'sinatra', '~> 2.1.0'
|
34
|
+
spec.add_runtime_dependency 'thin', '~> 1.7.2'
|
35
|
+
spec.add_runtime_dependency 'thor', '~> 1.0'
|
36
|
+
spec.add_runtime_dependency 'tty-config', '~> 0.4'
|
37
|
+
spec.add_runtime_dependency 'tty-link', '~> 0.1'
|
38
|
+
spec.add_runtime_dependency 'tty-prompt', '~> 0.22'
|
39
|
+
spec.add_runtime_dependency 'zeitwerk', '~> 2.4.0'
|
40
|
+
end
|
data/lib/fis.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ENV['THOR_SILENCE_DEPRECATION'] = 'true'
|
4
|
+
|
5
|
+
require 'faraday'
|
6
|
+
require 'faraday_middleware'
|
7
|
+
require 'learn_web'
|
8
|
+
require 'logger'
|
9
|
+
require 'oauth2'
|
10
|
+
require 'sinatra/base'
|
11
|
+
require 'thin'
|
12
|
+
|
13
|
+
# TTY Libraries
|
14
|
+
require 'tty-link'
|
15
|
+
require 'tty-prompt'
|
16
|
+
require 'tty-config'
|
17
|
+
require 'zeitwerk'
|
18
|
+
|
19
|
+
loader = Zeitwerk::Loader.for_gem
|
20
|
+
loader.log! if ENV['DEBUG']
|
21
|
+
loader.inflector.inflect 'fis' => 'FIS'
|
22
|
+
loader.inflector.inflect 'cli' => 'CLI'
|
23
|
+
loader.inflector.inflect 'ui' => 'UI'
|
24
|
+
loader.setup
|
25
|
+
|
26
|
+
# Entry point for the FIS CLI
|
27
|
+
module FIS
|
28
|
+
class Error < StandardError; end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
attr_writer :ui
|
32
|
+
|
33
|
+
def ui
|
34
|
+
@ui ||= UI.new(TTY::Prompt.new)
|
35
|
+
end
|
36
|
+
|
37
|
+
def config
|
38
|
+
@config ||= Configuration.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def client
|
42
|
+
@client ||= begin
|
43
|
+
token = config.fetch(:identity, :portal, :token)
|
44
|
+
|
45
|
+
if token.nil? || token.empty?
|
46
|
+
FIS.ui.newline
|
47
|
+
FIS.ui.error("You're not logged in. Please run `fis auth login` to continue.")
|
48
|
+
exit
|
49
|
+
end
|
50
|
+
|
51
|
+
Client::Base.new(token: token)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/fis/auth.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FIS
|
4
|
+
module Auth
|
5
|
+
DEFAULT_CLIENT_ID = 'b04559b9a82e2077088af40009d02053b74ce06af361b171e04ee853cdf3c89a'
|
6
|
+
|
7
|
+
CLIENT_ID = ENV.fetch('FIS_OAUTH2_CLIENT_ID', DEFAULT_CLIENT_ID)
|
8
|
+
LOCAL_URI = ENV.fetch('FIS_OAUTH2_LOCAL_URI', 'http://localhost:4444')
|
9
|
+
PROVIDER_URL = ENV.fetch('FIS_OAUTH2_PROVIDER_URL', 'http://localhost:3000')
|
10
|
+
REDIRECT_URI = ENV.fetch('FIS_OAUTH2_REDIRECT_URI', "#{LOCAL_URI}/callback")
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# PKCE modifies the standard authorization code grant.
|
4
|
+
# https://doorkeeper.gitbook.io/guides/ruby-on-rails/pkce-flow
|
5
|
+
|
6
|
+
module FIS
|
7
|
+
module Auth
|
8
|
+
# OAuth2 + PKCE helper class for authenticating against portal.flatironschool.com
|
9
|
+
class Client
|
10
|
+
CODE_CHALLENGE_METHOD = 'S256'
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@client = OAuth2::Client.new(
|
14
|
+
Auth::CLIENT_ID,
|
15
|
+
nil,
|
16
|
+
site: Auth::PROVIDER_URL,
|
17
|
+
redirect_uri: Auth::REDIRECT_URI
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def authorize_url
|
22
|
+
@client.auth_code.authorize_url(
|
23
|
+
code_challenge: code_challenge,
|
24
|
+
code_challenge_method: CODE_CHALLENGE_METHOD
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_token(code:)
|
29
|
+
@client.auth_code.get_token(code, code_verifier: code_verifier)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def code_challenge
|
35
|
+
padded_result = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier))
|
36
|
+
padded_result.split('=')[0] # Remove trailing '='
|
37
|
+
end
|
38
|
+
|
39
|
+
def code_verifier
|
40
|
+
@code_verifier ||= SecureRandom.hex(128)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FIS
|
4
|
+
module Auth
|
5
|
+
# Manages the local Sinatra/Thin server in a forked process
|
6
|
+
class Runner
|
7
|
+
def start
|
8
|
+
@pid = fork do
|
9
|
+
Thin::Logging.silent = true
|
10
|
+
Server.start!
|
11
|
+
end
|
12
|
+
|
13
|
+
Process.detach(@pid)
|
14
|
+
end
|
15
|
+
|
16
|
+
def stop
|
17
|
+
Process.kill('SIGINT', @pid) if @pid
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra/base'
|
4
|
+
|
5
|
+
module FIS
|
6
|
+
module Auth
|
7
|
+
# Local Sinatra server to respond to OAuth2 code requests.
|
8
|
+
class Server < Sinatra::Base
|
9
|
+
enable :raise_errors, :quiet
|
10
|
+
disable :sessions, :logging, :show_exceptions
|
11
|
+
|
12
|
+
set :bind, '127.0.0.1'
|
13
|
+
set :port, 4444
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@auth = FIS::Auth::Client.new
|
17
|
+
super(nil)
|
18
|
+
end
|
19
|
+
|
20
|
+
get '/' do
|
21
|
+
redirect @auth.authorize_url
|
22
|
+
end
|
23
|
+
|
24
|
+
get '/callback' do
|
25
|
+
auth = @auth.get_token(code: params[:code])
|
26
|
+
|
27
|
+
FIS.config.set(:identity, :portal, :token) { auth.token }
|
28
|
+
|
29
|
+
'Success! You may close this window and return to the CLI.'
|
30
|
+
rescue # rubocop:disable Style/RescueStandardError
|
31
|
+
'Authentication failure. Please try again from the CLI'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/fis/cli.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
module FIS
|
6
|
+
module CLI
|
7
|
+
# Root for all Thor CLI-based commands
|
8
|
+
class Base < Thor
|
9
|
+
Error = Class.new(StandardError)
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
super
|
13
|
+
|
14
|
+
prompt = TTY::Prompt.new(enable_color: !options['no-color'])
|
15
|
+
FIS.ui = UI.new(prompt)
|
16
|
+
FIS.ui.debug! if options['verbose']
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'version', 'version information'
|
20
|
+
def version
|
21
|
+
puts "v#{FIS::VERSION}"
|
22
|
+
end
|
23
|
+
map %w[--version -v] => :version
|
24
|
+
|
25
|
+
desc 'auth', 'Leverage Authentication'
|
26
|
+
subcommand 'auth', FIS::CLI::Auth
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/fis/cli/auth.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FIS
|
4
|
+
module CLI
|
5
|
+
# Authentication-related Thor subcommands
|
6
|
+
class Auth < Thor
|
7
|
+
desc 'login', 'Command description...'
|
8
|
+
method_option(
|
9
|
+
:help,
|
10
|
+
aliases: '-h',
|
11
|
+
type: :boolean,
|
12
|
+
desc: 'Display usage information'
|
13
|
+
)
|
14
|
+
def login(*)
|
15
|
+
if options[:help]
|
16
|
+
invoke :help, ['login']
|
17
|
+
else
|
18
|
+
FIS::CLI::Commands::Login.new(options).execute
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
desc 'logout', 'Command description...'
|
23
|
+
method_option(
|
24
|
+
:help,
|
25
|
+
aliases: '-h',
|
26
|
+
type: :boolean,
|
27
|
+
desc: 'Display usage information'
|
28
|
+
)
|
29
|
+
def logout(*)
|
30
|
+
if options[:help]
|
31
|
+
invoke :help, ['logout']
|
32
|
+
else
|
33
|
+
FIS::CLI::Commands::Logout.new(options).execute
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
desc 'whoami', 'Command description...'
|
38
|
+
method_option(
|
39
|
+
:help,
|
40
|
+
aliases: '-h',
|
41
|
+
type: :boolean,
|
42
|
+
desc: 'Display usage information'
|
43
|
+
)
|
44
|
+
def whoami(*)
|
45
|
+
if options[:help]
|
46
|
+
invoke :help, ['whoami']
|
47
|
+
else
|
48
|
+
FIS::CLI::Commands::Whoami.new.execute
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FIS
|
4
|
+
module CLI
|
5
|
+
module Commands
|
6
|
+
# `auth login` - Requests and adds the user's authentication credentials
|
7
|
+
class Login < Command
|
8
|
+
def initialize(options)
|
9
|
+
@options = options
|
10
|
+
@auth_local_server = FIS::Auth::Runner.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute
|
14
|
+
# TODO: Allow for token to be set with a flag
|
15
|
+
|
16
|
+
FIS.ui.newline
|
17
|
+
|
18
|
+
return FIS.ui.error("You're already logged in. Please run `fis auth logout` to continue.") if logged_in?
|
19
|
+
return FIS.ui.warn('OAuth not available. Please set your token using the `--token` flag.') if remote_ide?
|
20
|
+
|
21
|
+
@auth_local_server.start
|
22
|
+
|
23
|
+
FIS.ui.info('Logging into your Flatiron School account...')
|
24
|
+
open_browser!
|
25
|
+
|
26
|
+
sleep 1 until token_accessible? # TODO: Timeout?
|
27
|
+
|
28
|
+
FIS.ui.newline
|
29
|
+
|
30
|
+
FIS.ui.ok("You've successfully logged in.")
|
31
|
+
ensure
|
32
|
+
@auth_local_server.stop
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def logged_in?
|
38
|
+
FIS.config.fetch(:identity, :portal, :token)
|
39
|
+
end
|
40
|
+
|
41
|
+
def open_browser!
|
42
|
+
system('open', FIS::Auth::LOCAL_URI)
|
43
|
+
end
|
44
|
+
|
45
|
+
def remote_ide?
|
46
|
+
false # TODO: Check if in an environment without a browser
|
47
|
+
end
|
48
|
+
|
49
|
+
def token_accessible?
|
50
|
+
FIS.config.reread
|
51
|
+
token = FIS.config.fetch(:identity, :portal, :token)
|
52
|
+
!token.nil? && !token.empty?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FIS
|
4
|
+
module CLI
|
5
|
+
module Commands
|
6
|
+
# `auth logout` - Removes the user's authentication credentials
|
7
|
+
class Logout < Command
|
8
|
+
def initialize(options)
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute
|
13
|
+
FIS.config.unset(:identity, :portal, :token)
|
14
|
+
|
15
|
+
FIS.ui.newline
|
16
|
+
FIS.ui.ok('You\'ve successfully logged out.')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|