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

feat(rest): add OAS Enhancer service #4554

Merged
merged 2 commits into from
Mar 5, 2020

Conversation

dougal83
Copy link
Contributor

@dougal83 dougal83 commented Feb 3, 2020

Add OpenAPI enhancer service in @loopback/rest

Implements #4365

Checklist

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated

* `spec.component.securitySchemes`
*/
@bind(asSpecEnhancer)
export class SecuritySpecEnhancer implements OASEnhancer {
name = 'security';

modifySpec(spec: OpenApiSpec): OpenApiSpec {
const patchSpec = {components: {securitySchemes: SECURITY_SCHEME_SPEC}};
if (spec?.components?.securitySchemes?.jwt) return spec;
Copy link
Contributor

Choose a reason for hiding this comment

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

I would expect the security spec enhancers to be contributed by authentication strategies instead of being hard-coded in @loopback/rest module. This code was added as part of the test fixture intentionally.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Subject to the story(#4386) it is a default and I've opted to not override an existing item. @jannyHou What to do?

Copy link
Contributor

Choose a reason for hiding this comment

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

@raymondfeng I think @dougal83 is adding the default authorizor button in explorer.

Screen Shot 2020-02-07 at 2 04 03 PM

Screen Shot 2020-02-07 at 2 03 57 PM

Copy link
Contributor

Choose a reason for hiding this comment

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

@raymondfeng I think we agree to not bind a built-in jwt strategy into LB4 app, so the only thing we can provide the the security spec, as what @dougal83 adds here.

How about introduce an option useDefaultSecuritySpec, sets to true by default so that our explorer shows the button. If people provide their own auth strategy & security spec enhancer , they can turn off the option.

Copy link
Member

Choose a reason for hiding this comment

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

I would expect the security spec enhancers to be contributed by authentication strategies instead of being hard-coded in @loopback/rest module.

+1

How about introduce an option useDefaultSecuritySpec, sets to true by default so that our explorer shows the button. If people provide their own auth strategy & security spec enhancer , they can turn off the option.

As I mentioned in my other comment, it's my understanding that the REST component does not provide any authentication mechanism out of the box, there is no "default security". Therefore the option useDefaultSecuritySpec does not make sense to me.

IMO, the security spec should be contributed by @loopback/authentication, because that's the extension adding support for bearer tokens. The enhancer should be clever enough to handle the case when there are multiple authentication extensions and/or security specs involved (e.g. Bearer token, HTTP Basic/Digest auth, etc.) and preserve those additional security specs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@raymondfeng I think @dougal83 is adding the default authorizor button in explorer.

Screen Shot 2020-02-07 at 2 04 03 PM Screen Shot 2020-02-07 at 2 03 57 PM

@jannyHou #4386 Having read the issue/story properly just now... I had made a big mistake on my understanding... I thought that is was generally adding a default to the rest server! 🤦‍♂ To make amends I guess I'll have to look into explorer and making the change as per the story!

@dougal83 dougal83 force-pushed the add-oas-enhancer-service branch 2 times, most recently from aeb2ea5 to c788a70 Compare February 3, 2020 21:04
@jannyHou jannyHou self-assigned this Feb 7, 2020
@@ -717,6 +742,10 @@ export class RestServer extends Context implements Server, HttpServerLike {
spec = this.updateSpecFromRequest(spec, requestContext);
}

// Apply OAS enhancers to the OpenAPI specification
this.OASEnhancer.spec = spec;
spec = await this.OASEnhancer.applyEnhancerByName('security');
Copy link
Contributor

Choose a reason for hiding this comment

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

I would suggest rename it to be lb-default-security

Copy link
Contributor

@jannyHou jannyHou left a comment

Choose a reason for hiding this comment

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

@dougal83 Thank you so much for taking care of the spec enhancer story!

The way how the enhancer service gets added looks reasonable to me. A few minor suggestions.

When created the story, I didn't realize applying the enhancer would turn the getApiSpec into an async function. I tend to keep it sync and apply the default security enhancer somewhere else, like _serveOpenApiSpec. And I would like to hear more opinions from @bajtos @raymondfeng @hacksparrow who's more familiar with the rest module regarding where to apply the default security enhancer 🙇‍♀ .

type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
};

/**
* A spec enhancer to add bearer token OpenAPI security entry to
* A spec enhancer to add jwt bearer token OpenAPI security entry to
* `spec.component.securitySchemes`
*/
@bind(asSpecEnhancer)
export class SecuritySpecEnhancer implements OASEnhancer {
name = 'security';
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Thank you @dougal83 for contributing the pull request.

I feel the proposed implementation is going in a wrong direction at high level, see my comment below.

@@ -717,6 +742,10 @@ export class RestServer extends Context implements Server, HttpServerLike {
spec = this.updateSpecFromRequest(spec, requestContext);
}

// Apply OAS enhancers to the OpenAPI specification
this.OASEnhancer.spec = spec;
spec = await this.OASEnhancer.applyEnhancerByName('security');
Copy link
Member

Choose a reason for hiding this comment

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

I don't think the REST server should be invoking specific enhancers defined by a name, because that makes it impossible for other extensions to contribute new enhancers.

As I understand the proposed design:

  1. The REST API invokes all registered API enhancers via applyAllEnhancers().
  2. Extensions like @loopback/authentication contribute custom API enhancers to provide additional OpenAPI metadata like the authentication scheme.

The REST component (@loopback/rest) should not include any security schemas in the OpenAPI spec out of the box, simply because it does not implement any authentication out of the box.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Totally agree, I was being a slave to example in story #4380 Could you provide direction on following:

  • Add option to disable all enhancers on rest server? Alternatively option to override by providing array list of enhancers to apply?
  • Would I follow similar pattern to authentication strategies say spec-enhancers folder alongside or would I seek to embed the enhancers within strategies. If you have the time, perhaps a top level stepwise implementation path would be useful to follow.

Copy link
Member

Choose a reason for hiding this comment

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

Add option to disable all enhancers on rest server? Alternatively option to override by providing array list of enhancers to apply?

I am proposing to leave this part out of the initial implementation and wait until there is user demand for that.

Would I follow similar pattern to authentication strategies say spec-enhancers folder alongside or would I seek to embed the enhancers within strategies. If you have the time, perhaps a top level stepwise implementation path would be useful to follow.

IMO, it's not a concern of this pull request how the spec enhancer classes are registered.

I am expecting the @loopback/authentication extension to contribute a binding with @bind(asSpecEnhancer) decorator. As I understand the current implementation of applyAllEnhancers, it will automatically pick up such class.

See my ModelApiBooter spike in #3617 for an example. I really wish we had better documentation about the ExtensionPoint/Extension pattern :-/

Here the app decides to register a component (it's CrudRestComponent in the spike, it would be @loopback/authentication in your case):

https://github.com/strongloop/loopback-next/blob/d8fe4736fd07fc9da34ed345cbb77bdbd761b752/examples/todo/src/application.ts#L29

Here the component implements an Extension:

https://github.com/strongloop/loopback-next/blob/d8fe4736fd07fc9da34ed345cbb77bdbd761b752/packages/rest-crud/src/crud-rest-builder.plugin.ts#L30-L31

And here is the Extension implementation contributed by the Component to the Application context:

https://github.com/strongloop/loopback-next/blob/d8fe4736fd07fc9da34ed345cbb77bdbd761b752/packages/rest-crud/src/crud-rest.component.ts#L9-L11

@dougal83
Copy link
Contributor Author

@bajtos I've cut this PR down down to a singular purpose. Please let me know if it is back on track.

@dougal83

This comment has been minimized.

@dougal83 dougal83 force-pushed the add-oas-enhancer-service branch from e633dd9 to 2da2b42 Compare February 18, 2020 09:16
@dougal83 dougal83 changed the title Add OpenAPI enhancer service in @loopback/rest feat(rest): add OAS Enhancer service Feb 20, 2020
@dougal83 dougal83 requested a review from raymondfeng February 20, 2020 23:19
@jannyHou
Copy link
Contributor

@dougal83 @bajtos got distracted by the auth migration last week, sorry for replying late.

Let me clarify the goal first:

  • For Add bearer auth scheme as the default security scheme #4386, the ultimate goal is to enable the "authorize" button in the explorer out-of-box in a LB4 app, and the default scheme(bearer auth scheme) should be the spec snippet I post in story description. Do we still agree on it? If yes, we can discuss how to do it.

  • For Add OpenAPI enhancer service in @loopback/rest #4380, the ultimate goal is to add an OAI enhancer service in the rest server, regardless of what enhancers it picks up by OAIEnhancer.applyEnhancerByName() or OAIEnhancer.applyAllEnhancers(), and where those enhancers are applied(inside getApiSpec or in an app's constructor).

And if we still agree on ^, let's discuss each module's responsibility. I think I somehow misunderstand the expected approaches and my understanding has been "@loopback/rest provides the bearer auth scheme enhancer" and "it should be applied inside the rest server class, like the consolidation enhancer added in #4365" all the time. If this is not the expected behaviour anymore, first sorry for misleading @dougal83 on the implementation... then let's agree on which module provides the enhancer and where to apply it.

I am expecting the @loopback/authentication extension to contribute a binding with @Bind(asSpecEnhancer) decorator. As I understand the current implementation of applyAllEnhancers, it will automatically pick up such class.

@loopback/authentication is good to me. And yes applyAllEnhancers would automatically pick up it. Do we want to call applyAllEnhancers inside the rest server class? inside getApiSpec or _serveOpenApiSpec? Or in the app constructor?

When created the story, I didn't realize applying the enhancer would turn the getApiSpec into an async function. I tend to keep it sync and apply the default security enhancer somewhere else, like _serveOpenApiSpec

If we modify the app template and apply the enhancers in app constructor, then we don't have the concern above. WDYT?

@dougal83
Copy link
Contributor Author

@jannyHou Thank you for the reply. Don't worry about misleading, it's more likely my misunderstanding. :)

From looking at the stories and attempting PRs, it does make sense to have the rest server apply the extensions by default via applyAllEnhancers but you make a good alternative suggestion. 👍

From comments made I also attempted to extend the AuthStrategies provider to contribute the OASEhancer. Love to hear your thoughts on the idea!

@raymondfeng
Copy link
Contributor

@dougal83 The PR looks good to me now. Would you please add a test to verify that an enhancer is invoked as part of getApiSpec() call?

@dougal83 dougal83 force-pushed the add-oas-enhancer-service branch from 2da2b42 to f572622 Compare February 24, 2020 19:49
BREAKING CHANGE: Api specifications are now emitted as a Promise instead
of a value object.  Calls to getApiSpec function must switch from
the old style to new style as follows:

1. Old style

```ts
function() {
  // ...
  const spec = restApp.restServer.getApiSpec();
  // ...
}
```

2. New style

```ts
async function() {
  // ...
  const spec = await restApp.restServer.getApiSpec();
  // ...
}
```
add openapi spec enhancer to rest server

impl. loopbackio#4380

Signed-off-by: Douglas McConnachie <[email protected]>
@dougal83 dougal83 force-pushed the add-oas-enhancer-service branch from f572622 to 7558cee Compare February 24, 2020 22:05
@dougal83 dougal83 mentioned this pull request Feb 25, 2020
3 tasks
@dougal83
Copy link
Contributor Author

@bajtos @raymondfeng Please let me know if further changes are required.

@dhmlau dhmlau added this to the Mar 2020 milestone Mar 3, 2020
Copy link
Contributor

@jannyHou jannyHou left a comment

Choose a reason for hiding this comment

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

👏 LGTM Good to apply the security spec in a separate PR. And really appreciate your effort spent on the OAS enhancer stories. I am looking at the other PRs.

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.

The new version looks great 👏 Thank you @dougal83 for the persistence in pushing this change forward, despite long silences on our side ❤️

@@ -229,6 +238,16 @@ export class RestServer extends Context implements Server, HttpServerLike {
this.bind(RestBindings.HANDLER).toDynamicValue(() => this.httpHandler);
}

protected _setupOASEnhancerIfNeeded() {
if (this._OASEnhancer != null) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: we could use this._OASEnhancer = this.getSync(OAS_ENHANCER_SERVICE, {optional: true}); to check so that the service can be bound to the server.

@raymondfeng raymondfeng merged commit 62d55eb into loopbackio:master Mar 5, 2020
@dougal83 dougal83 deleted the add-oas-enhancer-service branch March 5, 2020 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants