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 support for "securityDefinitions" #40

Merged
merged 5 commits into from Jul 10, 2017
Merged

Add support for "securityDefinitions" #40

merged 5 commits into from Jul 10, 2017

Conversation

@jhthorsen
Copy link
Owner

@jhthorsen jhthorsen commented Jun 12, 2017

I have no idea if this should be merged, so I thought you could vote/decide.

What do you think?

@kivilahtio
@chandwki

Sidenote to @chandwki: You can now look up the spec in under() 12baad9

Sidenote to @leejo: Would it be cool to make a helper/extension that would do OAuth2 using this logic?

@jhthorsen jhthorsen self-assigned this Jun 12, 2017
@jhthorsen jhthorsen requested review from leejo, mrenvoize and jberger Jun 12, 2017
@jhthorsen jhthorsen mentioned this pull request Jun 12, 2017
$wrapper = sub {
my $c = shift;
my $cb = shift @cc || sub { die 'No security handler defined' };
$c->$cb(shift @cc, $wrapper);

This comment has been minimized.

@jhthorsen

jhthorsen Jun 12, 2017
Author Owner

I was also considering doing:

# App wide, instead of plugin wide:
$c->openapi->security->dummy($config, $wrapper);

# Could use something like "openapi_security_dummy" by default, but then be overridden by "x-mojo-helper" or something.
$c->$helper_name($config, $wrapper);

The reason why I want to use helpers is because I feel a "callback hash" does not encourage re-use.

This comment has been minimized.

@kivilahtio

kivilahtio Jun 13, 2017

I am sure helpers are slick, especially since you use them in their own pseudo-namespace "openapi_security_dummy".
However I find helpers to be very confusing and I remember struggling to understand from where these cryptic $c->$helper_name -things spawn from?

Since you have a specific use-case for the "Security Definitions Object"-implementations it would be more clear to the end-user to find those implementations in
$c->openapi->security->dummy($config, $wrapper);

You can access the App from most everywhere anyway.

This comment has been minimized.

@jhthorsen

jhthorsen Jun 13, 2017
Author Owner

@kivilahtio: Both $c->openapi->security->dummy and $c->openapi_security_dummy are helpers.

@@ -85,6 +87,9 @@ sub _add_routes {
if (@parameters) {
$op_spec->{parameters} = [@parameters, @{$op_spec->{parameters} || []}];
}
if ($op_spec->{security}) {

This comment has been minimized.

@jhthorsen

jhthorsen Jun 12, 2017
Author Owner

Missing inheritance, meaning you can't set "security" globally in the API spec.

This comment has been minimized.

@kivilahtio

kivilahtio Jun 13, 2017

You have inheritance for path parameters, using the same logic you can implement inheritance for the global "Security Requirement Object" (http://swagger.io/specification/#securityRequirementObject) ?

Not sure how important this is for us, since we have a separate scope for each operation object anyway, which overloads the global "Security Requirement Object".
To me, global inheritance doesn't look very difficult to add, so I would recommend adding it for the sake of specification completeness.

This comment has been minimized.

@jhthorsen

jhthorsen Jun 13, 2017
Author Owner

Yeah, I just mentioned it so I don't forget in case this should be merged :)

dummy => sub {
my ($c, $config, $next) = @_;
return $c->$next if $c->req->headers->authorization;
return $c->render(json => $config, status => 401);

This comment has been minimized.

@jhthorsen

jhthorsen Jun 12, 2017
Author Owner

I kind of like the idea of calling $next or needing to render a document.

Need to fix typo. Should be:

return $c->render(openapi => $config, status => 401);

This comment has been minimized.

@kivilahtio

kivilahtio Jun 13, 2017

I agree.
You must make it explicit in the documentation that this is expected and have error checking if somebody improperly forgets to render on auth failure.
Using clear error descriptions please.

This comment has been minimized.

@jhthorsen

jhthorsen Jun 13, 2017
Author Owner

"Using clear error descriptions please"..? What do you mean? You decide the document to render. You can do this if you like:

return $c->render(json => {sorry => "you don't have access"}, status => 401);

The way to mark the user as authenticated or authorised is by calling $c->$next

This comment has been minimized.

@kivilahtio

kivilahtio Jun 13, 2017

Yes.
My point was to catch the programmer error where one doesn't render anything by mistake or by not understanding the documentation, if the auth fails, and the $c->$next is not called.

I just wanted to make sure that the programmer gets some other error than "nothing rendered" if he doesn't understand that you must render something on a failed authentication. "nothing rendered" is really really vague and tracking the source of this problem takes ages.

This can prolly be dealt with in _security_action() ?

This comment has been minimized.

@jhthorsen

jhthorsen Jun 13, 2017
Author Owner

No, since if you don't call render() or $next, then mojo will just sit there and nothing gets rendered. Just like you expect from a standard non-blocking under route. I could possibly add delay(), but I don't want to because it adds complexity that doesn't add anything from the standard mojo behaviour.

This comment has been minimized.

@kivilahtio

kivilahtio Jun 13, 2017

So what happens then if next is not called and nothing is rendered?
The client timeouts?

This comment has been minimized.

@jhthorsen

jhthorsen Jun 13, 2017
Author Owner

Yes. And the server. "inactivity timeout".

$wrapper = sub {
my $c = shift;
my $cb = shift @cc || sub { die 'No security handler defined' };

This comment has been minimized.

@kivilahtio

kivilahtio Jun 13, 2017

The fatal exception is too vague.
It should state something like:
"You haven't defined any 'Security Definitions Object'-implementations as a OpenAPI-plugin 'security'-parameter"

This way one immediately knows where to look for what is missing.
Otherwise one must dig through the whole codebase to figure out why this error is given.

return sub {
my $c = shift;
my @cc
= map { my ($name, $config) = %$_; ($security_cb->{$name}, $config); } @$security_settings;

This comment has been minimized.

@kivilahtio

kivilahtio Jun 13, 2017

I presume:
-2nd parameter of security settings is the subroutine implementing a "Security Scheme Object"? (http://swagger.io/specification/#securitySchemeObject)
-1st parameter is the name of the "Security Scheme Object", in this test case it would be 'dummy'?

The naming of the variable $config is obscuring. It should explicitly state that it is a anonymous subroutine, even better, of the type "Security Scheme Object"-implementation.
Maybe something like
$securitySchemeObjectImplementation

This comment has been minimized.

@jhthorsen

jhthorsen Jun 13, 2017
Author Owner

$config is not a callback. In this case, $name is "dummy" and $config is []. You don't get the "securityDefinitions" object. You get the data from "security": [{"dummy": []}], meaning it will always (I think) be an empty array, except if you work with oauth2.

This comment has been minimized.

@kivilahtio

kivilahtio Jun 13, 2017

Oh yeah, the "Security Scheme Object"-implementation is referenced via $security_cb->{$name} and the $config is actually the "Security Requirement Object".
This discovery doesn't change the fact that the variable should be named to mean what it is, in this case:
$securityRequirementObject
Had it been so, we wouldn't have had this discussion :)

@kivilahtio
Copy link

@kivilahtio kivilahtio commented Jun 13, 2017

Sidenote to @\leejo: Would it be cool to make a helper/extension that would do OAuth2 using this logic?

It would.
@mrenvoize is prolly interested in this as well.

@leejo
leejo approved these changes Jun 13, 2017
Copy link
Collaborator

@leejo leejo left a comment

looks fine with current discussions

@leejo
Copy link
Collaborator

@leejo leejo commented Jun 13, 2017

Sidenote to @leejo: Would it be cool to make a helper/extension that would do OAuth2 using this logic?

In the security callback you should be able to use it with Mojolicious::Plugin::OAuth2::Server as is:

return $c->$next if $c->oauth( $optional_scope );
@mrenvoize
Copy link
Collaborator

@mrenvoize mrenvoize commented Jun 15, 2017

Still struggling to find time to properly dig into this, but in a quick excursion around it, it's showing allot of promise :)

@jhthorsen jhthorsen force-pushed the security branch from e7c6da0 to e6e7edf Jun 28, 2017
@@ -83,6 +85,9 @@ sub _add_routes {
if $op_spec->{operationId} and $uniq{o}{$op_spec->{operationId}}++;
die qq([OpenAPI] Route name "$name" is not unique.) if $name and $uniq{r}{$name}++;

if ($op_spec->{security}) {
$route = $route->under('/')->to(cb => $self->_security_action($op_spec->{security}));

This comment has been minimized.

@jberger

jberger Jun 28, 2017
Collaborator

The per-op under can't replace the global route. That would cause the security definitions to build up as you generate more routes!

@jberger jberger force-pushed the security branch from e6e7edf to e78d87b Jul 6, 2017
@@ -62,6 +64,10 @@ sub _add_routes {
$route_prefix = "$spec_route_name.";
}

if ($api_spec->get('/securityDefinitions')) {
$route = $route->under('/')->to(cb => $self->_security_action($api_spec));

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

👍

return sub {
my $c = shift;
my @security = @{ $c->openapi->spec->{security} || $global };

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

Are you sure that $global shouldn't get merged with the spec security?

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

No, it is overridden entirely. This is confirmed by the fact that the documentation says explicitly that a set of global security policies can be ignored by setting the op security to an empty array

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

Awesome! Thanks 👍

my $name = join '|||', $req, sort @$scopes;
my $check = [$name, $security_cb->{$req}, $definitions->{$req}, $scopes];
if (exists $cache{$name}) {

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

I don't get this part. Is it pre-optimization?

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

Perhaps, but I wouldn't want to have checks run multiple times if they occur in multiple OR conditions.

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

Can you write a test/spec where this happens? I think this whole block makes the code harder to read, so I don't want it unless it has any effect on "the real world".

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

The cache test does this already. It defines two different ORed checks each that require pass1. The first also requires fail1 which fails and therefore the second check is attempted. Once that one succeeds and renders, it checks the hash of number of times that it was called, which thus tests that pass1 was only called once.

push @checks, $check;
}
Mojo::IOLoop->delay(

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

Why don't you use $c->delay?

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

Perhaps I should, though that gives less flexibility in how the 500 is rendered. Maybe the logic from that should be copied though (wrt keeping the transaction alive).

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

How come? There's already a openapi 500 handler in place. I think that should be sufficient, and a lot better than no error handling.

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

Will calling $c->reply->exception as the delay helper does work correctly in the plugin? If so then sure, it should definitely do that.

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

Works like a charm. That's what my tests are doing now, becuase of https://github.com/kraih/mojo/blob/master/lib/Mojolicious/Plugin/DefaultHelpers.pm#L84

$delay->catch(sub { $c->helpers->reply->exception(pop) and undef $tx })->wait;
# otherwise perform the check
my $end = $delay->begin(0);
$c->$action($def, $scopes, sub { $end->($cache{$name} = shift) });

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

I'm not sure if it's a good idea to run the checks in parallell. Think it would be much easier to implement a serial checking mechanism.

Maybe I misunderstand something?

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

If they are going to be asynchronous anyway, I don't see why they can't be in parallel.

security => {
dummy => sub {
my ($c, $definition, $scopes, $cb) = @_;
return $cb->(1) if $c->req->headers->authorization;

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

I want $cb to be get $c as the first argument.

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

I can agree with $c as the first argument if you want it. That said it doesn't add any value just consistency.

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

I want, hehe. Then we can kill the begin(0) above as well, which makes the code easier to read.

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

I will just become begin since I still would need to cache the result.

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

Heh, actually it doesn't actually remove the begin(0) after all, just changes the shift to a $_[1].

dummy => sub {
my ($c, $definition, $scopes, $cb) = @_;
return $cb->(1) if $c->req->headers->authorization;
return $cb->(0);

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

I was thinking $c->render would be issue a 401, but that doensn't work, since there is an OR between the checks...

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

My original response was

How can you render from a failing condition. You might have another ORed check that would cause a success. I think there has to be a security_failed event (or so) to handle when the routing is complete.

A possible alternative would be an arrayref that contains the failed results. If the array is empty at the end of a check then it succeeds. Values could then be accumulated and rendered as a standard JSON error response.

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

How about reversing the code, so we could do something like the code below? That way, we could use the first error, merge them or pass the errors on to some other handler at the end.

$c->$cb({error => "You shall not pass!"});

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

That is kinda what I was saying in my second paragraph above. In this case I guess we have them call $c->$cb() or $c->$cb(undef) to indicate no error (ie. pass). We would need to consider what they should return. I'd think probably an error string or else an array of error strings. Then if no success is given the result could be the standard error document where each failed condition is an error in the errors object (perhaps even with the path to the security check that failed).

};

my $t = Test::Mojo->new;
subtest 'global' => sub {

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

I really don't like subtests.

This comment has been minimized.

@jberger

jberger Jul 6, 2017
Collaborator

Why not? These tests are grouped logically. I need a block anyway for the local on the diagnostic hash. I won't fight you on it though.

This comment has been minimized.

@jhthorsen

jhthorsen Jul 6, 2017
Author Owner

It's just a matter of taste. I argued with @marcusramberg about perltidy for two years, and now I can't live without it... Maybe the same will happen with subtest, but not tonight 👎

@jhthorsen jhthorsen force-pushed the security branch from c067f7e to ea96657 Jul 10, 2017
@jhthorsen jhthorsen force-pushed the security branch from ea96657 to 26fcc14 Jul 10, 2017
@jhthorsen jhthorsen changed the title Add basic support for "securityDefinitions" Add support for "securityDefinitions" Jul 10, 2017
@jberger jberger merged commit a0db237 into master Jul 10, 2017
2 checks passed
2 checks passed
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

5 participants
You can’t perform that action at this time.