osso 0.0.6 → 0.1.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +1 -0
  3. data/.rubocop.yml +4 -16
  4. data/Gemfile +3 -3
  5. data/Gemfile.lock +76 -70
  6. data/Rakefile +3 -0
  7. data/bin/console +3 -0
  8. data/db/schema.rb +2 -2
  9. data/lib/osso.rb +1 -0
  10. data/lib/osso/db/migrate/20201125143501_add_salesforce_to_provider_service_enum.rb +28 -0
  11. data/lib/osso/graphql/mutations/configure_identity_provider.rb +4 -1
  12. data/lib/osso/graphql/mutations/create_enterprise_account.rb +4 -1
  13. data/lib/osso/graphql/mutations/create_identity_provider.rb +8 -3
  14. data/lib/osso/graphql/mutations/create_oauth_client.rb +4 -1
  15. data/lib/osso/graphql/mutations/delete_enterprise_account.rb +4 -1
  16. data/lib/osso/graphql/mutations/delete_identity_provider.rb +4 -1
  17. data/lib/osso/graphql/mutations/delete_oauth_client.rb +4 -1
  18. data/lib/osso/graphql/mutations/invite_admin_user.rb +6 -0
  19. data/lib/osso/graphql/mutations/regenerate_oauth_credentials.rb +10 -1
  20. data/lib/osso/graphql/mutations/set_redirect_uris.rb +2 -0
  21. data/lib/osso/graphql/mutations/update_app_config.rb +4 -1
  22. data/lib/osso/graphql/query.rb +26 -31
  23. data/lib/osso/graphql/schema.rb +0 -1
  24. data/lib/osso/graphql/types/identity_provider_service.rb +1 -0
  25. data/lib/osso/lib/analytics.rb +55 -0
  26. data/lib/osso/lib/route_map.rb +2 -0
  27. data/lib/osso/models/account.rb +1 -1
  28. data/lib/osso/models/identity_provider.rb +3 -2
  29. data/lib/osso/routes/admin.rb +37 -5
  30. data/lib/osso/routes/auth.rb +2 -0
  31. data/lib/osso/routes/oauth.rb +10 -4
  32. data/lib/osso/version.rb +1 -1
  33. data/lib/tasks/bootstrap.rake +6 -4
  34. data/osso-rb.gemspec +5 -3
  35. data/spec/graphql/mutations/create_identity_provider_spec.rb +1 -1
  36. data/spec/models/identity_provider_spec.rb +1 -0
  37. data/spec/routes/admin_spec.rb +27 -9
  38. data/spec/routes/auth_spec.rb +5 -3
  39. data/spec/routes/oauth_spec.rb +20 -12
  40. data/spec/spec_helper.rb +2 -0
  41. data/spec/support/views/hosted_login.erb +1 -0
  42. data/spec/support/views/saml_login_form.erb +1 -0
  43. metadata +40 -9
  44. data/spec/routes/app_spec.rb +0 -6
@@ -14,7 +14,10 @@ module Osso
14
14
  def resolve(**args)
15
15
  oauth_client = Osso::Models::OauthClient.new(args)
16
16
 
17
- return response_data(oauth_client: oauth_client) if oauth_client.save
17
+ if oauth_client.save
18
+ Osso::Analytics.capture(email: context[:email], event: self.class.name.demodulize, properties: args)
19
+ return response_data(oauth_client: oauth_client)
20
+ end
18
21
 
19
22
  response_error(oauth_client.errors)
20
23
  end
@@ -18,7 +18,10 @@ module Osso
18
18
  def resolve(**args)
19
19
  customer = enterprise_account(**args)
20
20
 
21
- return response_data(enterprise_account: nil) if customer.destroy
21
+ if customer.destroy
22
+ Osso::Analytics.capture(email: context[:email], event: self.class.name.demodulize, properties: args)
23
+ return response_data(enterprise_account: nil)
24
+ end
22
25
 
23
26
  response_error(customer.errors)
24
27
  end
@@ -14,7 +14,10 @@ module Osso
14
14
  def resolve(id:)
15
15
  identity_provider = Osso::Models::IdentityProvider.find(id)
16
16
 
17
- return response_data(identity_provider: nil) if identity_provider.destroy
17
+ if identity_provider.destroy
18
+ Osso::Analytics.capture(email: context[:email], event: self.class.name.demodulize, properties: { id: id })
19
+ return response_data(identity_provider: nil)
20
+ end
18
21
 
19
22
  response_error(identity_provider.errors)
20
23
  end
@@ -14,7 +14,10 @@ module Osso
14
14
  def resolve(id:)
15
15
  oauth_client = Osso::Models::OauthClient.find(id)
16
16
 
