Skip to content

Add linter rule to suggest the scope parameter #765

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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
35 changes: 35 additions & 0 deletions docs/suggest-scope-parameter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# SuggestScopeParameter

## Category

ARM Warning

## Applies to

ARM OpenAPI(swagger) specs

## Related ARM Guideline Code

- N/a

## Description

OpenAPI authors can use the `scope` path parameter to indicate that an API is applicable to various scopes (Tenant,
Management Group, Subscription, Resource Group, etc.) rather than explicitly defining each scope in the spec.

## How to fix

Remove all explicitly-scoped paths that only vary in scope and create a path with the `scope` parameter.

Copy link
Member

@rkmanda rkmanda Jan 29, 2025

Choose a reason for hiding this comment

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

This may not be a good idea from a customer viewpoint as it makes it less obvious as to what scopes are truly supported. Before we decide one way or the other, lets first check how TypeSpec generates the different API paths for this scenario and align the linter rules to work with that.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see support in TypeSpec for the scope parameter, but I'm also not seeing how you can generate a spec with a resource at multiple scopes aside from extension resources. I might look into this a bit more and follow up

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure that there is any use of a scope parameter outside of extension resources. I have not been able to find one in the private API specs repo yet, and I'm not sure how it would fit with the RPC.

Copy link
Member Author

Choose a reason for hiding this comment

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

The scenario described in #750 is a rare case where an RP is attempting to basically duplicate endpoints. I will change the rule to look for duplicate endpoints like this.

Example of explicitly-scoped paths that only vary in scope:

2. Explicitly-scoped path (by subscription)
**`/subscriptions/{subscriptionId}`**`/providers/Microsoft.Bakery/breads`

3. Explicitly-scoped path (by resource group):
**`/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}`**`/providers/Microsoft.Bakery/breads`

These two paths can be replaced with a single path that uses the `scope` parameter:

1. Path with scope parameter:
**`/{scope}`**`/providers/Microsoft.Bakery/breads`
1 change: 1 addition & 0 deletions packages/rulesets/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Patches

- [AllTrackedResourcesMustHaveDelete][TrackedResourcePatchOperation] Skip the api paths if it has PrivateEndpointConnectionProxy
- [SuggestScopeParameter] Added this rule to suggest using the scope parameter if paths can be combined

### Patches

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ rules.push({
appliesTo_JsonQuery: "$",
*run(doc, node, path, ctx) {
const msg =
'The response in the GET collection operation "{0}" does not match the response definition in the individual GET operation "{1}" .'
'The response in the GET collection operation "{0}" does not match the response definition in the individual GET operation "{1}".'
/**
* 1 travel all resources and find all the resources that have a collection get
* - by searching all the models return by a get operation and verify the schema
Expand Down
16 changes: 16 additions & 0 deletions packages/rulesets/src/spectral/az-arm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import resourceNameRestriction from "./functions/resource-name-restriction"
import responseSchemaSpecifiedForSuccessStatusCode from "./functions/response-schema-specified-for-success-status-code"
import { securityDefinitionsStructure } from "./functions/security-definitions-structure"
import skuValidation from "./functions/sku-validation"
import suggestScopeParameter from "./functions/suggest-scope-parameter"
import { systemDataInPropertiesBag } from "./functions/system-data-in-properties-bag"
import { tagsAreNotAllowedForProxyResources } from "./functions/tags-are-not-allowed-for-proxy-resources"
import { tenantLevelAPIsNotAllowed } from "./functions/tenant-level-apis-not-allowed"
Expand Down Expand Up @@ -319,6 +320,21 @@ const ruleset: any = {
},
},

SuggestScopeParameter: {
description:
"Duplicate operations that vary only by scope can be defined with a single operation that has a scope parameter. This reduces the number of operations in the spec.",
severity: "warn",
stagingOnly: true,
message: "{{error}}",
resolved: true,
formats: [oas2],
given: "$.paths",
then: {
field: "@key",
function: suggestScopeParameter,
},
},

///
/// ARM RPC rules for Get patterns
///
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Suggests combining paths that differ only in scope by defining a scope parameter.
*
* This function checks if the given path can be combined with other paths in the Swagger document
* by introducing a scope parameter. It returns suggestions for paths that differ only in scope.
*/

const suggestScopeParameter = (path: any, _opts: any, ctx: any) => {
const swagger = ctx?.documentInventory?.resolved

let lowerCasePath: string

if (
path === null ||
typeof path !== "string" ||
path.length === 0 ||
swagger === null ||
(lowerCasePath = path.toLocaleLowerCase()).includes("{scope}")
) {
return []
}

const suffix = path.substring(path.lastIndexOf("/providers")).toLocaleLowerCase()

// Find all paths that differ only in scope
const matchingPaths = Object.keys(swagger.paths).filter((p: string) => {
const lower = p.toLocaleLowerCase()
return !lower.includes("{scope}") && lower !== lowerCasePath && lower.endsWith(suffix)
})

return matchingPaths.map((match: string) => {
return {
// note: only matched path is included in the message so that Spectral can deduplicate errors
message: `Path with suffix "${suffix}" differs from another path only in scope. These paths share a suffix and can be combined by defining a scope parameter.`,
path: ctx.path,
}
})
}

export default suggestScopeParameter
Loading