osso 0.0.8 → 0.1.2

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +1 -0
  3. data/.rubocop.yml +4 -16
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +60 -55
  6. data/Rakefile +1 -0
  7. data/bin/console +3 -0
  8. data/db/schema.rb +4 -4
  9. data/lib/osso.rb +1 -0
  10. data/lib/osso/db/migrate/20210201220556_add_generic_saml_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 -3
  29. data/lib/osso/routes/admin.rb +47 -5
  30. data/lib/osso/routes/auth.rb +2 -0
  31. data/lib/osso/routes/oauth.rb +1 -1
  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 -1
  37. data/spec/routes/admin_spec.rb +54 -9
  38. data/spec/routes/auth_spec.rb +5 -3
  39. data/spec/routes/oauth_spec.rb +7 -13
  40. data/spec/spec_helper.rb +2 -0
  41. data/spec/support/views/saml_login_form.erb +1 -0
  42. metadata +39 -15
  43. data/spec/routes/app_spec.rb +0 -6
@@ -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
 
@@ -8,6 +8,7 @@ module Osso
8
8
  value('GOOGLE', 'Google SAML Identity Provider', value: 'GOOGLE')
9
9
  value('OKTA', 'Okta Identity Provider', value: 'OKTA')
10
10
  value('ONELOGIN', 'OneLogin Identity Provider', value: 'ONELOGIN')
11
+ value('OTHER', 'Generic SAML Identity Provider', value: 'OTHER')
11
12
  value('PING', 'PingID Identity Provider', value: 'PING')
12
13
  value('SALESFORCE', 'Salesforce Identity Provider', value: 'SALESFORCE')
13
14
  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,7 +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
+ name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
34
34
  }
35
35
  end
36
36
 
@@ -56,7 +56,7 @@ module Osso
56
56
 
57
57
  def set_sso_issuer
58
58
  parts = [domain, oauth_client_id]
59
-
59
+
60
60
  parts.unshift('https:/') if ENTITY_ID_URI_REQUIRED.any?(service)
61
61
 
62
62
  self.sso_issuer = parts.join('/')
@@ -10,16 +10,46 @@ 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
14
+ plugin :json_parser
13
15
  plugin :middleware
14
16
  plugin :render, engine: 'erb', views: ENV['RODAUTH_VIEWS'] || DEFAULT_VIEWS_DIR
15
17
  plugin :route_csrf
16
18
 
17
19
  plugin :rodauth do
18
- enable :login, :verify_account
20
+ enable :login, :verify_account, :jwt
21
+
22
+ base_uri = URI.parse(ENV.fetch('BASE_URL'))
23
+ base_url base_uri
24
+ domain base_uri.host
25
+
26
+ jwt_secret ENV.fetch('SESSION_SECRET')
27
+ only_json? false
28
+
29
+ email_from { "Osso <no-reply@#{domain}>" }
19
30
  verify_account_set_password? true
20
- already_logged_in { redirect login_redirect }
21
31
  use_database_authentication_functions? false
22
32
 
33
+ after_login do
34
+ Osso::Analytics.identify(email: account[:email], properties: account)
35
+ end
36
+
37
+ verify_account_view do
38
+ render :admin
39
+ end
40
+
41
+ login_view do
42
+ render :admin
43
+ end
44
+
45
+ verify_account_email_subject do
46
+ DB[:accounts].one? ? 'Your Osso instance is ready' : 'You\'ve been invited to start using Osso'
47
+ end
48
+
49
+ verify_account_email_body do
50
+ DB[:accounts].one? ? render('verify-first-account-email') : render('verify-account-email')
51
+ end
52
+
23
53
  before_create_account_route do
24
54
  request.halt unless DB[:accounts].empty?
25
55
  end
@@ -31,16 +61,28 @@ module Osso
31
61
  r.rodauth
32
62
 
33
63
  def current_account
34
- Osso::Models::Account.find(rodauth.session['account_id']).
35
- context.
64
+ Osso::Models::Account.find(
65
+ rodauth.
66
+ session.
67
+ to_hash.
68
+ stringify_keys['account_id'],
69
+ ).context.
36
70
  merge({ rodauth: rodauth })
37
71
  end
38
72
 
39
73
  r.on 'admin' do
40
- rodauth.require_authentication
41
74
  erb :admin, layout: false
42
75
  end
43
76
 
77
+ r.post 'idp' do
78
+ onboarded = Osso::Models::IdentityProvider.
79
+ not_pending.
80
+ where(domain: r.params['domain']).
81
+ exists?
82
+
83
+ { onboarded: onboarded }.to_json
84
+ end
85
+
44
86
  r.post 'graphql' do
45
87
  rodauth.require_authentication
46
88
 
@@ -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,
@@ -22,7 +22,7 @@ module Osso
22
22
 
23
23
  @providers = find_providers
24
24
 
25
- redirect "/auth/saml/#{@providers.first.id}" if @providers.one?
25
+ return erb :saml_login_form if @providers.one?
26
26
 
27
27
  return erb :multiple_providers if @providers.count > 1
28
28
 
data/lib/osso/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Osso
4
- VERSION = '0.0.8'
4
+ VERSION = '0.1.2'
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
data/osso-rb.gemspec CHANGED
@@ -22,15 +22,17 @@ Gem::Specification.new do |spec|
22
22
  spec.add_runtime_dependency 'mail', '~> 2.7.1'
23
23
  spec.add_runtime_dependency 'omniauth-multi-provider'
24
24
  spec.add_runtime_dependency 'omniauth-saml'
25
+ spec.add_runtime_dependency 'posthog-ruby'
25
26
  spec.add_runtime_dependency 'rack', '>= 2.1.4'
26
27
  spec.add_runtime_dependency 'rack-contrib'
27
28
  spec.add_runtime_dependency 'rack-oauth2'
29
+ spec.add_runtime_dependency 'rack-protection', '~> 2.1.0'
28
30
  spec.add_runtime_dependency 'rake'
29
- spec.add_runtime_dependency 'rodauth', '~> 2.6.0'
30
- spec.add_runtime_dependency 'sequel', '>= 5.37', '< 5.40'
31
+ spec.add_runtime_dependency 'rodauth', '~> 2.9'
32
+ spec.add_runtime_dependency 'sequel', '~> 5.40'
31
33
  spec.add_runtime_dependency 'sequel-activerecord_connection', '>= 0.3', '< 2.0'
32
34
  spec.add_runtime_dependency 'sinatra'
33
- spec.add_runtime_dependency 'sinatra-activerecord'
35
+ spec.add_runtime_dependency 'sinatra-activerecord', '>= 2.0.22'
34
36
  spec.add_runtime_dependency 'sinatra-contrib'
35
37
 
36
38
  spec.add_development_dependency 'annotate', '~> 3.1'