17
- return response_data(oauth_client: nil) if oauth_client.destroy
17
+ if oauth_client.destroy
18
+ Osso::Analytics.capture(email: context[:email], event: self.class.name.demodulize, properties: { id: id })
19
+ return response_data(oauth_client: nil)
20
+ end
18
21
 
19
22
  response_error(oauth_client.errors)
20
23
  end
@@ -23,6 +23,12 @@ module Osso
23
23
  if admin_user.save
24
24
  verify_user(email)
25
25
 
26
+ Osso::Analytics.capture(email: context[:email], event: self.class.name.demodulize, properties: {
27
+ invited_email: email,
28
+ invited_role: role,
29
+ invited_oauth_client_id: oauth_client_id,
30
+ })
31
+
26
32
  return response_data(admin_user: admin_user)
27
33
  end
28
34
 
@@ -15,7 +15,16 @@ module Osso
15
15
  oauth_client = Osso::Models::OauthClient.find(id)
16
16
  oauth_client.regenerate_secrets!
17
17
 
18
- return response_data(oauth_client: oauth_client) if oauth_client.save
18
+ if oauth_client.save
19
+ Osso::Analytics.capture(
20
+ email: context[:email],
21
+ event: self.class.name.demodulize,
22
+ properties: {
23
+ oauth_client_id: id
24
+ }
25
+ )
26
+ return response_data(oauth_client: oauth_client)
27
+ end
19
28
 
20
29
  response_error(oauth_client.errors)
21
30
  end
@@ -18,6 +18,8 @@ module Osso
18
18
  update_existing(oauth_client, redirect_uris)
19
19
  create_new(oauth_client, redirect_uris)
20
20
 
21
+ Osso::Analytics.capture(email: context[:email], event: self.class.name.demodulize, properties: redirect_uris)
22
+
21
23
  response_data(oauth_client: oauth_client.reload)
22
24
  rescue StandardError => e
23
25
  response_error(e)
@@ -15,7 +15,10 @@ module Osso
15
15
 
16
16
  def resolve(**args)
17
17
  app_config = Osso::Models::AppConfig.find
18
- return response_data(app_config: app_config) if app_config.update(**args)
18
+ if app_config.update(**args)
19
+ Osso::Analytics.capture(email: context[:email], event: self.class.name.demodulize, properties: args)
20
+ return response_data(app_config: app_config)
21
+ end
19
22
 
20
23
  response_error(app_config.errors)
21
24
  end
@@ -16,44 +16,39 @@ module Osso
16
16
 
17
17
  field :oauth_clients, null: true, resolver: Resolvers::OAuthClients
18
18
 
19
- field(
20
- :identity_provider,
21
- Types::IdentityProvider,
22
- null: true,
23
- resolve: ->(_obj, args, _context) { Osso::Models::IdentityProvider.find(args[:id]) },
24
- ) do
19
+ field :admin_users, [Types::AdminUser], null: false
20
+
21
+ field :app_config, Types::AppConfig, null: false
22
+
23
+ field :current_user, Types::AdminUser, null: false
24
+
25
+ field :identity_provider, Types::IdentityProvider, null: true do
25
26
  argument :id, ID, required: true
26
27
  end
27
28
 
28
- field(
29
- :app_config,
30
- Types::AppConfig,
31
- null: false,
32
- resolve: ->(_obj, _args, _context) { Osso::Models::AppConfig.find },
33
- )
34
-
35
- field(
36
- :oauth_client,
37
- Types::OauthClient,
38
- null: true,
39
- resolve: ->(_obj, args, _context) { Osso::Models::OauthClient.find(args[:id]) },
40
- ) do
29
+ field :oauth_client, Types::OauthClient, null: true do
41
30
  argument :id, ID, required: true
42
31
  end
43
32
 
44
- field(
45
- :admin_users,
46
- [Types::AdminUser],
47
- null: false,
48
- resolve: ->(_obj, _args, _context) { Osso::Models::Account.all },
49
- )
33
+ def admin_users
34
+ Osso::Models::Account.all
35
+ end
36
+
37
+ def app_config
38
+ Osso::Models::AppConfig.find
39
+ end
40
+
41
+ def current_user
42
+ context.to_h
43
+ end
50
44
 
51
- field(
52
- :current_user,
53
- Types::AdminUser,
54
- null: false,
55
- resolve: ->(_obj, _args, context) { context.to_h },
56
- )
45
+ def identity_provider(id:)
46
+ Osso::Models::IdentityProvider.find(id)
47
+ end
48
+
49
+ def oauth_client(id:)
50
+ Osso::Models::OauthClient.find(id)
51
+ end
57
52
  end
58
53
  end
59
54
  end
