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.
@@ -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
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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