Skip to content

Commit

Permalink
[feature] Web hook drive strategy
Browse files Browse the repository at this point in the history
Addresses: #682

Api Keys:

https://user-images.githubusercontent.com/50227291/196992417-ab91ed2e-1514-4abf-9954-d44123542583.mov

Webhook:

https://user-images.githubusercontent.com/50227291/196992487-4c3c9593-4cb6-4b8c-8f7b-c6e22d9385a2.mov

**The webhook's payload can be accessed as `parameters[:request]['body']` in external api client.**

Eg: https://github.com/restarone/violet_rails/pull/1167/files#diff-1aa9cadc4b4c76bf7ec3d7c0cf8d0da66be3ba87336bad4d32241c99d53aa6beR603-R623

```
    class ExternalApiModelExample
      def initialize(parameters)
        @external_api_client = parameters[:external_api_client]
        @request = parameters[:request]
      end
  
      def start
        if @request['body']['type'] == 'customer.created'
          @external_api_client.api_namespace.api_resources.create(
            properties: {
              request_body: @request["body"]	
            }
          )
        end
      end
```

Custom webhook verification method:

https://user-images.githubusercontent.com/50227291/197921958-a06b85a8-7c7a-4e3b-afb9-889fe5c6e110.mov

Co-authored-by: Pralish Kayastha <[email protected]>
Co-authored-by: Pralish Kayastha <[email protected]>
Co-authored-by: Prashant Khadka <[email protected]>
  • Loading branch information
4 people authored Jan 17, 2023
1 parent f661907 commit b036672
Show file tree
Hide file tree
Showing 64 changed files with 2,329 additions and 857 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ gem 'bcrypt_pbkdf', '>= 1.0', '< 2.0'
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'pry'
end

group :development do
Expand Down Expand Up @@ -113,4 +114,6 @@ gem "turbo-rails", "~> 1.1"

gem "redis-namespace", "~> 1.8"

gem 'stripe-rails'

gem 'devise-two-factor', "4.0.2"
11 changes: 11 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ GEM
climate_control (0.2.0)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
coderay (1.1.3)
comfy_bootstrap_form (4.0.9)
rails (>= 5.0.0)
concurrent-ruby (1.1.10)
Expand Down Expand Up @@ -310,6 +311,9 @@ GEM
orm_adapter (0.5.0)
parallel (1.20.1)
pg (1.2.3)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (4.0.6)
puma (5.6.4)
nio4r (~> 2.0)
Expand Down Expand Up @@ -443,6 +447,11 @@ GEM
sshkit (1.21.2)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stripe (7.1.0)
stripe-rails (2.3.5)
rails (>= 5.1)
responders
stripe (>= 3.15.0)
temple (0.8.2)
thor (1.2.1)
tilt (2.0.10)
Expand Down Expand Up @@ -534,6 +543,7 @@ DEPENDENCIES
mocha
net-ssh (>= 6.0.2)
pg (~> 1.1)
pry
puma (~> 5.6)
rack-cors
rack-mini-profiler (~> 2.0)
Expand All @@ -551,6 +561,7 @@ DEPENDENCIES
sinatra
sitemap_generator
spring
stripe-rails
turbo-rails (~> 1.1)
turnout (~> 2.5)
tzinfo-data
Expand Down
6 changes: 3 additions & 3 deletions app/controllers/api/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Api::BaseController < ActionController::API
def authenticate_request
if @api_namespace.requires_authentication
unless validate_bearer_token
render json: { status: 'unauthorized', code: 401 }
render json: { status: 'unauthorized', code: 401 }, status: 401
end
end
end
Expand All @@ -15,8 +15,8 @@ def validate_bearer_token
bearer_token = request.headers['Authorization']
if bearer_token
token = bearer_token.split(' ')[1]
api_client = @api_namespace.api_clients.find_by(bearer_token: token)
if api_client
api_key = @api_namespace.api_keys.any? { |api_key| api_key.token == token }
if api_key
return true
else
return false
Expand Down
40 changes: 40 additions & 0 deletions app/controllers/api/external_api_clients_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
class Api::ExternalApiClientsController < Api::BaseController
before_action :find_external_api_client
after_action :unload_api_connection_class, only: :webhook
skip_before_action :authenticate_request

def webhook
# should only exist if drive strategy is webhook
raise ActionController::RoutingError.new('Not Found') unless @external_api_client.drive_strategy == ExternalApiClient::DRIVE_STRATEGIES[:webhook]

