-
Notifications
You must be signed in to change notification settings - Fork 30
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
Implement object argument type #77
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a powerful feature. As such it warrants careful review, especially from a security point-of-view.
This seems well thought out, but I am concerned about the unchecked ability to modify the object structure from what could very well be user-supplied data. Eg. for an object x
, where a postcode
property with a number
is expected, this input could potentially cause unexpected issues:
--x.postcode '{ "toString": null }'`
I don't see a way to safely implement this feature without requiring a schema along with a hookable validator (eg. for full Joi validation).
The |
Thanks for the review!
This crossed my mind, but I figured that the user should perform application-level validation once they obtain the parsed arguments. Even the basic arguments could have this issue: perhaps your calculator CLI needs
If the input does not look like JSON then the value will be taken as a string. I will note this in a code comment in the |
Ah, I missed the parsing fall-through. I would definitely oppose that part of the feature. The convenience is by far outweighed by the unexpected side-effects, like the names This brings me back to the only way to do this safely, is full schema support. Using Validate/Joi, would also allow for nice error messages. The only part that I guess could work without it, is non-fallthrough plain JSON object support without any merging. But then it seems like a non-feature, as the app could just do the parse. |
I would just like to add, that I have previously used Joi for validating command lines with minimist doing the basic parsing, which worked quite well. |
Those particular keywords wont resolve as strings, but not because of the pass-through: it's the parsing itself that makes them not strings. Do you have an issue with the pass-through (if so, any thoughts on what the behavior should be for non-JSON? An error?) or these keywords not being treated as strings (if so, would you recommend we parse objects and arrays but not simple values? how would a user set a value to
What is it about merging that is special? I think this feature would still be plenty useful if it performed deep assignments rather than deep merges and I would be cool making that change if it quells your concerns.
I am having trouble following exactly why this feature necessitates full schema support in bossy— could you break it down a little bit more? I do follow that the app consuming bossy output cannot make any assumptions about the contents of an object arg (aside from the fact that it is an array or object), which seems acceptable to me.
Totally! I would expect users of bossy to do the exact same thing: use bossy for the basic parsing and then validate the result with something like Joi. Minimist allows unstructured input (including deep objects, though no JSON parsing), so applications consuming minimist output should perform validation on the result, and the same goes for bossy. I genuinely look forward to hearing your perspective on these questions, but I realize we might not end in full agreement and could need to solicit some additional review. Perhaps someone will have some new ideas for us |
I see the value of what you are trying to do, and appreciate that you want to accommodate my thoughts. While I am by no means a security researcher, I have a keen interest in security, and have found and reported several issues. As such I see potential problems if someone decides to invoke a tool with this feature as part of some automated process. Specifically, it is possible that someone will pass a user-supplied value to a specific field (eg. One compromise that might work without a schema, is to only support strings for the path-based merging. Ie. |
I definitely don't have as much security knowledge as you @kanongil but I feel with that compromise we'll make both the feature and the code more complicated for what I think should be the developer responsibility. IMO that's not because someone uses I'd be more inclined to add the feature as @devinivy intended. |
@Nargonath What I propose is actually aligned with (at least one likely interpretation of) the included docs. How do you expect developers to know that passing Also, where is the extra complexity? My proposal would require one |
I consulted @nlf for feedback due to his background in security. I will try to represent his thoughts accurately.
I thought through all the feedback so far, and came-up with a plan that I think at least speaks to the issues that have come-up, including validation:
I hope this may do the trick for us! |
Sorry for the poke, but @nlf @kanongil @geek @Nargonath if any of you have time to take a peek at the latest updates that would be awesome and appreciated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still feel there are security gotchas with parsePrimitives: false
, but it is better than before.
@@ -50,33 +50,82 @@ include the usage value formatted at the top of the message. | |||
Options accepts the following keys: | |||
* `colors` - Determines if colors are enabled when formatting usage. Defaults to whatever TTY supports. | |||
|
|||
### `object(name, parsed)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new method is nice and explicit.
API.md
Outdated
* `valid`: A value or array of values that the argument is allowed to equal. | ||
* `valid`: A value or array of values that the argument is allowed to equal. Does not apply to `json` type arguments. | ||
|
||
* `parsePrimitives`: When `true`, arguments of the `json` type will parse JSON primitives rather than treat them as strings. For example, `--pet.name null` will result in the output `{ pet: { name: 'null' } }` by default. However, when `parsePrimitives` is `true`, the same input would result in the output `{ pet: { name: null } }`. The same applies for other JSON primitives too, i.e. booleans and numbers. When this option is `true`, users may represent string values as JSON in order to avoid ambiguity, e.g. `--pet.name '"null"'`. It's recommended that applications using this option document the behavior for their users. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making this an option means that it will be much safer and there will be less surprises. I don't feel that it properly describes that the value will fallback to the string value if the parsing fails.
Maybe this could have another parsePrimitives: 'strict'
mode, where there is no fallback?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will update the docs to make it clearer 👍 and I will look into parsePrimitives: 'strict'
. If I do implement 'strict'
then it would parse primitives strictly and only allow primitives, otherwise return an error.
LICENSE.md
Outdated
@@ -1,5 +1,5 @@ | |||
Copyright (c) 2014-2020, Sideway Inc, and project contributors | |||
Copyright (c) 2014, Walmart. | |||
Copyright (c) 2014-2021, Sideway Inc, and project contributors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is outside the scope of this patch. And should it really extend "Sideway Inc" copyright like that? Hasn't it been transferred away?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to extend the copyright to project contributors (i.e. myself with this patch) into 2021. I will add it on a new line to represent the fact that Sideway hasn't contributed in 2021.
let jsonDef; | ||
|
||
if (opt.includes('.')) { | ||
const maybeDef = keys[opt.split('.')[0]]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not work with 'json' objects defined at any depth but 0. Eg. for a definition like this:
'pet.owner': { type: 'json' }
I don't know if that should supported, but I don't see any docs, tests, or checks against it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Supporting this causes some usability and predictability issues (esp. with Bossy.object()
), so it is not supported at this time. It's enforced by Joi, and Bossy.object()
has an assertion that relates to this issue. The test for it is here: https://github.com/hapijs/bossy/pull/77/files#diff-5bb8db779819ddef5956a5d9d5949c05ef7445237656ca37bf2f02720271440bR465-R474
lib/index.js
Outdated
Bounce.ignore(err, SyntaxError); | ||
} | ||
|
||
if (!last.def.parsePrimitives && (!value || typeof value !== 'object')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you want to always parse objects & arrays? It means that even with parsePrimitives: false
, someone that expects a simple string could crash the app with {"toString":null}
, possibly producing unexpected results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone is looking to guarantee they receive a string input rather than json input then they would want to use a dot-separated string
-type arg, then optionally use Bossy.object()
to roll it up into an object. This can even be combined with a json
-type arg to get the best of both worlds and sidestep any unpredictability if the application would like to guarantee certain deep properties be strings. If the application wants a string but doesn't know the deep property at which they require that string, then I think it's fair that they will need to validate this themselves.
|
||
flags[name].push(value); | ||
} | ||
else if (last.def.type === 'json') { | ||
Hoek.merge(flags[name], value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a default value was provided, the merge will modify it. I suspect the default value will need to be cloned to avoid surprises.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each definition is cloned near the top of Bossy.parse()
, and I do have a test confirming that defaults aren't mutated, so I think we're good 👍 https://github.com/hapijs/bossy/pull/77/files#diff-5bb8db779819ddef5956a5d9d5949c05ef7445237656ca37bf2f02720271440bR362
Hoek.assert(!opt.includes('.'), `Cannot build an object at a deep path: ${opt} (contains a dot)`); | ||
|
||
const initial = parsed.hasOwnProperty(opt) ? parsed[opt] : {}; | ||
const depth = (path) => path.split('.').length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be an internal
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that I understand why this would need to live on internals
. There is no styleguide rule about this that I am aware of, and in my opinion colocating this utility near where it's used is going to be a bit clearer (at least for me).
This PR implements the the ability to have an object-type argument so that the user can build-up an object value using dot-separated paths and JSON. For example, an object argument named
pet
might be built from--pet '{ "type": "dog" }' --pet.name Maddie
, resulting in the parsing output{ pet: { type: 'dog', name: 'Maddie' } }
. Comparable functionality can be found in other arg parsers such as yargs.I am not sure if this may be applicable within lab, but if this lands I believe yargs can be removed from confidence, and will simplify some code in hpal-debug as well, which would be useful to hapi pal. I believe this could also open-up some possibilities, e.g. to build request payloads from the command line.