atomic_admin 1.1.1 → 2.0.0.beta.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/atomic_admin/api/admin/v1/application_instances_controller.rb +3 -0
  3. data/app/controllers/atomic_admin/api/admin/v1/applications_controller.rb +3 -0
  4. data/app/controllers/atomic_admin/api/admin/v1/lti_installs_controller.rb +3 -0
  5. data/app/controllers/atomic_admin/api/admin/v1/lti_platforms_controller.rb +3 -0
  6. data/app/controllers/atomic_admin/api/admin/v1/sites_controller.rb +3 -0
  7. data/app/controllers/atomic_admin/api/admin/v1/tenant_client_id_strategies_controller.rb +3 -0
  8. data/app/controllers/atomic_admin/api/admin/v1/tenant_deployments_controller.rb +3 -0
  9. data/app/controllers/atomic_admin/api/admin/v1/tenant_platform_guid_strategies_controller.rb +3 -0
  10. data/app/controllers/atomic_admin/v1/admin_controller.rb +49 -0
  11. data/app/controllers/atomic_admin/v1/application_instances_controller.rb +127 -0
  12. data/app/controllers/atomic_admin/v1/applications_controller.rb +49 -0
  13. data/app/controllers/atomic_admin/{atomic_lti_install_controller.rb → v1/lti_installs_controller.rb} +18 -12
  14. data/app/controllers/atomic_admin/v1/lti_platforms_controller.rb +50 -0
  15. data/app/controllers/atomic_admin/v1/sites_controller.rb +46 -0
  16. data/app/controllers/atomic_admin/v1/tenant_client_id_strategies_controller.rb +48 -0
  17. data/app/controllers/atomic_admin/v1/tenant_deployments_controller.rb +47 -0
  18. data/app/controllers/atomic_admin/v1/tenant_platform_guid_strategies_controller.rb +63 -0
  19. data/app/controllers/concerns/filtering.rb +54 -0
  20. data/app/controllers/concerns/require_jwt_token.rb +77 -0
  21. data/config/routes.rb +25 -9
  22. data/lib/atomic_admin/interaction.rb +48 -0
  23. data/lib/atomic_admin/jwt_token/jwks_decoder.rb +41 -0
  24. data/lib/atomic_admin/jwt_token/secret_decoder.rb +29 -0
  25. data/lib/atomic_admin/jwt_token.rb +5 -65
  26. data/lib/atomic_admin/schema/application_instance_configuration_schema.rb +61 -0
  27. data/lib/atomic_admin/schema/application_instance_create_schema.rb +77 -0
  28. data/lib/atomic_admin/schema/application_instance_general_settings_schema.rb +156 -0
  29. data/lib/atomic_admin/schema/application_instance_license_details_schema.rb +99 -0
  30. data/lib/atomic_admin/schema/application_instance_schema.rb +18 -0
  31. data/lib/atomic_admin/schema/application_instance_trial_details_schema.rb +67 -0
  32. data/lib/atomic_admin/schema/application_instance_xml_config_schema.rb +66 -0
  33. data/lib/atomic_admin/schema/atomic_application_update_schema.rb +57 -0
  34. data/lib/atomic_admin/schema.rb +13 -0
  35. data/lib/atomic_admin/version.rb +1 -1
  36. data/lib/atomic_admin.rb +10 -6
  37. metadata +35 -10
  38. data/app/controllers/atomic_admin/application_controller.rb +0 -10
  39. data/app/controllers/atomic_admin/atomic_lti_platform_controller.rb +0 -43
  40. data/app/controllers/atomic_admin/atomic_tenant_client_id_strategy_controller.rb +0 -59
  41. data/app/controllers/atomic_admin/atomic_tenant_deployment_controller.rb +0 -79
  42. data/app/controllers/atomic_admin/atomic_tenant_platform_guid_strategy_controller.rb +0 -58
