fis 0.1.1

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