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

SOAP policy #567

Merged
merged 11 commits into from
Feb 9, 2018
Merged

SOAP policy #567

merged 11 commits into from
Feb 9, 2018

Conversation

davidor
Copy link
Contributor

@davidor davidor commented Jan 30, 2018

This policy adds support for a very small subset of SOAP.

This policy basically expects a SOAPAction URI in the SOAPAction header or the content-type header.
The SOAPAction header is used in v1.1: https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383528, whereas the Content-Type header is used in v1.2: https://www.w3.org/TR/soap12-part2/#ActionFeature

The SOAPAction URI is matched against the mapping rules defined in the configuration of the policy and the usage resulting from that is authorized and reported against 3scale's backend.

@davidor
Copy link
Contributor Author

davidor commented Jan 30, 2018

@mikz I didn't add a JSON schema. I'll wait until #565 is merged.

local soap_usage = context.service:get_usage(
ngx.req.get_method(), soap_action_uri)

context.add_to_usage = soap_usage
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mikz I considered several options here but this seemed the cleanest one. We could pass the URI instead and do the mapping rules matching later.

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder how several policies would interact. Or if this would be twice in the chain.

My idea was to make each policy to increment the values in the context. That way the policy can decide what to do: add, replace, remove, ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely. If in the future we have more policies doing the same thing, we'll need to update add_to_usage instead of simply assign it.

Copy link
Contributor

Choose a reason for hiding this comment

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

What I mean is that we can just context.usage.hits = (context.usage.hits or 0) + 1 in the rewrite phase. Or expose some table merger function to merge those. Or have them in an array and merge them in the proxy:access. It just does not feel right to have add_to_usage when the usage key is there too.

end
end

function _M:access(service)
function _M:access(service, context)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mikz I decided to pass the context instead of passing just the usage to be increased because in the future there might be other policies passing other information in the context. This way we won't need to change the signature of the access() method again.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure. https://github.com/3scale/apicast/pull/556/files#diff-31313c92616b54028dac1ba183e6f79aR287 was all the info it needed. It is quite a lot of params, but not that bad imo.

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'm OK with adding 2 or 3 extract params. I'm more concerned about changing the signature of access() in the future and breaking backwards compatibility.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. Maybe we can build on #556 and try to figure out out there and then just use it from here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good 👍

-- This policy adds support for a very small subset of SOAP.
-- This policy basically expects a SOAPAction URI in the SOAPAction header or
-- the content-type header.
-- The SOAPAction header is used in v1.1:

Choose a reason for hiding this comment

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