@@ -0,0 +1,77 @@
1
+ module RequireJwtToken
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ attr_accessor :auth_source
6
+
7
+ before_action :validate_admin_token
8
+ before_action :validate_internal_token
9
+ end
10
+
11
+ def is_atomic_admin?
12
+ self.auth_source == :atomic_admin
13
+ end
14
+
15
+ def is_internal?
16
+ self.auth_source == :internal
17
+ end
18
+
19
+ protected
20
+
21
+ def validate_admin_token
22
+ encoded_token = get_encoded_token(request)
23
+ decoder = AtomicAdmin::JwtToken::JwksDecoder.new(AtomicAdmin.admin_jwks_url)
24
+ token = decoder.decode(encoded_token)&.first
25
+ validate_claims!(token)
26
+ self.auth_source = :atomic_admin
27
+
28
+ token
29
+ rescue Exception => e
30
+ # Capture all exceptions to let the internal token validation handle it
31
+ Rails.logger.error "Admin JWT Error occured #{e.inspect}"
32
+ nil
33
+ end
34
+
35
+ def validate_internal_token
36
+ return if is_atomic_admin?
37
+
38
+ encoded_token = get_encoded_token(request)
39
+ decoder = AtomicAdmin::JwtToken::SecretDecoder.new(AtomicAdmin.internal_secret)
40
+ token = decoder.decode!(encoded_token)
41
+ validate_claims!(token)
42
+
43
+ current_application_instance_id = request.env['atomic.validated.application_instance_id']
44
+ if current_application_instance_id && current_application_instance_id != token["application_instance_id"]
45
+ raise AtomicAdmin::JwtToken::InvalidTokenError, "Invalid application instance id"
46
+ end
47
+
48
+ @user_tenant = token["user_tenant"] if token["user_tenant"].present?
49
+ @user = User.find(token["user_id"])
50
+
51
+ sign_in(@user, event: :authentication, store: false)
52
+ self.auth_source = :internal
53
+ rescue JWT::DecodeError, AtomicAdmin::JwtToken::InvalidTokenError => e
54
+ Rails.logger.error "Internal JWT Error occured #{e.inspect}"
55
+ render json: { error: "Unauthorized: Invalid token." }, status: :unauthorized
56
+ end
57
+
58
+ private
59
+
60
+ def get_encoded_token(req)
61
+ return req.params[:jwt] if req.params[:jwt]
62
+
63
+ header = req.headers["Authorization"] || req.headers[:authorization]
64
+ raise AtomicAdmin::JwtToken::MissingTokenError, "No authorization header found" if header.nil?
65
+
66
+ token = header.split(" ").last
67
+ raise AtomicAdmin::JwtToken::MissingTokenError, "Invalid authorization header string" if token.nil?
68
+
69
+ token
70
+ end
71
+
72
+ def validate_claims!(token)
73
+ if AtomicAdmin.audience != token["aud"]
74
+ raise AtomicAdmin::JwtToken::InvalidTokenError, "Expected audience to be #{AtomicAdmin.audience} but was #{token["aud"]}"
75
+ end
76
+ end
77
+ end
data/config/routes.rb CHANGED
@@ -1,13 +1,29 @@
1
1
  AtomicAdmin::Engine.routes.draw do
2
- # namespace :lti do
3
- resources :atomic_lti_platform
4
- resources :atomic_lti_install
5
- resources :atomic_tenant_deployment
6
- post '/atomic_tenant_deployment/search', to: 'atomic_tenant_deployment#search'
2
+ namespace :api do
3
+ namespace :admin do
4
+ namespace :v1 do
5
+ resources :lti_platforms
6
+ resources :lti_installs
7
+ resources :tenant_deployments
8
+ resources :sites
7
9
 
8
- resources :atomic_tenant_platform_guid_strategy
9
- post '/atomic_tenant_platform_guid_strategy/search', to: 'atomic_tenant_platform_guid_strategy#search'
10
- post '/atomic_tenant_client_id_strategy/search', to: 'atomic_tenant_client_id_strategy#search'
10
+ resources :applications do
11
+ member do
12
+ get :interactions
13
+ end
11
14
 
12
- resources :atomic_tenant_client_id_strategy
15
+ resources :application_instances do
16
+ member do
17
+ get :interactions
18
+ get :stats
19
+ end
20
+
21
+ resources :tenant_client_id_strategies
22
+ resources :tenant_platform_guid_strategies
23
+ resources :tenant_deployments
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
13
29
  end
