Rails rswag - override default server to provide a different base URL - ruby-on-rails

The API I am working with has a few endpoints to be routed through VeryGoodSecurity for PCI compliance.
It does seem as though OpenAPI V3 supports overriding servers as mentioned in this page
The global servers array can be overridden on the path level or operation level. This is handy if some endpoints use a different server or base path than the rest of the API. Common examples are:
How can this be done with rswag?
I tried some thing like this:
path('/v1/payment-methods/cards') do
post('Create a payment method from card details') do
tags('Payment Method')
consumes('application/json')
produces('application/json')
# ....
# Rest of the API will be on api.tryedge.com
servers([{
url: 'https://secure.tryedge.com',
description: 'Edge secure card portal'}])
Hoping to achieve some thing like this in Swagger YML:
/v1/payment-methdos/cards:
post:
servers:
- url: https://secure.tryedge.com
description: Edge secure card portal
But I am getting an error.
undefined method `servers' for RSpec::ExampleGroups::ApiV1PaymentMethodsController::V1PaymentMethodsCards::Post:Class
Any help highly appreciated. Thanks in advance.

rswag doesn't define a full set of helpers (including servers as experienced in the above issue) to cover the entirety of the OpenAPI v3 schema.
However, it is possible to achieve the same result by manipulating the test case metadata as highlighted by #engineersmnky 's comments above.
path('/v1/payment_methods/cards') do
post('Link a card') do
tags('Payment Method')
consumes('application/json')
produces('application/json')
security([bearerAuth: []])
operationId('linkCard')
description('Links a card to an existing Edge customer.')
metadata[:operation][:servers] = [{ url: 'https://secure.tryedge.com', description: 'Edge secure card portal' }]
This is because rswag is building the schema by updating the metadata of the test case.

Related

Rails - Build adapter for several APIs

I need help on my thoughts on building an adapter for several APIs in Rails.
Let me explain first:
I want my clients to be able to add third-party extensions to my app.
I have a model Extension and another CustomExtension.
I create the extensions myself, that appear then to the "Available extensions" section. When a user clicks on "Add extension", it goes to the "My extensions" section (which is represented by CustomExtension).
I created an extension called API Connector and i want it to work as follows:
When a user clicks on "Add extension", he picks a category (that i have defined myself) the API is part of (like "Customer Reviews"). Then, the user will enter some fields like an api_key or an api_endpoint. Then, i want to be able to connect to this api and display some other fields relevant to the api (like the name of where it comes from, example: for Customer Reviews, if a user connects the Google API for it, i want to rename the extension from API Connector to Google Customer Reviews).
In a few words, I want to be able to connect several and different APIs with the same interface and let the user do it without implementing the API in my project.
EDIT — More info:
The APIs might not have the same authentication process or the same properties. They can be very different from each other.
The technical requirements are RESTful APIs and JSON-based.
As I understand it, you want to create a way for users to connect to APIs that are unknown until runtime, based on the parameters that a user defines? If so, there's a Ruby library (now removed from Rails) that's built for allowing easy connection to REST APIs, maybe that could be of help here?
https://github.com/rails/activeresource
So, suppose I want to pull breed info from the Cat API. Here's some example code that would let me define that at runtime:
require "active_resource"
require "ostruct"
##
# This is just a data-store. It could be an ActiveRecord object or some other set
# of values that you need for the API. You'll have to establish your own criteria
# for types of API configuration you can support
#config = OpenStruct.new(
# We need to set a custom header and value
header_name: 'x-api-key',
# get your own key here: https://docs.thecatapi.com
header_value: '96120fe6-0846-41c6-9a1d-8a70e691dd47',
base_url: "https://api.thecatapi.com/v1/",
# What's the path where this resource can be found
resource_name: "breeds",
# don't add ".json" to the URLs
use_json_extension: false,
)
##
# Create an anonymous class at runtime that inherits from ActiveResource::Base
#custom_extension = Class.new(ActiveResource::Base)
##
# Config our class based on the user-provided values.
#custom_extension.class_eval do
self.include_format_in_path = #config.use_json_extension
self.site = #config.base_url
self.headers[#config.header_name] = #config.header_value
self.element_name = #config.resource_name
# Log errors here
self.logger = Logger.new("./output.log")
end
puts #custom_extension.all.to_s
With any luck, that should download a list of cat breeds for you. Which should be enough to demonstrate the concept. The docs for ActiveResource can be found here: https://rubydoc.info/gems/activeresource
Be careful that you're not importing dangerous content from a source provided by a user!
Hopefully that's what you are looking for?

graphql_devise for authentication in Rails/Graphql/Apollo/React. "field requires authentication" error

I have a project where I am setting up a Rails API with Graphql and React/Apollo. I've purpled all the google links looking for the best authentication solution, but it seems I haven't been able to find a clear answer.
I settled on using the graphql_devise gem, which leverages devise & devise_token_auth. I would have preferred to find a JWT solution, but just couldn't. (If you have any obvious suggestions please tell me!)
First I mounted a separate auth route, but ran into problems with multiple endpoints when I was setting up ApolloClient. I couldn't figure out how to direct auth related requests to my auth endpoint, while letting the rest go through to my graphql one. (If figuring this out is the easiest solution, please tell me!) Instead, I mounted the auth routes in my own schema, as instructed by the docs:
class MyApiSchema < GraphQL::Schema
use GraphqlDevise::SchemaPlugin.new(
query: Types::QueryType,
mutation: Types::MutationType,
resource_loaders: [
GraphqlDevise::ResourceLoader.new('User', only: [:login, :logout])
]
)
mutation(Types::MutationType)
query(Types::QueryType)
And edited the execute line in graphql_controller.rb:
result = MyApiSchema.execute(query, variables: variables, context: graphql_context(:user), operation_name: operation_name)
At this point, running a test query in postman is successful. I can access the graphql route with a userLogin mutation without any headers set, and get a successful response with client, uid & token. Authentication of my other queries also works—success with headers, denied without.
But when I try to perform the same queries using useQuery in react, it doesn't work. In the Apollo Client Developer Tools plugin in Chrome, it doesn't work either, returning only [Object object]. By looking at the request in my Network tab, I can see that this is the result of the same error: "photo field requires authentication".
When I pry into the rails server, I can see that the headers are being received. I can even authenticate the user manually in my graphql_controller before the execute method is called, so I don't think that it is a CORS issue. I have set up the rack-cors gem in Rails to expose the required headers.
When I drill into the code, it seems that the graphql_devise method set_current_resource is failing to return my user. This seems to stem from the devise method set_user_by_token, but I have not been able to figure out why it is failing.
If anyone has any experience with implementing this gem & stack, I would very much appreciate your input! If you have a better way of approaching authentication, I'd love to know what your strategy is. If you can help me solve this ... field requires authentication error, I will love you forever.
Apologies if I've provided too much/too little info, or if my question is too vague. Please let me know if there's something specific I should know to ask/show in my code. Hope you can help! Thanks.
I've managed to find the issue, and I thought I'd explain.
Having traced the problem to the set_user_by_token method in the devise_token_auth gem, I bundle open devise_token_auth'd, and put a byebug inside. This revealed that #token.token was not being extracted correctly from the headers.
The problem was that my header access-token was somehow being converted to accessToken, so when devise tried to set the token info from the request headers using this key, it returned nil.
I do not know why or where this conversion takes place. I suspect it originates from Apollo/React rather than Rails, since the headers on my postman query were not altered. In react, when I set the headers they are set with access-token, as below, but it seems that at some point in the life of my request they are changed.
How I have set the headers in React:
const client = new ApolloClient({
cache,
link: new HttpLink({
uri: 'http://localhost:3000/graphql',
headers: {
accessToken: localStorage.getItem('access-token'),
client: localStorage.getItem('client'),
uid: localStorage.getItem('uid')
},
}),
});
The keys that devise_token_auth uses to select the headers from the request can be changed in the initializers/devise_token_auth.rb file.
I edited them as follows:
config.headers_names = {:'access-token' => 'accessToken',
:'client' => 'client',
:'expiry' => 'expiry',
:'uid' => 'uid',
:'token-type' => 'token-type' }
This means that my front-end is now working smoothly, and I can perform queries/mutations with authentication. However, now in my postman queries I have to change the header to accessToken if I want them to work.
It would be great to know exactly at what point my header from React is changed to camelCase, but for now, it's working. Hope this helps if anyone goes through the same frustrations!

GraphQL Ruby Using NameSpace

I am using graphql-ruby in my rails application.
Currently, my app's directory structure looks like this.
app
- controllers
- application_controller.rb
- graphql_controller.rb
- admin
- application_controller.rb
- graphql_controller.rb
- graphql
- types
- mutations
- graphql_schema.rb
I am trying to make a simple admin page but I'm not sure how I should hanlde namespaces with graphql-ruby.
Should I make Admin directory under graphql as well and make types and mutations under it for the data I want to use on the admin page??
Also, should I make another endpoint for Admin like the code below??
Rails.application.routes.draw do
namespace :admin do
post :graphql, to: 'graphql#execute'
end
post :graphql, to: 'graphql#execute'
end
Can you possibly give me the link of a project that does what I am trying to do with graphql-ruby??? That would be a tremendous help.
From https://graphql.org/
GraphQL APIs are organized in terms of types and fields, not endpoints. Access the full capabilities of your data from a single endpoint.
Hence, creating two endpoints as you have suggested would go against that principle. You probably shouldn't do it, but most importantly, there's no need to.
Suppose you have a type ProductType with a couple of fields. You can use that same type to both query/display the product data in your website and edit it with a mutation in the admin page. Granted, you may have to deal with authorizing some specific queries and mutations, but it shouldn't be any harder than dealing with authorization in REST.
See more about GraphQL authorization in Ruby.

What is the Best Practice for testing api controllers(active model serializers)?

I am working on an app and up to this point I have been testing just stuff like authentication and request response codes. but it seems like a good idea to test the structure of the payload. ie. if there is embedded resources or sidloaded resources. how do you guys test this. here is a sample of some of the testing I am doing. I am using active model serializers. but seems like a bit of cruft to organize.
describe '#index' do
it 'should return an array of email templates' do
template = EmailTemplate.new(name: 'new email template')
EmailTemplate.stub(:all).and_return([template])
get :index
payload = {:email_templates => [JSON.parse(response.body)["email_templates"][0].symbolize_keys]}
template_as_json_payload = {:email_templates => [ActiveModel::SerializableResource.new(template).as_json[:email_template] ]}
expect(payload).to eq(template_as_json_payload)
end
end
I'm fond of defining a schema for JSON responses and validating that any response seen in a test conforms to it. That alone does not guarantee that the values of the response are correct but it does tell you that the structure of the response matched your expectations.
Those schemas then become part of the documentation of the API which client implementations can reference. By using the schema in tests I get more confidence that the API documentation is not going to fall out of sync with the implementation. Making schema changes in order to get a passing test is also a good prompt for me to consider if a change is safe for existing API clients or if I need to release a new API version.
The folks at Thoughtbot have a nice example of validating schemas using rspec: https://robots.thoughtbot.com/validating-json-schemas-with-an-rspec-matcher
This is one way to do it:
body = JSON.parse(response.body)
assert_equal(body.keys, ["id", "author"])
assert_equal(body["author"].keys, ["id", "name"])
But you should checkout the link that Jonah shared, it's worth reading.

Prevent Authlogic from establishing a session/cookie for non-HTML requests

I'm using Authlogic and Rails 3. On top of the regular browser-based user experience (logging in via form and whatnot), I'd like to implement an API.
Authlogic seems to support single access tokens that don't persist by default. I supply them by adding a GET argument as in:
/users.xml?user_credentails=my_single_access_token
Question: Is there any way I can have Authlogic accept the API key via HTTP Basic Auth? Highrise does something just like this, allowing for:
curl -u 605b32dd:X http://sample.highrisehq.com/people/1.xml
The same with Freshbooks:
curl -u insert_token_here:X https://sample.freshbooks.com/api/2.1/xml-in -d '[xml body here]'
How I would go about imitating this functionality? I can't even figure out where the input data (postdata from forms, HTTP basic, API token) are taken in. I've boiled it down to a call to UserSessions.find with no arguments, but I lose track of it after there.
Any help would be much appreciated!
Related question: I'd also like to disable session persistence (make it so that no cookie is stored) if HTTP basic is used. Any help on this too would be appreciated!
If you're implementing an API, you could consider building a separate Rack application that is then mounted at '/api/1.0/...' and shares your models.
That way you are not tying yourself into having your API directly related to your public routes, which could be difficult to construct for the API user.
A good approach would be to create a simple Sinatra application that exposes just the methods that you want, and to then create a separate authentication strategy:
require 'sinatra'
require 'active_support' # all the Rails stuff
require 'lib/user' # your User class
require 'sinatra/respond_to' # gem install sinatra-respond_to
Sinatra::Application.register Sinatra::RespondTo
use Rack::Auth::Basic, "API", do |username, password|
User.find_by_login(username).valid_password?(password)
end
get '/api/1.0/posts' do
#posts = Post.recent # assuming you have a Post model...
respond_to do |wants|
wants.xml { #posts.to_xml }
wants.to_json { #posts.to_json }
end
end
get '/api/1.0/users/:id' do
#user = User.find_by_login(params[:id])
# Careful here - don't release personal details!
respond_to do |wants|
wants.xml { #user.to_xml }
wants.to_json { #user.to_json }
end
end
Versioning your API with a '1.0' (or similar) in the path means that if you change your models you can create a new version of your API without breaking your users' existing code.
Using this you should be able to allow users to authenticate with HTTP Basic in the form:
curl -u steven:password http://example.com/api/1.0/users/steven.xml
curl -u steven:password http://example.com/api/1.0/users/steven.json
curl -u steven:password http://example.com/api/1.0/posts.xml
To get this running, save it as 'api.rb', and either run it as a Rack Middleware, or create a 'config.ru' file like so:
require 'api'
run Sinatra::Application
And then from that directory:
rackup
Disclaimer: I'm not a 100% this is possible in the way your describing without hacking up Authlogic's core functionality.
The first issue your going to have is that authlogic prevents the use of SSO tokens for authentication unless the request is ATOM or RSS to override this you need to pass a config paramater see here: http://rdoc.info/github/binarylogic/authlogic/master/Authlogic/Session/Params/Config
To the core issue: I don't see any 'easy' way to handle this functionality, however what you could do for something like curl is pass the user token as a paramater (using the -G option) just like you would when visiting the url.
cURL Documentation: http://curl.haxx.se/docs/manpage.html
Forgive me if I misunderstand your question, but I think the answer is a simple "no." You're mixing two metaphors here. If you want a secure API key, use the single access token; if you want to use http basic access authentication, you need a different base64 glyph -- and http basic auth isn't particularly secure (unless used over https, which isn't generally practical).
In more detail:
Per the wikipedia, http basic authentication is intended to provide a username and password in a simple, standard, but fairly insecure base64 encoded glyph.
To use basic auth, then I believe you want to generate the glyph via a simple
Base64.encode64("#{user.name}:#{password}")
...and I'd probably do this by having the user type their password, since you can't derive the password from the crypted_password that authlogic stores in your database.
But the upshot is that this is a very different beast from the single_access_token, and the two can't be mixed.

Resources