return render json: { message: 'Webhook not enabled', success: false }, status: 400 unless @external_api_client.enabled

if @external_api_client.webhook_verification_method.present?
verified, message = Webhook::Verification.new(request, @external_api_client.webhook_verification_method).call

return render json: { message: message, success: false }, status: 401 unless verified
end

@model_definition = @external_api_client.evaluated_model_definition

# add render method on model_definition
# render should only be available on controller context
@model_definition.define_method(:render) do |args|
return { render: true, args: args }
end

response = @model_definition.new(external_api_client: @external_api_client, request: request).start

render response[:args] if response.is_a?(Hash) && response[:render]
end

private

def unload_api_connection_class
ExternalApiClient.send(:remove_const, @model_definition.name.split('::').last.to_sym) if @model_definition
end

def find_external_api_client
@external_api_client = ExternalApiClient.find_by(slug: params[:external_api_client])
end
end
53 changes: 53 additions & 0 deletions app/controllers/comfy/admin/api_keys_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
class Comfy::Admin::ApiKeysController < Comfy::Admin::Cms::BaseController
before_action :ensure_authority_for_read_api_keys_only_in_api, only: %i[ index ]
before_action :ensure_authority_for_view_api_keys_details_only_in_api, only: %i[ show ]
before_action :ensure_authority_for_delete_access_for_api_keys_only_in_api, only: %i[ destroy ]
before_action :ensure_authority_for_full_access_for_api_keys_only_in_api, only: %i[ new edit create update ]

before_action :set_api_key, only: [:update, :edit, :destroy, :show]

def index
@api_keys = ApiKey.all
end

def new
@api_key = ApiKey.new
@api_key.api_namespace_keys.build
end

def edit
end

def create
@api_key = ApiKey.new(api_key_params)

if @api_key.save
redirect_to api_key_path(id: @api_key.id), notice: "Api key was successfully created."
else
render :new, status: :unprocessable_entity
end
end

def update
if @api_key.update(api_key_params)
redirect_to api_key_path(id: @api_key.id), notice: "Api key was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end

def destroy
@api_key.destroy
redirect_to api_keys_path, notice: "Api key was successfully destroyed."
end

private

def set_api_key
@api_key = ApiKey.find_by(id: params[:id])
end

def api_key_params
params.require(:api_key).permit(:label, :authentication_strategy, api_namespace_ids: [])
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ def show
# GET /external_api_clients/new
def new
@external_api_client = ExternalApiClient.new(api_namespace_id: @api_namespace.id)
@external_api_client.build_webhook_verification_method
end

# GET /external_api_clients/1/edit
def edit
@external_api_client.build_webhook_verification_method unless @external_api_client.webhook_verification_method.present?
end

# POST /external_api_clients or /external_api_clients.json
Expand Down Expand Up @@ -106,7 +108,9 @@ def external_api_client_params
:max_requests_per_minute,
:max_workers,
:model_definition,
:drive_every
:drive_every,
:require_webhook_verification,
webhook_verification_method_attributes: [:id, :webhook_type, :webhook_secret, :custom_method_definition]
).merge({
api_namespace_id: @api_namespace.id,
})
Expand Down
74 changes: 59 additions & 15 deletions app/controllers/subdomains/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,37 @@ def ensure_authority_for_full_access_for_api_form_only_in_api
end
end

def ensure_authority_for_read_api_keys_only_in_api
unless user_authorized_for_api_keys_accessibility?(ApiNamespace::API_ACCESSIBILITIES[:read_api_keys_only])
flash.alert = "You do not have the permission to do that. Only users with full_access or read_access or delete_acess for ApiKeys are allowed to perform that action."
redirect_back(fallback_location: root_url)
end
end

def ensure_authority_for_view_api_keys_details_only_in_api
unless user_authorized_for_api_keys_accessibility?(ApiNamespace::API_ACCESSIBILITIES[:view_api_keys_details_only])
flash.alert = "You do not have the permission to do that. Only users with full_access or read_access for ApiKeys are allowed to perform that action."
redirect_back(fallback_location: root_url)
end
end

def ensure_authority_for_full_access_for_api_keys_only_in_api
unless user_authorized_for_api_keys_accessibility?(ApiNamespace::API_ACCESSIBILITIES[:full_access_for_api_keys_only])
flash.alert = "You do not have the permission to do that. Only users with full_access for ApiKeys are allowed to perform that action."
redirect_back(fallback_location: root_url)
end
end