@@ -14,7 +14,6 @@ GraphQL::Relay::BaseConnection.register_connection_implementation(
14
14
  module Osso
15
15
  module GraphQL
16
16
  class Schema < ::GraphQL::Schema
17
- use ::GraphQL::Pagination::Connections
18
17
  query Types::QueryType
19
18
  mutation Types::MutationType
20
19
 
@@ -9,6 +9,7 @@ module Osso
9
9
  value('OKTA', 'Okta Identity Provider', value: 'OKTA')
10
10
  value('ONELOGIN', 'OneLogin Identity Provider', value: 'ONELOGIN')
11
11
  value('PING', 'PingID Identity Provider', value: 'PING')
12
+ value('SALESFORCE', 'Salesforce Identity Provider', value: 'SALESFORCE')
12
13
  end
13
14
  end
14
15
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'posthog-ruby'
4
+
5
+ module Osso
6
+ # Osso::Analytics provides an interface to track product analytics for any provider.
7
+ # Osso recommends PostHog as an open source solution for your product analytics needs.
8
+ # If you want to use another product analytics provider, you can patch the Osso::Analytics
9
+ # class yourself in your parent application. Be sure to implement the public
10
+ # .identify and .capture class methods with the required method signatures and require
11
+ # your class after requiring Osso.
12
+ class Analytics
13
+ class << self
14
+ def identify(email:, properties: {})
15
+ return unless configured?
16
+
17
+ client.identify({
18
+ distinct_id: email,
19
+ properties: properties.merge(instance_properties),
20
+ })
21
+ end
22
+
23
+ def capture(email:, event:, properties: {})
24
+ return unless configured?
25
+
26
+ client.capture(
27
+ distinct_id: email,
28
+ event: event,
29
+ properties: properties.merge(instance_properties),
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ def configured?
36
+ ENV['POSTHOG_API_KEY'].present?
37
+ end
38
+
39
+ def client
40
+ @client ||= PostHog::Client.new({
41
+ api_key: ENV['POSTHOG_API_KEY'],
42
+ api_host: ENV['POSTHOG_HOST'],
43
+ on_error: proc { |_status, msg| print msg },
44
+ })
45
+ end
46
+
47
+ def instance_properties
48
+ {
49
+ instance_url: ENV['BASE_URL'],
50
+ osso_plan: ENV['OSSO_PLAN'],
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rack/protection'
4
+
3
5
  module Osso
4
6
  module RouteMap
5
7
  def self.included(klass)
@@ -3,7 +3,7 @@
3
3
  module Osso
4
4
  module Models
5
5
  class Account < ::ActiveRecord::Base
6
- enum status_id: { 1 => :Unverified, 2 => :Verified, 3 => :Closed }
6
+ enum status_id: { Unverified: 1, Verified: 2, Closed: 3 }
7
7
 
8
8
  def context
9
9
  {
@@ -18,7 +18,7 @@ module Osso
18
18
 
19
19
  ENTITY_ID_URI_REQUIRED = [
20
20
  'PING',
21
- ]
21
+ ].freeze
22
22
 
23
23
  def name
24
24
  service.titlecase
@@ -30,6 +30,7 @@ module Osso
30
30
  idp_sso_target_url: sso_url,
31
31
  idp_cert: sso_cert,
32
32
  issuer: sso_issuer,
33
+ name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
33
34
  }
34
35
  end
35
36
 
@@ -55,7 +56,7 @@ module Osso
55
56
 
56
57
  def set_sso_issuer
57
58
  parts = [domain, oauth_client_id]
58
-
59
+
59
60
  parts.unshift('https:/') if ENTITY_ID_URI_REQUIRED.any?(service)
60
61
 
61
62
  self.sso_issuer = parts.join('/')
@@ -10,16 +10,45 @@ module Osso
10
10
  DB = Sequel.postgres(extensions: :activerecord_connection)
11
11
  use Rack::Session::Cookie, secret: ENV.fetch('SESSION_SECRET')
12
12
 
13
+ plugin :json
13
14
  plugin :middleware
14
15
  plugin :render, engine: 'erb', views: ENV['RODAUTH_VIEWS'] || DEFAULT_VIEWS_DIR
15
16
  plugin :route_csrf
16
17
 
17
18
  plugin :rodauth do
18
- enable :login, :verify_account
19
+ enable :login, :verify_account, :jwt
20
+
21
+ base_uri = URI.parse(ENV.fetch('BASE_URL'))
22
+ base_url base_uri
23
+ domain base_uri.host
24
+
25
+ jwt_secret ENV.fetch('SESSION_SECRET')
26
+ only_json? false
27
+
28
+ email_from { "Osso <no-reply@#{domain}>" }
19
29
  verify_account_set_password? true
20
- already_logged_in { redirect login_redirect }
21
30
  use_database_authentication_functions? false
22
31
 
32
+ after_login do
33
+ Osso::Analytics.identify(email: account[:email], properties: account)
34
+ end
35
+
36
+ verify_account_view do
37
+ render :admin
38
+ end
39
+
40
+ login_view do
41
+ render :admin
42
+ end
43
+
44
+ verify_account_email_subject do
45
+ DB[:accounts].one? ? 'Your Osso instance is ready' : 'You\'ve been invited to start using Osso'
46
+ end
47
+
48
+ verify_account_email_body do
49
+ DB[:accounts].one? ? render('verify-first-account-email') : render('verify-account-email')
50
+ end
51
+
23
52
  before_create_account_route do
24
53
  request.halt unless DB[:accounts].empty?
25
54
  end
@@ -31,13 +60,16 @@ module Osso
31
60
  r.rodauth
32
61
 
33
62
  def current_account
34
- Osso::Models::Account.find(rodauth.session['account_id']).
35
- context.
63
+ Osso::Models::Account.find(
64
+ rodauth.
65
+ session.
66
+ to_hash.
67
+ stringify_keys['account_id'],
68
+ ).context.
36
69
  merge({ rodauth: rodauth })
37
70
  end
38
71
 
39
72
  r.on 'admin' do
40
- rodauth.require_authentication
41
73
  erb :admin, layout: false
42
74
  end
43
75
 
@@ -14,6 +14,8 @@ module Osso
14
14
  /[0-9a-f]{8}-[0-9a-f]{3,4}-[0-9a-f]{4}-[0-9a-f]{3,4}-[0-9a-f]{12}/.
15
15
  freeze
16
16
 
17
+ use Rack::Protection, allow_if: ->(env) { Rack::Request.new(env)&.path&.end_with?('callback') }
18
+
17
19
  use OmniAuth::Builder do
18
20
  OmniAuth::MultiProvider.register(
19
21
  self,
@@ -16,13 +16,14 @@ module Osso
16
16
  # Once they complete IdP login, they will be returned to the
17
17
  # redirect_uri with an authorization code parameter.
18
18
  get '/authorize' do
19
- identity_providers = find_providers
20
-
21
19
  validate_oauth_request(env)
22
20
 
23
- redirect "/auth/saml/#{identity_providers.first.id}" if identity_providers.one?
21
+ return erb :hosted_login if render_hosted_login?
22
+
23
+ @providers = find_providers
24
+
25
+ return erb :saml_login_form if @providers.one?
24
26
 
25
- @providers = identity_providers.not_pending
26
27
  return erb :multiple_providers if @providers.count > 1
27
28
 
28
29
  raise Osso::Error::MissingConfiguredIdentityProvider.new(domain: params[:domain])
@@ -61,6 +62,10 @@ module Osso
61
62
 
62
63
  private
63
64
 
65
+ def render_hosted_login?
66
+ [params[:email], params[:domain]].all?(&:nil?)
67
+ end
68
+
64
69
  def find_providers
65
70
  if params[:email]
66
71
  user = Osso::Models::User.
@@ -71,6 +76,7 @@ module Osso
71
76
 
72
77
  Osso::Models::IdentityProvider.
73
78
  joins(:oauth_client).
79
+ not_pending.
74
80
  where(
75
81
  domain: domain_from_params,
76
82
  oauth_clients: { identifier: params[:client_id] },
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Osso
4
- VERSION = '0.0.6'
4
+ VERSION = '0.1.0'
5
5
  end
@@ -8,9 +8,11 @@ namespace :osso do
8
8
  desc 'Bootstrap Osso data for a deployment'
9
9
  task :bootstrap do
10
10
  %w[Production Staging Development].each do |environment|
11
+ next if Osso::Models::OauthClient.find_by_name(environment)
12
+
11
13
  Osso::Models::OauthClient.create!(
12
14
  name: environment,
13
- ) unless Osso::Models::OauthClient.find_by_name(environment)
15
+ )
14
16
  end
15
17
 
16
18
  Osso::Models::AppConfig.create
@@ -18,7 +20,7 @@ namespace :osso do
18
20
  admin_email = ENV['ADMIN_EMAIL']
19
21
 
20
22
  if admin_email
21
- admin = Osso::Models::Account.create(
23
+ Osso::Models::Account.create(
22
24
  email: admin_email,
23
25
  status_id: 1,
24
26
  role: 'admin',
@@ -29,10 +31,10 @@ namespace :osso do
29
31
  rodauth = Osso::Admin.rodauth.new(Osso::Admin.new({
30
32
  'HTTP_HOST' => base_uri.host,
31
33
  'SERVER_NAME' => base_uri.to_s,
32
- 'rack.url_scheme' => base_uri.scheme
34
+ 'rack.url_scheme' => base_uri.scheme,
33
35
  }))
34
36
 
35
- account = rodauth.account_from_login(admin_email)
37
+ rodauth.account_from_login(admin_email)
36
38
  rodauth.setup_account_verification
37
39
  end
38
40
  end