@@ -0,0 +1,48 @@
1
+ module AtomicAdmin::Interaction
2
+ class Manager
3
+ def initialize
4
+ @interactions = {}
5
+ @curr_index = 0
6
+ end
7
+
8
+ def add(key, **kwargs)
9
+ @interactions[key] = {
10
+ **kwargs,
11
+ order: @curr_index,
12
+ }
13
+ @curr_index += 1
14
+ end
15
+
16
+ def get(key)
17
+ @interactions[key]
18
+ end
19
+
20
+ def tap
21
+ yield self
22
+ self
23
+ end
24
+
25
+ def resolve(**kwargs)
26
+ sorted = @interactions.sort_by { |key, interaction| interaction[:order] }
27
+ sorted.map do |key, interaction|
28
+ type = interaction[:type]
29
+ hash = {
30
+ key: key,
31
+ type: type,
32
+ title: interaction[:title],
33
+ icon: interaction[:icon],
34
+ }
35
+
36
+ case type
37
+ when :jsonform
38
+ schema_factory = interaction[:schema]
39
+ schema = schema_factory.new(**kwargs)
40
+ hash[:schema] = schema.schema
41
+ hash[:uischema] = schema.uischema
42
+ end
43
+
44
+ hash
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+ module AtomicAdmin::JwtToken
2
+ # Decodes a JWT token using the JWKS endpoint
3
+ # This is used for decoding JWT tokens issued by the new
4
+ # admin app
5
+ class JwksDecoder
6
+ ALGORITHMS = ["RS256"].freeze
7
+
8
+ def initialize(jwks_url, algorithms = ALGORITHMS)
9
+ @jwks_url = jwks_url
10
+ @algorithms = algorithms
11
+ end
12
+
13
+ def decode(token, validate = true)
14
+ load_admin_jwks = ->(options) do
15
+ Rails.cache.delete("atomic_admin_jwks") if options[:kid_not_found]
16
+
17
+ # NOTE: the cached keys only expire when we recieve a kid_not_found error
18
+ keys = Rails.cache.fetch("atomic_admin_jwks") do
19
+ HTTParty.get(@jwks_url).parsed_response
20
+ end
21
+
22
+ JWT::JWK::Set.new(keys).select { |k| k[:use] == "sig" }
23
+ end
24
+
25
+ JWT.decode(
26
+ token,
27
+ nil,
28
+ validate,
29
+ { algorithms: @algorithms, jwks: load_admin_jwks },
30
+ )
31
+ end
32
+
33
+ def decode!(token)
34
+ token = decode(token)
35
+ raise AtomicAdmin::JwtToken::InvalidTokenError, "Unable to decode jwt token" if token.blank?
36
+ raise AtomicAdmin::JwtToken::InvalidTokenError, "Invalid token payload" if token.empty?
37
+
38
+ token[0]
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ module AtomicAdmin::JwtToken
2
+ # Decodes a JWT token using a known secret. This is used for decoding
3
+ # JWT tokens issued by the application itself for the old admin app
4
+ class SecretDecoder
5
+ ALGORITHM = "HS512".freeze
6
+
7
+ def initialize(secret, algorithm = ALGORITHM)
8
+ @secret = secret
9
+ @algorithm = algorithm
10
+ end
11
+
12
+ def decode(token, validate = true)
13
+ JWT.decode(
14
+ token,
15
+ @secret,
16
+ validate,
17
+ { algorithm: @algorithm },
18
+ )
19
+ end
20
+
21
+ def decode!(token)
22
+ token = decode(token)
23
+ raise AtomicAdmin::JwtToken::InvalidTokenError, "Unable to decode jwt token" if token.blank?
24
+ raise AtomicAdmin::JwtToken::InvalidTokenError, "Invalid token payload" if token.empty?
25
+
26
+ token[0]
27
+ end
28
+ end
29
+ end
@@ -1,69 +1,9 @@
1
- ## Note: This code is basically copied out of the starter app to authenticate
2
- ## admin app api calls. Lives at /app/controllers/concerns/jwt_token.rb in
3
- ## starter app
1
+ require_relative 'jwt_token/jwks_decoder'
2
+ require_relative 'jwt_token/secret_decoder'
3
+
4
4
  module AtomicAdmin
5
5
  module JwtToken
6
-
7
- ALGORITHM = "HS512".freeze
8
-
9
6
  class InvalidTokenError < StandardError; end