def ensure_authority_for_delete_access_for_api_keys_only_in_api
unless user_authorized_for_api_keys_accessibility?(ApiNamespace::API_ACCESSIBILITIES[:delete_access_for_api_keys_only])
flash.alert = "You do not have the permission to do that. Only users with full_access or delete_access for ApiKeys are allowed to perform that action."
redirect_back(fallback_location: root_url)
end
end

def ensure_authority_for_viewing_all_api
unless user_authorized_to_view_all_api?(ApiNamespace::API_ACCESSIBILITIES[:full_read_access_in_api_namespace])
flash.alert = "You do not have the permission to do that. Only users with full_access or full_read_access or full_access_api_namespace_only are allowed to perform that action."
flash.alert = "You do not have the permission to do that. Only users with access in ApiNamespaces or access in ApiKeys are allowed to perform that action."
redirect_back(fallback_location: root_url)
end
end
Expand All @@ -183,26 +211,26 @@ def ensure_authority_for_creating_api

private
def user_authorized_for_api_accessibility?(api_permissions, check_categories: true)
user_api_accessibility = current_user.api_accessibility
return false unless current_user.api_accessibility['api_namespaces'].present?

return false unless user_api_accessibility.present?
api_namespaces_accessibility = current_user.api_accessibility['api_namespaces']

is_user_authorized = false

