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

Validate the unknown object is either plain object of primitive #2572

Merged
merged 16 commits into from
Nov 1, 2024

Conversation

lakhveerk
Copy link
Contributor

For more information about how to contribute to this repo, visit this page.

Description

In externalAppAuthnetication and externalAppAutneticationForCEA, some APIs accept IActionExecuteInvokeRequest as input. This interface has a data property that can be of type string | Record<string, unknown>. The unknown type in Record is unknown. Currently, we post the value as it is to host sdk. However, it could have contained values that are not serializable.
The copilot team that consumes the API confirms that the type would be either primitive or plain object. This PR validate the data and throws if that is not the case.

Main changes in the PR:

  1. Added validation function isPrimitiveOrPlainObject in utils.ts file.
  2. Added validation for IActionExecuteInvokeRequest.data element in ExternalAppAuthentication and ExternalAppAuthenticationForCEA capabilities.

Validation

Validation performed:

  1. <Step 1>
  2. <Step 2>

Unit Tests added:

Unit tests are required for all changes. If no unit tests were added as part of this change, please explain why they aren't necessary.

<Yes/No>

End-to-end tests added:

<Yes/No>

Additional Requirements

Change file added:

Ensure the change file meets the formatting requirements.

<Yes/No>

Related PRs:

Remove this section if n/a

Next/remaining steps:

List the next or remaining steps in implementing the overall feature in subsequent PRs (or is the feature 100% complete after this?).

Remove this section if n/a

  • Item 1
  • Item 2

Screenshots:

Remove this section if n/a

Before After
< image1 > < image2 >

@lakhveerk lakhveerk requested a review from a team as a code owner October 24, 2024 16:52
@lakhveerk lakhveerk marked this pull request as draft October 24, 2024 17:01
@lakhveerk lakhveerk marked this pull request as ready for review October 24, 2024 18:37
Copy link
Contributor

@TrevorJoelHarris TrevorJoelHarris left a comment

Choose a reason for hiding this comment

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

Small questions about testing

@@ -504,3 +504,46 @@ export function validateUuid(id: string | undefined | null): void {
throw new Error('id must be a valid UUID');
}
}

/**
* @beta
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably no need for a @beta tag here since this isn't exported outside of the library

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed!

}

// Check all properties of the object recursively
return Object.keys(value).every((key) => isPrimitiveOrPlainObject(value[key]));
Copy link
Contributor

Choose a reason for hiding this comment

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

I couldn't tell by looking at your unit tests: do you have any unit tests that hit line 548 and actually recurse through? That would be I guess an object with no functions at the top level but a function inside of one of its members.

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 added the second expect in this test to explicitly test the function in nested object.

it('should return false for nested structures with non-plain objects', () => {
  expect(isPrimitiveOrPlainObject({ a: [1, 2, new Date()] })).toBe(false);
  expect(isPrimitiveOrPlainObject({ a: { b: [1, 2, function () {}] } })).toBe(false);
});

*
*/
export function isPrimitiveOrPlainObject(value: unknown, depth: number = 0): boolean {
if (depth > 1000) {
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 added this change as well since you last reviewed @TrevorJoelHarris

Copy link
Contributor

Choose a reason for hiding this comment

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

just curious, where the 1000 from? why we decide to use 1000 instead of other number?

Copy link
Contributor Author

@lakhveerk lakhveerk Nov 1, 2024

Choose a reason for hiding this comment

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

Because Anand from 3pcxp said, he does not see it going any further than that, and he recommended capping it

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: It would be great if you could add this context as a comment for this function to help the next developer understand the logic.

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 added a comment saying Recursion is limited to a maximum depth of 1000 to prevent excessive nesting and potential stack overflow. Does that work?

}

// Check if the value is a primitive type or null
if (
Copy link
Contributor

@MengyiGong MengyiGong Nov 1, 2024

Choose a reason for hiding this comment

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

nit: what about this approach ( it covers all primitive types without needing to list each one individually) :

 return value === null || (typeof value !== 'object' && typeof value !== 'function');
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The function needs to allow the nested primitives in an object as well. This check will incorrectly return false if the value was a plain object or an array of primitives.

* Limited to Microsoft-internal use
*/
function validateQueryMessageExtensionRequest(originalRequestInfo: IQueryMessageExtensionRequest): void {
if (originalRequestInfo.commandId.length > 64) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I might not know much of the background context, and I feel confused and curious about where these magic numbers (like 64, 5, 512) come from.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They all came from 3pcxp team. In this PR, I am just dividing the already existing function into smaller functions. These numbers were added when the support was added for MEs in last December

@lakhveerk lakhveerk requested a review from MengyiGong November 1, 2024 17:58
@lakhveerk lakhveerk merged commit ab4604e into main Nov 1, 2024
28 checks passed
@lakhveerk lakhveerk deleted the lakhveer/validateRecord branch November 1, 2024 20:12
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