10
-
11
- def self.valid?(token, secret = nil, algorithm = ALGORITHM)
12
- decode(token, secret, true, algorithm)
13
- end
14
-
15
- def self.decode(token, secret = nil, validate = true, algorithm = ALGORITHM)
16
- JWT.decode(
17
- token,
18
- secret || Rails.application.secrets.auth0_client_secret,
19
- validate,
20
- { algorithm: algorithm },
21
- )
22
- end
23
-
24
- def decoded_jwt_token(req, secret = nil)
25
- token = AtomicAdmin::JwtToken.valid?(encoded_token(req), secret)
26
- raise InvalidTokenError, "Unable to decode jwt token" if token.blank?
27
- raise InvalidTokenError, "Invalid token payload" if token.empty?
28
-
29
- token[0]
30
- end
31
-
32
- def validate_token
33
- token = decoded_jwt_token(request)
34
- raise InvalidTokenError if Rails.application.secrets.auth0_client_id != token["aud"]
35
-
36
- current_application_instance_id = request.env['atomic.validated.application_instance_id']
37
- if current_application_instance_id && current_application_instance_id != token["application_instance_id"]
38
- raise InvalidTokenError
39
- end
40
-
41
- @user_tenant = token["user_tenant"] if token["user_tenant"].present?
42
- @user = User.find(token["user_id"])
43
-
44
- sign_in(@user, event: :authentication, store: false)
45
- rescue JWT::DecodeError, InvalidTokenError => e
46
- Rails.logger.error "JWT Error occured #{e.inspect}"
47
- begin
48
- render json: { error: "Unauthorized: Invalid token." }, status: :unauthorized
49
- rescue NoMethodError
50
- raise GraphQL::ExecutionError, "Unauthorized: Invalid token."
51
- end
52
- end
53
-
54
- protected
55
-
56
- def encoded_token(req)
57
- return req.params[:jwt] if req.params[:jwt]
58
-
59
- header = req.headers["Authorization"] || req.headers[:authorization]
60
- raise InvalidTokenError, "No authorization header found" if header.nil?
61
-
62
- token = header.split(" ").last
63
- raise InvalidTokenError, "Invalid authorization header string" if token.nil?
64
-
65
- token
66
- end
67
-
7
+ class MissingTokenError < StandardError; end
68
8
  end