if user_api_accessibility.keys.include?('all_namespaces')
if api_namespaces_accessibility.keys.include?('all_namespaces')
is_user_authorized = api_permissions.any? do |access_name|
user_api_accessibility.dig('all_namespaces', access_name).present? && user_api_accessibility.dig('all_namespaces', access_name) == 'true'
api_namespaces_accessibility.dig('all_namespaces', access_name).present? && api_namespaces_accessibility.dig('all_namespaces', access_name) == 'true'
end
elsif user_api_accessibility.keys.include?('namespaces_by_category')
elsif api_namespaces_accessibility.keys.include?('namespaces_by_category')
if check_categories && (categories = @api_namespace.categories.pluck(:label)) && categories.present?
categories.any? do |category|
is_user_authorized = api_permissions.any? do |access_name|
user_api_accessibility.dig('namespaces_by_category', category, access_name).present? && user_api_accessibility.dig('namespaces_by_category', category, access_name) == 'true'
api_namespaces_accessibility.dig('namespaces_by_category', category, access_name).present? && api_namespaces_accessibility.dig('namespaces_by_category', category, access_name) == 'true'
end
end
else
is_user_authorized = api_permissions.any? do |access_name|
user_api_accessibility.dig('namespaces_by_category', 'uncategorized', access_name).present? && user_api_accessibility.dig('namespaces_by_category', 'uncategorized', access_name) == 'true'
api_namespaces_accessibility.dig('namespaces_by_category', 'uncategorized', access_name).present? && api_namespaces_accessibility.dig('namespaces_by_category', 'uncategorized', access_name) == 'true'
end
end
end
Expand All @@ -211,24 +239,40 @@ def user_authorized_for_api_accessibility?(api_permissions, check_categories: tr
end

def user_authorized_to_view_all_api?(api_permissions)
user_api_accessibility = current_user.api_accessibility
return false unless current_user.api_accessibility.keys.present?

return false unless user_api_accessibility.present?
api_namespaces_accessibility = current_user.api_accessibility['api_namespaces']
api_keys_accessibility = current_user.api_accessibility['api_keys']

is_user_authorized = false

if user_api_accessibility.keys.include?('all_namespaces')
if api_namespaces_accessibility.present? && api_namespaces_accessibility.keys.include?('all_namespaces')
is_user_authorized = api_permissions.any? do |access_name|
user_api_accessibility.dig('all_namespaces', access_name).present? && user_api_accessibility.dig('all_namespaces', access_name) == 'true'
api_namespaces_accessibility.dig('all_namespaces', access_name).present? && api_namespaces_accessibility.dig('all_namespaces', access_name) == 'true'
end
elsif user_api_accessibility.keys.include?('namespaces_by_category')
categories = user_api_accessibility.dig('namespaces_by_category').keys
elsif api_namespaces_accessibility.present? && api_namespaces_accessibility.keys.include?('namespaces_by_category')
categories = api_namespaces_accessibility.dig('namespaces_by_category').keys

categories.any? do |category|
is_user_authorized = api_permissions.any? do |access_name|
user_api_accessibility.dig('namespaces_by_category', category, access_name).present? && user_api_accessibility.dig('namespaces_by_category', category, access_name) == 'true'
api_namespaces_accessibility.dig('namespaces_by_category', category, access_name).present? && api_namespaces_accessibility.dig('namespaces_by_category', category, access_name) == 'true'
end
end
elsif api_keys_accessibility.present?
is_user_authorized = ApiNamespace::API_ACCESSIBILITIES[:read_api_keys_only].any? do |access_name|
api_keys_accessibility.dig(access_name).present? && api_keys_accessibility.dig(access_name) == 'true'
end
end

is_user_authorized
end

def user_authorized_for_api_keys_accessibility?(api_permissions)
return false unless current_user.api_accessibility['api_keys'].present?

api_keys_accessibility = current_user.api_accessibility['api_keys']
is_user_authorized = api_permissions.any? do |access_name|
api_keys_accessibility[access_name].present? && api_keys_accessibility[access_name] == 'true'
end

is_user_authorized
Expand Down
35 changes: 22 additions & 13 deletions app/helpers/api_accessibility_helper.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
module ApiAccessibilityHelper
def has_access_to_main_category?(api_accessibility, top_category)
api_accessibility.dig(top_category).present?
api_accessibility.dig('api_namespaces', top_category).present?
end

def has_access_to_specific_category?(api_accessibility, category_label)
api_accessibility.dig('namespaces_by_category', category_label).present?
api_accessibility.dig('api_namespaces', 'namespaces_by_category', category_label).present?
end

def has_sub_access_to_specific_category?(api_accessibility, sub_access, top_category, category_label = nil)
if top_category == 'all_namespaces'
api_accessibility.dig(top_category, sub_access).present?
api_accessibility.dig('api_namespaces', top_category, sub_access).present?
else
api_accessibility.dig(top_category, category_label, sub_access).present?
api_accessibility.dig('api_namespaces', top_category, category_label, sub_access).present?
end
end

def has_no_sub_access_to_specific_category?(api_accessibility, top_category, category_label = nil)
if top_category == 'all_namespaces'
api_accessibility.dig(top_category)&.keys.present?
api_accessibility.dig('api_namespaces', top_category)&.keys.present?
else
api_accessibility.dig(top_category, category_label)&.keys.present?
api_accessibility.dig('api_namespaces', top_category, category_label)&.keys.present?
end
end

def has_access_to_api_accessibility?(api_permissions, user, api_namespace)
user_api_accessibility = user.api_accessibility
user_api_accessibility = user.api_accessibility['api_namespaces']

return false unless user_api_accessibility.present?

Expand Down Expand Up @@ -52,22 +52,31 @@ def has_access_to_api_accessibility?(api_permissions, user, api_namespace)

is_user_authorized
end

def has_access_to_api_keys?(api_accessibility, access_name)
api_accessibility.dig('api_keys', access_name).present? && api_accessibility.dig('api_keys', access_name) == 'true'
end

def has_only_uncategorized_access?(api_accessibility)
return false if api_accessibility.keys.include?('all_namespaces')
if api_accessibility.keys.include?('namespaces_by_category')
categories = api_accessibility['namespaces_by_category'].keys
api_namespaces_accessibility = api_accessibility['api_namespaces']

return false if api_namespaces_accessibility.keys.include?('all_namespaces')

if api_namespaces_accessibility.keys.include?('namespaces_by_category')
categories = api_namespaces_accessibility['namespaces_by_category'].keys
return true if categories.size == 1 && categories[0] == 'uncategorized'
end

false
end

def filter_categories_by_api_accessibility(api_accessibility, categories)
if api_accessibility.keys.include?('all_namespaces')
api_namespaces_accessibility = api_accessibility['api_namespaces']

if api_namespaces_accessibility.keys.include?('all_namespaces')
categories
elsif api_accessibility.keys.include?('namespaces_by_category')
accessible_categories = api_accessibility['namespaces_by_category'].keys - ['uncategorized']
elsif api_namespaces_accessibility.keys.include?('namespaces_by_category')
accessible_categories = api_namespaces_accessibility['namespaces_by_category'].keys - ['uncategorized']

categories.where(label: accessible_categories)
end
Expand Down
Loading

0 comments on commit b036672

Please sign in to comment.