"....v1.1 of the SOAP Standard"
(just to avoid any confusion in readers about versions of apicast or something?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

-- https://www.w3.org/TR/soap12-part2/#ActionFeature
-- The SOAPAction URI is matched against the mapping rules defined for the
-- service and the usage resulting from that is authorized and reported
-- against 3scale's backend.

Choose a reason for hiding this comment

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

"...and reported to the API Management platform using the Service Management API" or similar?

Copy link
Contributor Author

@davidor davidor Jan 31, 2018

Choose a reason for hiding this comment

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

I usually refer to backend as '3scale's backend', sounds simpler than that, but would be good to reach an agreement on that. Should we call it by its new upstream name?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd call it 3scale's backend. Definitely try not to write its full name because I could not get it right and have to copy paste it from somehwere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed @mikz

local _M = policy.new('SOAP policy')

local soap_action_header = 'SOAPAction'
local soap_action_ctype = 'application/soap+xml?action='
Copy link
Contributor

@mikz mikz Jan 31, 2018

Choose a reason for hiding this comment

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

I think this has to be parsed as URI args. Because we can't control the action appears right after as it can have & and be the last param.

Also quick google search shows some people use ; to concat params. We should verify if that can really be the case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right @mikz . I wrongly assumed that action would be the only param.

Copy link
Contributor Author

@davidor davidor Feb 7, 2018

Choose a reason for hiding this comment

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

About using ;, I think we need to support that instead of ?...&... :
https://developer.mozilla.org/es/docs/Web/HTTP/Headers/Content-Type and https://tools.ietf.org/html/rfc1341

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep. https://tools.ietf.org/html/rfc7231#section-3.1.1.1 says parameters are separated by ; and https://tools.ietf.org/html/rfc3902 says it is a parameter. I wonder where I've seen the example with ?. Can't find it now.


if soap_action_uri then
local soap_usage = context.service:get_usage(
ngx.req.get_method(), soap_action_uri)
Copy link
Contributor

Choose a reason for hiding this comment

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

So this is using service mapping rules? I'm not sure users can define full URLs there.
I though we will have some mapping rules in this policy.

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 assumed it was possible to define something like /a_path#some_action in the mapping rules. That's why I decided to do it this way. I see now that it's not possible. With that constraint, I think we need to include mapping rules in the configuration of this policy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mikz Let me know what you think about this solution:

  • Add a mapping rules array in the policy config. Each element will contain: pattern, http_method, metric_name, and delta. The same fields we have for the service mapping rules.
  • Add a add_mapping_rule() method in Service. This would simply accept the fields above, add the ones needed to construct the rule, and then add it to the self.rules table. We'll need to make sure that add_mapping_rule() is only called once during the lifetime of the policy so we don't add duplicated rules.
  • The above will let us call the existing Service:get_usage(). This method returns the metrics to auth and report based on the mapping rules defined. We'll call this method only when we receive a SOAP action.

Copy link
Contributor

Choose a reason for hiding this comment

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

@davidor interesting idea. I'm not sure if I like the idea of merging then into one list. I think they should be applied separately. IMO there could be some rules that match both and because some are absolute and some relative it could be quite nasty as / would increment hits all the time and twice.
Maybe we need to exposeUsageExtractor:get_usage which is initialized with list of mapping rules. Basically exposing the low level API where you can inject own mapping rules.

Copy link
Contributor Author

@davidor davidor Feb 1, 2018

Choose a reason for hiding this comment

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

I get your first point, but wouldn't that be coherent with the way mapping rules work currently?

Suppose that the policy adds this mapping rule: /a_path#a_soap_action => +1 a_soap. If the user has already defined /a_path => +1 a_path, we should increase both a_soap and a_path by 1. If the user already defined / => +1 hits, and hits is a parent metric of the other 2, well, it's going to be increased by 3(!), but that's how it currently works no?

Regarding your second suggestion about exposing UsageExtractor:get_usage, could you please expand a bit on it or provide some pseudo-code or examples?

Copy link
Contributor

@mikz mikz Feb 1, 2018

Choose a reason for hiding this comment

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

I think the issue is when you have following rules:

/ => hits
/a => a

And in soap you'll have
http://example.com/soap#a => a

If you match http://example.com/soap#a it will return hits=1&a=1.

So it depends if this replaces mapping rules or complements them.
If it would first match mapping rules and then soap rules and merged the results then for a request:

GET /
Content-Type: application/xml+soap?action=http://example.com/soap#a

it would match hits=2&a=1. Which is wrong IMO. Because the path mapping rules should not be applied to soap mapping rules (because they are relative and not absolute).
I'm not thinking about parent/child metrics, just about applying mapping rules to the request path/ soap action.
The point is that current path relative mapping rules like /a can unintentionally match soap services like http://example.com/a and double counting. And also match different services like http://example.com/a and http://foobar.com/a.

And the signature of UsageExtractor would be quite simple.

local usage_rules = UsageExtractor.new({
  { pattern = 'http://example.com/a', http_method = 'GET', metric_name = 'a', delta = 1 }
})
local usage = usage_rules:get_usage(...)

And the Service:get_usage would use this too. But we would just have an API to create own independent list of rules.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mikz Thanks for the example.

It's clear to me now why you think that a url received via SOAP Action should not be matched against the service mapping rules. It should only be matched against the mapping rules specified in the SOAP policy. I agree with that.

Now, what happens when the SOAP policy is enabled but a request does not contain a SOAP Action ? I think we should apply the service mapping rules in that case.

I like the idea of extracting a UsageExtractor module from Service and Configuration. This is something I've been thinking about. Those 2 modules have many responsabilities and need to be splitted. This seems to be a good excuse to do it :) I'll address this task in a separate PR.

@davidor davidor force-pushed the soap-policy branch 2 times, most recently from ab3ec72 to fd7c54f Compare January 31, 2018 16:50
@davidor davidor changed the title SOAP policy [WIP] SOAP policy Feb 7, 2018
@@ -0,0 +1 @@
return require('soap')
Copy link
Contributor

Choose a reason for hiding this comment

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

So I started to think about this. And I think we can expose some support tooling in different module, so it can be unit tested.

Lets say the main policy code is in soap_policy.lua. Then there can be soap.lua that has stuff like "extracting the soap action" and it can be unit tested in busted.

That would allow us not exposing extra methods on the policy, but still exposing it internally (if the loading works) for tests. And policies should not be able to load other policies (but that is not enforced yet), so we should be fine and the code should be used only from tests.

Just some food for though. I'd like to hear your take.

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 would be a nice improvement.

Otherwise, in some cases Proxy reports the usage associated with the
service mapping rules instead of the merged one.
@davidor davidor changed the title [WIP] SOAP policy SOAP policy Feb 8, 2018
@davidor
Copy link
Contributor Author

davidor commented Feb 8, 2018

This is ready @mikz . There have been a few changes. Mainly because the code is now using the modules extracted in #571 , #573 , and #580 .

I also addressed your comments.


local function usage_from_matching_rules(soap_action_uri, rules)
return mapping_rules_matcher.get_usage_from_matches(
nil, soap_action_uri, {}, rules)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mikz Notice that I'm sending http_method = nil here. Not sure if we should take it into account for SOAP actions.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm. SOAP can be used with GET and POST.
Aha! The GET does not have the Content-Type header. So technically it should be only POST. I guess.

But it looks like the GET is used as some REST hybrid, so I guess we could leave that to the mapping rules.
I'd possibly even go for hardcording POST there.

Copy link
Contributor Author

@davidor davidor Feb 8, 2018

Choose a reason for hiding this comment

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

I don't really know about this. The Content-Type is included in the request, not in the SOAP action URI.
Depending on what we decide here we'll need to add it to the JSON schema also. Notice that for now, I only included 'pattern', 'metric', and 'delta', and left out other fields that are present in the proxy rules.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. We are initializing the mapping rules directly from the config. I was thinking that we could just override the http_method attribute with POST there. And then pass the real http method here. So we match it only when the request is POST.
But it is probably not really important.

}
end