69
- end
9
+ end
@@ -0,0 +1,61 @@
1
+ module AtomicAdmin::Schema
2
+ class ApplicationInstanceConfigurationSchema < ApplicationInstanceSchema
3
+ def schema
4
+ {
5
+ type: "object",
6
+ properties: {
7
+ config: {
8
+ type: "object",
9
+ },
10
+ lti_config: {
11
+ type: "object",
12
+ },
13
+ },
14
+ }
15
+ end
16
+
17
+ def uischema
18
+ {
19
+ type: "VerticalLayout",
20
+ elements: [
21
+ {
22
+ type: "Group",
23
+ label: "Custom App Instance Configuration",
24
+ elements: [
25
+ {
26
+ type: "Control",
27
+ scope: "#/properties/config",
28
+ options: {
29
+ format: "json",
30
+ props: {
31
+ size: "full",
32
+ label: "",
33
+ height: "400px",
34
+ },
35
+ },
36
+ },
37
+ ],
38
+ },
39
+ {
40
+ type: "Group",
41
+ label: "LTI Config",
42
+ elements: [
43
+ {
44
+ type: "Control",
45
+ scope: "#/properties/lti_config",
46
+ options: {
47
+ format: "json",
48
+ props: {
49
+ size: "full",
50
+ label: "",
51
+ height: "400px",
52
+ },
53
+ },
54
+ },
55
+ ],
56
+ },
57
+ ],
58
+ }
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,77 @@
1
+
2
+ module AtomicAdmin::Schema
3
+ class ApplicationInstanceCreateSchema
4
+ attr_accessor :application
5
+
6
+ def initialize(application)
7
+ @application = application
8
+ end
9
+
10
+ def schema
11
+ sites = Site.all
12
+
13
+ {
14
+ type: "object",
15
+ required: ["nickname", "site_id", "lti_key"],
16
+ properties: {
17
+ nickname: {
18
+ type: "string",
19
+ minLength: 1,
20
+ },
21
+ primary_contact: {
22
+ type: ["string", "null"],
23
+ },
24
+ lti_key: {
25
+ type: "string",
26
+ },
27
+ site_id: {
28
+ type: "number",
29
+ oneOf: sites.map do |site|
30
+ {
31
+ title: site.url,
32
+ const: site.id
33
+ }
34
+ end
35
+ },
36
+ },
37
+ }
38
+ end
39
+
40
+ def uischema
41
+ {
42
+ type: "VerticalLayout",
43
+ elements: [
44
+ {
45
+ type: "HorizontalLayout",
46
+ elements: [
47
+ {
48
+ type: "Control",
49
+ scope: "#/properties/nickname",
50
+ },
51
+ {
52
+ type: "Control",
53
+ scope: "#/properties/primary_contact",
54
+ },
55
+ ],
56
+ },
57
+ {
58
+ type: "HorizontalLayout",
59
+ elements: [
60
+ {
61
+ type: "Control",
62
+ scope: "#/properties/site_id",
63
+ props: {
64
+ label: "Site"
65
+ }
66
+ },
67
+ {
68
+ type: "Control",
69
+ scope: "#/properties/lti_key",
70
+ },
71
+ ]
72
+ }
73
+ ]
74
+ }
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,156 @@
1
+ module AtomicAdmin::Schema
2
+ class ApplicationInstanceGeneralSettingsSchema < ApplicationInstanceSchema
3
+
4
+ def schema
5
+ sites = Site.all
6
+
7
+ {
8
+ type: "object",
9
+ properties: {
10
+ nickname: {
11
+ type: "string",
12
+ minLength: 1,
13
+ },
14
+ primary_contact: {
15
+ type: ["string", "null"],
16
+ },
17
+ created_at: {
18
+ type: "string",
19
+ format: "date-time",
20
+ },
21
+ lti_key: {
22
+ type: "string",
23
+ },
24
+ lti_secret: {
25
+ type: "string",
26
+ },
27
+ # The available sites is based on the sites that have been created
28
+ # so this would require it to be dynamically generated on the fly
29
+ site_id: {
30
+ type: "number",
31
+ oneOf: sites.map do |site|
32
+ {
33
+ title: site.url,
34
+ const: site.id
35
+ }
36
+ end
37
+ },
38
+ domain: {
39
+ type: "string",
40
+ },
41
+ canvas_token: {
42
+ type: ["string", "null"],
43
+ },
44
+ rollbar_enabled: {
45
+ type: "boolean",
46
+ },
47
+ use_scoped_developer_key: {
48
+ type: "boolean",
49
+ },
50
+ },
51
+ required: ["nickname"],
52
+ }
53
+ end
54
+
55
+ def uischema
56
+ token_preview = @application_instance.canvas_token_preview() || "Not set"
57
+ {
58
+ type: "HorizontalLayout",
59
+ elements: [
60
+ {
61
+ type: "Group",
62
+ label: "General Settings",
63
+ elements: [
64
+ {
65
+ type: "VerticalLayout",
66
+ elements: [
67
+ {
68
+ type: "Control",
69
+ scope: "#/properties/nickname",
70
+ },
71
+ {
72
+ type: "Control",
73
+ scope: "#/properties/primary_contact",
74
+ },
75
+ {
76
+ type: "Control",
77
+ scope: "#/properties/created_at",
78
+ label: "Date Created",
79
+ options: {
80
+ format: "date",
81
+ props: {
82
+ isReadOnly: true,
83
+ size: "small",
84
+ },
85
+ },
86
+ },
87
+ {
88
+ type: "Control",
89
+ scope: "#/properties/site_id",
90
+ label: "LMS URL",
91
+ options: {
92
+ props: {
93
+ menuSize: "auto",
94
+ }
95
+ }
96
+ },
97
+ {
98
+ type: "Control",
99
+ scope: "#/properties/domain",
100
+ label: "LTI Tool Domain",
101
+ },
102
+ {
103
+ type: "Control",
104
+ scope: "#/properties/canvas_token",
105
+ label: "Canvas Token",
106
+ options: {
107
+ props: {
108
+ message: "Current Canvas Token: #{token_preview}",
109
+ },
110
+ },
111
+ },
112
+ {
113
+ type: "Control",
114
+ scope: "#/properties/rollbar_enabled",
115
+ label: "Enable Rollbar",
116
+ },
117
+ {
118
+ type: "Control",
119
+ scope: "#/properties/use_scoped_developer_key",
120
+ label: "Use Scoped Developer Key",
121
+ },
122
+ ],
123
+ },
124
+ ],
125
+ },
126
+ {
127
+ type: "Group",
128
+ label: "LTI Key and Secret",
129
+ elements: [
130
+ {
131
+ type: "VerticalLayout",
132
+ elements: [
133
+ {
134
+ type: "Control",
135
+ scope: "#/properties/lti_key",
136
+ label: "LTI Key",
137
+ options: {
138
+ props: {
139
+ isReadOnly: true,
140
+ },
141
+ },
142
+ },
143
+ {
144
+ type: "Control",
145
+ scope: "#/properties/lti_secret",
146
+ label: "LTI Secret",
147
+ },
148
+ ],
149
+ },
150
+ ],
151
+ },
152
+ ],
153
+ }
154
+ end
155
+ end
156
+ end