Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CORS headers for swagger/openapi endpoints so that the spec can be rendered with petstore.swagger.io #570

Merged
merged 1 commit into from
Sep 15, 2017

Conversation

b-admike
Copy link
Contributor

@b-admike b-admike commented Sep 13, 2017

Add /swagger-ui which would redirect users to the online SwaggerUI instance with the app's OpenAPI spec loaded and CORS enabled so they could access their endpoints from it.

Description

Related issues

Checklist

  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style
    guide

@b-admike b-admike changed the title add explorer endpoint [WIP] add explorer endpoint Sep 13, 2017
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am afraid this pull request makes very little sense to me.

Even if we decide to include GET /explorer endpoint on LoopBack apps, then I think this should be contributed by an "explorer" extension, it should not be built into core/rest layer.

Maybe I misunderstood your intentions, what am I missing?

@@ -147,6 +147,12 @@ export class Application extends Context {
const options = OPENAPI_SPEC_MAPPING[request.url];
return this._serveOpenApiSpec(request, response, options);
}

if (request.method === 'GET' && request.url === '/explorer') {
response.setHeader('Access-Control-Allow-Origin', '*');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, we need to set Access-Control headers inside _serveOpenApiSpec, not here. GET /explorer would be invoked from the browser as a regular request for an HTML page.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the purpose of the story was to allow CORS for all endpoints defined by an application's OpenAPI spec, so that users can test them out using the SwaggerUI instance. With that in mind, I think it should still be set in this method, otherwise it will only be allowed for the /swagger.{json,yaml} or /openapi.{json,yaml} endpoints. Two things I'm not sure of though are if there are any security implications by doing so, and if we should allow it to be set by our application developers.


if (request.method === 'GET' && request.url === '/explorer') {
response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader('Location', 'http://editor.swagger.io');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In LoopBack 3.x, /explorer was opening SwaggerUI, not the editor.

@bajtos
Copy link
Member

bajtos commented Sep 13, 2017

This is the live SwaggerUI instance that people can use to get Explorer-like view of their API: http://petstore.swagger.io/

@b-admike
Copy link
Contributor Author

@bajtos Thank you. I was thinking we could use /explorer endpoint temporarily built into core, but I agree in making an extension for it. This is just an initial commit to get feedback early.

if (options.version === '3.0.0') {
specObj = await swagger2openapi.convertObj(specObj, {direct: true});
appUrl.replace('swagger', 'openapi');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think replace will change appUrl at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I will fix it pending on discussion of #570 (review).

response.setHeader('content-type', 'application/json; charset=utf-8');
response.statusCode = 308;
response.setHeader('Location', 'http://petstore.swagger.io/?url='
+ appUrl);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong. We should not deal with redirect here for the swagger/openapi endpoint. Our responsibility is to serve specs.

Copy link
Contributor Author

@b-admike b-admike Sep 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I misunderstood the story, but I thought we should do it as part of the following acceptance criteria:

Pass generated swagger.json to editor.swagger.io so that users can leverage the existing editor tools on their local application.

@kjdelisle thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my understanding, too. @raymondfeng How do we pass the information along to the server at editor.swagger.io without modifying the request?

Copy link
Contributor

@raymondfeng raymondfeng Sep 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two separate concerns:

  1. Have endpoints from LB to serve openapi/swagger specs
  2. A UI to render the spec as api explorer

Most UI such as editor.swagger.io has an option to import URL or set the location. If we want to use redirect to editor.swagger.io, the path should not be /swagger.json. At lease we need to have something like /swagger.json?ui=true. It should NOT be replacing 1 at all.

For the GA version, we'll have /explorer to serve the UI.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay it makes sense to me now that we need to preserve the /{openapi,swagger}.{json,yaml} endpoints. I will take a crack at implementing /swagger.json?ui=true.

@@ -234,16 +237,28 @@ export class Application extends Context {
) {
options = options || {version: '2.0', format: 'json'};
let specObj = this.getApiSpec();
let appPort = await this.get('http.port');
// TODO make this more robust i.e. get host, protocol, etc. dynamically
let appUrl = 'http://127.0.0.1:' + appPort + '/swagger';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hostname and port should be default the Host header of the http request. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host

Copy link
Member

@bajtos bajtos Sep 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, this will not work in production(like) environments, where the app is running behind a load balancer (nginx, Amalgam8, etc.). The Host header of the http request will be showing the internal hostname not accessible from outside. We could use X-Forwarded-* family of headers to determine the real public host, but I think it's not worth the effort.

@bajtos
Copy link
Member

bajtos commented Sep 14, 2017

Maybe I misunderstood the story, but I thought we should do it as part of the following acceptance criteria:

Pass generated swagger.json to editor.swagger.io so that users can leverage the existing editor tools on their local application.

Sorry for the confusion!

As I understand this CORS related story, we need to add CORS headers to /swagger* and /openapi endpoint responses so that people can open Swagger UI or Swagger Editor SPA (single-page app) running somewhere else and let it load swagger/openapi spec from their running LoopBack application by manually pasting http://localhost:3000/openapi.json (or similar) to that SPA. This is not possible right now: the browser will prevent the SPA from accessing http://localhost:3000/openapi.json, because the response from LoopBack-next does not allow cross-site requests.

@b-admike
Copy link
Contributor Author

@bajtos Thank you for clarifying. Can you PTAL at the latest changes? (cc @raymondfeng @kjdelisle) If we expect the users to manually paste the swagger/openapi spec url into SwaggerUI, we can document how they can do that in our wiki.

@@ -145,7 +148,7 @@ export class Application extends Context {
// content-negotiation to support XML clients, I don't want the OpenAPI
// spec to be converted into an XML response.
const options = OPENAPI_SPEC_MAPPING[request.url];
return this._serveOpenApiSpec(request, response, options);
return this._serveOpenApiSpec(request, response, options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional? The indentation seems to be wrong.

@b-admike b-admike self-assigned this Sep 14, 2017
@@ -135,6 +135,9 @@ export class Application extends Context {
request: ServerRequest,
response: ServerResponse,
) {
// allow CORS support for all endpoints so that users
// can test with online SwaggerUI instance
response.setHeader('Access-Control-Allow-Origin', '*');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I missed it earlier, but is there a reason why we need the header to be *?

Copy link
Contributor

@kjdelisle kjdelisle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the header needs to be set to *, then LGTM, but I imagine that it should be a bit more restrictive than that, yeah?

@raymondfeng raymondfeng changed the title [WIP] add explorer endpoint Add CORS headers for swagger/openapi endpoints so that the spec can be rendered with editor.swagger.io Sep 14, 2017
@@ -135,6 +135,9 @@ export class Application extends Context {
request: ServerRequest,
response: ServerResponse,
) {
// allow CORS support for all endpoints so that users
// can test with online SwaggerUI instance
response.setHeader('Access-Control-Allow-Origin', '*');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get we need to set the Header for all endpoints for Swagger but I'm not sure if users would want that enabled in production, especially with the *. Can this be configurable / toggleable via options (ideally ENV variable)? / Can we look into (perhaps as a separate issue) look into turning this into a configurable component?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should follow the defaults described by http://loopback.io/doc/en/lb3/middleware.json.html#cors-settings.

Access-Control-Allow-Origin: <Origin request header>
Access-Control-Allow-Credentials: true
Access-Control-Allow-Max-Age: 86400

Please note CORS is only enforced by browsers and it's not a security concern for the server itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@virkt25 I think for GA we are aiming to do that (see #488), so I'll leave it as is for now.

@b-admike b-admike force-pushed the feat/add-cors branch 3 times, most recently from 056da52 to 4882e5a Compare September 15, 2017 06:33
@@ -135,6 +135,14 @@ export class Application extends Context {
request: ServerRequest,
response: ServerResponse,
) {
// allow CORS support for all endpoints so that users
// can test with online SwaggerUI instance
if (process.env.ENABLE_CORS) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 for depending on environment variables.

If you want a feature flag to controller this behaviour, then use app-level configuration, e.g. app.getSync('allow-cors-for-api-spec') here. Then it's up to the applications (or loopback-boot) to wire this binding to the value of process.env.ENABLE_CORS.

IMO, CORS should be always enabled for these endpoints.

Copy link
Contributor Author

@b-admike b-admike Sep 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajtos Thank you for the insight. I think that can be done later, and I am fine for leaving them always enabled for now as well.

@@ -147,6 +155,10 @@ export class Application extends Context {
const options = OPENAPI_SPEC_MAPPING[request.url];
return this._serveOpenApiSpec(request, response, options);
}
if (request.method === 'GET' && request.url &&
request.url.match(/\/swagger.json\?ui=true/)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it weird that a query parameter is changing the response so dramatically. Can we use a plain url path like /explorer or /swagger-ui instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense to me. I will change it to /swagger-ui.

) {
response.statusCode = 308;
response.setHeader('Location', 'http://petstore.swagger.io/?url='
+ 'http://' + request.headers.host + '/swagger.json');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@b-admike b-admike force-pushed the feat/add-cors branch 2 times, most recently from ccb9e6d to a9d52ff Compare September 15, 2017 14:11
@b-admike b-admike changed the title Add CORS headers for swagger/openapi endpoints so that the spec can be rendered with editor.swagger.io Add CORS headers for swagger/openapi endpoints so that the spec can be rendered with petstore.swagger.io Sep 15, 2017
// can test with online SwaggerUI instance
response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader('Access-Control-Allow-Credentials', 'true');
response.setHeader('Access-Control-Allow-Max-Age', '86400');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation of code does not match indentation of comments, PTAL.

@@ -147,6 +153,10 @@ export class Application extends Context {
const options = OPENAPI_SPEC_MAPPING[request.url];
return this._serveOpenApiSpec(request, response, options);
}
if (request.method === 'GET' && request.url &&
request.url === '/swagger-ui') {
return this._renderOpenApiSpec(request, response);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: perhaps _redirectToSwaggerUI would be a better name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that's a more descriptive name. 👍

@b-admike b-admike force-pushed the feat/add-cors branch 4 times, most recently from ebf68f7 to b134edb Compare September 15, 2017 18:44
Add /swagger-ui path which would redirect users to the
online SwaggerUI instance with the app's OpenAPI spec
loaded and enable CORS so they could access their
endpoints from it.
@b-admike b-admike merged commit 07b025f into master Sep 15, 2017
@b-admike b-admike deleted the feat/add-cors branch September 15, 2017 20:12
@b-admike b-admike removed the review label Sep 15, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add CORS support to swagger/openapi endpoints
5 participants