soap_policy:rewrite(context)
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh. This is so good 🥇 Great we can unit test this.

local function soap_action_in_ctype(headers)
local ctype = headers['Content-Type']

if ctype and starts_with(ctype, soap_action_ctype) then
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this won't be that easy 🤔
The RFC says those are all equivalent:

text/html;charset=utf-8
text/html;charset=UTF-8
Text/HTML;Charset="utf-8"
text/html; charset="utf-8"

And looks like it can have whitespace before ; too. Look at the definition: media-type = type "/" subtype *( OWS ";" OWS parameter ). And OWS is optional white space.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Check last commit.

local name = header_param_split[1]
local value = header_param_split[2]
if name == "action" then
return value
Copy link
Contributor

Choose a reason for hiding this comment

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

The value can be either "url" or just url. We should strip the quotes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is also solved in the new commit.

local function soap_action_from_ctype_params(params)
local params_split = re.split(params, ";")
local params_without_blanks = ngx.re.gsub(params, '\\s', '')
local params_split = re.split(params_without_blanks, ";")
Copy link
Contributor

Choose a reason for hiding this comment

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

You could also split by [[\s*;\s*]] to remove spaces around ;, right?
Seems a bit evil to strip all spaces from it. What if there is a space in the url? Now when I think about it.
What happens when there is ; in soap action? That would be quite broken.

if ctype and starts_with(ctype, soap_action_ctype) then
-- The Content-Type can be a mix of upper and lower-case chars. Convert it to
-- include only lower-case chars to be able to compare it.
if ctype and starts_with(lower(ctype), soap_action_ctype) then
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this would not work for application/xml+soap ; action="" because of the whitespace.

local policy_config = {
mapping_rules = {
{
pattern = '/soap_action$',
Copy link
Contributor

Choose a reason for hiding this comment

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

Because the RFC says these should be full URL we should test also with full URLs. To verify all the escaping works.

Copy link
Contributor

@mikz mikz left a comment

Choose a reason for hiding this comment

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

👍 excellent 🥇

@davidor davidor merged commit 272aa57 into master Feb 9, 2018
@davidor davidor deleted the soap-policy branch February 9, 2018 09:31
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.

3 participants