fis 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|