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

CLOUDP-286341: Validate Spectral Rule Application (PoC) #293

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions openapi/v2.json
Original file line number Diff line number Diff line change
@@ -21582,6 +21582,11 @@
}
},
"/api/atlas/v2/groups/{groupId}/serviceAccounts/{clientId}/accessList": {
"x-xgen-IPA-exception": {
"xgen-IPA-104-resource-has-GET": {
"reason": "Testing"
}
},
"get": {
"description": "Returns all access list entries that you configured for the specified Service Account for the project. Available as a preview feature.",
"operationId": "listProjectServiceAccountAccessList",
12 changes: 12 additions & 0 deletions openapi/v2.yaml
Original file line number Diff line number Diff line change
@@ -164,7 +164,7 @@
format: date-time
type: string
responses:
accepted:

Check warning on line 167 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
description: Accepted.
badRequest:
content:
@@ -199,7 +199,7 @@
schema:
$ref: '#/components/schemas/ApiError'
description: Forbidden.
gone:

Check warning on line 202 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
content:
application/json:
example:
@@ -232,7 +232,7 @@
schema:
$ref: '#/components/schemas/ApiError'
description: Method Not Allowed.
noBody:

Check warning on line 235 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
description: This endpoint does not return a response body.
notFound:
content:
@@ -517,7 +517,7 @@
type: string
title: AWS
type: object
AWSCreateDataProcessRegionView:

Check warning on line 520 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/CreateDataProcessRegionView'
- properties:
@@ -549,7 +549,7 @@
required:
- enabled
type: object
AWSDataProcessRegionView:

Check warning on line 552 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/DataProcessRegionView'
- properties:
@@ -709,7 +709,7 @@
type: integer
title: AWS Cluster Hardware Settings
type: object
AWSInterfaceEndpoint:

Check warning on line 712 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
description: Group of Private Endpoint settings.
properties:
cloudProvider:
@@ -826,7 +826,7 @@
readOnly: true
type: boolean
type: object
AWSPrivateLinkConnection:

Check warning on line 829 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
description: Group of Private Endpoint Service settings.
properties:
cloudProvider:
@@ -1482,7 +1482,7 @@
example: ALERT_CONFIG_ADDED_AUDIT
title: Alert Audit Types
type: string
AlertConfigView:

Check warning on line 1485 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
description: Alert settings allows to select which conditions trigger alerts and how users are notified.
properties:
created:
@@ -1856,7 +1856,7 @@
- TOKYO_JPN
- SINGAPORE_SGP
type: string
ApiAtlasDataLakeStorageView:

Check warning on line 1859 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
$ref: '#/components/schemas/DataLakeStorage'
ApiAtlasFTSAnalyzersViewManual:
description: Settings that describe one Atlas Search custom analyzer.
@@ -4245,7 +4245,7 @@
type: string
title: Azure
type: object
AzureCreateDataProcessRegionView:

Check warning on line 4248 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/CreateDataProcessRegionView'
- properties:
@@ -4257,7 +4257,7 @@
type: string
type: object
type: object
AzureDataProcessRegionView:

Check warning on line 4260 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/DataProcessRegionView'
- properties:
@@ -4589,7 +4589,7 @@
- vnetName
title: AZURE
type: object
AzurePrivateEndpoint:

Check warning on line 4592 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
description: Group of Private Endpoint settings.
properties:
cloudProvider:
@@ -4635,7 +4635,7 @@
- cloudProvider
title: AZURE
type: object
AzurePrivateLinkConnection:

Check warning on line 4638 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
description: Group of Private Endpoint Service settings.
properties:
cloudProvider:
@@ -5682,7 +5682,7 @@
- $ref: '#/components/schemas/ApiStreamsAWSRegionView'
- $ref: '#/components/schemas/ApiStreamsAzureRegionView'
type: object
BasicBSONList:

Check warning on line 5685 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
description: List that contains the search criteria that the query uses. To use the values in key-value pairs in these predicates requires **Project Data Access Read Only** permissions or greater. Otherwise, MongoDB Cloud redacts these values.
items:
description: List that contains the search criteria that the query uses. To use the values in key-value pairs in these predicates requires **Project Data Access Read Only** permissions or greater. Otherwise, MongoDB Cloud redacts these values.
@@ -6945,7 +6945,7 @@
required:
- providerName
type: object
CloudProviderAccessAWSIAMRoleRequestUpdate:

Check warning on line 6948 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/CloudProviderAccessRoleRequestUpdate'
- properties:
@@ -7102,7 +7102,7 @@
required:
- providerName
type: object
CloudProviderAccessAzureServicePrincipalRequestUpdate:

Check warning on line 7105 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/CloudProviderAccessRoleRequestUpdate'
- properties:
@@ -7153,7 +7153,7 @@
required:
- providerName
type: object
CloudProviderAccessDataLakeFeatureUsage:

Check warning on line 7156 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/CloudProviderAccessFeatureUsage'
- properties:
@@ -7162,7 +7162,7 @@
type: object
description: Details that describe the Atlas Data Lakes linked to this Amazon Web Services (AWS) Identity and Access Management (IAM) role.
type: object
CloudProviderAccessEncryptionAtRestFeatureUsage:

Check warning on line 7165 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/CloudProviderAccessFeatureUsage'
- properties:
@@ -7171,7 +7171,7 @@
type: object
description: Details that describe the Key Management Service (KMS) linked to this Amazon Web Services (AWS) Identity and Access Management (IAM) role.
type: object
CloudProviderAccessExportSnapshotFeatureUsage:

Check warning on line 7174 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/CloudProviderAccessFeatureUsage'
- properties:
@@ -7251,7 +7251,7 @@
readOnly: true
type: string
type: object
CloudProviderAccessGCPServiceAccount:

Check warning on line 7254 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/CloudProviderAccessRole'
- properties:
@@ -7317,14 +7317,14 @@
required:
- providerName
type: object
CloudProviderAccessGCPServiceAccountRequestUpdate:

Check warning on line 7320 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/CloudProviderAccessRoleRequestUpdate'
description: Details that describe the features linked to the GCP Service Account.
required:
- providerName
type: object
CloudProviderAccessPushBasedLogExportFeatureUsage:

Check warning on line 7327 in openapi/v2.yaml

GitHub Actions / Lint (pull_request)

oas3-unused-component

Potentially unused component has been detected.
allOf:
- $ref: '#/components/schemas/CloudProviderAccessFeatureUsage'
- properties:
@@ -47360,6 +47360,9 @@
- Service Accounts
x-xgen-owner-team: apix
/api/atlas/v2/groups/{groupId}/serviceAccounts/{clientId}/accessList:
x-xgen-IPA-exception:
xgen-IPA-104-resource-has-GET:
reason: "Testing"
get:
description: Returns all access list entries that you configured for the specified Service Account for the project. Available as a preview feature.
operationId: listProjectServiceAccountAccessList
@@ -47485,6 +47488,9 @@
- Service Accounts
x-xgen-owner-team: apix
/api/atlas/v2/groups/{groupId}/serviceAccounts/{clientId}/secrets:
x-xgen-IPA-exception:
xgen-IPA-104-resource-has-GET:
reason: "Testing"
post:
description: Create a secret for the specified Service Account in the specified Project. Available as a preview feature.
operationId: createProjectServiceAccountSecret
@@ -51065,6 +51071,9 @@
- Service Accounts
x-xgen-owner-team: apix
/api/atlas/v2/orgs/{orgId}/serviceAccounts/{clientId}/accessList:
x-xgen-IPA-exception:
xgen-IPA-104-resource-has-GET:
reason: "Testing"
get:
description: Returns all access list entries that you configured for the specified Service Account for the organization. Available as a preview feature.
operationId: listServiceAccountAccessList
@@ -51226,6 +51235,9 @@
- Service Accounts
x-xgen-owner-team: apix
/api/atlas/v2/orgs/{orgId}/serviceAccounts/{clientId}/secrets:
x-xgen-IPA-exception:
xgen-IPA-104-resource-has-GET:
reason: "Testing"
post:
description: Create a secret for the specified Service Account. Available as a preview feature.
operationId: createServiceAccountSecret
4,123 changes: 3,565 additions & 558 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
{
"name": "mongodb-openapi",
"description": "MongoDB repository with OpenAPI specification",
"type": "module",
"scripts": {
"format": "npx prettier . --write",
"format-check": "npx prettier . --check",
"lint-js": "npx eslint **/*.js"
"lint-js": "npx eslint **/*.js",
"ipa-validation": "spectral lint ./openapi/v2.yaml --ruleset=./tools/ipa/ipa-spectral.yaml -v"
},
"dependencies": {
"openapi-to-postmanv2": "4.24.0"
"@stoplight/spectral-cli": "^6.14.2",
"@stoplight/spectral-core": "^1.19.4",
"@stoplight/spectral-ruleset-bundler": "^1.6.1",
"@stoplight/spectral-runtime": "^1.1.3",
"jsonpath": "^1.1.1",
"jsonpath-ng": "^1.0.4",
"jsonpath-plus": "^10.2.0",
"openapi-to-postmanv2": "4.24.0",
"xmlbuilder2": "^3.1.1"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
31 changes: 31 additions & 0 deletions tools/ipa/ExemptionCollector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class ExemptionCollector {
constructor() {
if (!ExemptionCollector.instance) {
this.exemptions = [];
console.log('ExemptionCollector instantiated'); // Debug log
ExemptionCollector.instance = this; // Store the singleton instance
}

return ExemptionCollector.instance;
}

log(ruleName, context, details) {
console.log('Adding to collector:', { ruleName, context, details });
this.exemptions.push({
rule: ruleName,
path: context.path.join('.'),
details,
});
console.log('Current exemptions:', this.exemptions);
}

getExemptions() {
console.log('Retrieving exemptions:', this.exemptions);
return this.exemptions;
}
}

// Create a singleton collector
const exemptionCollector = new ExemptionCollector();
Object.freeze(exemptionCollector); // Prevent accidental modification
export default exemptionCollector;
55 changes: 55 additions & 0 deletions tools/ipa/formatters/custom-formatter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { create } from 'xmlbuilder2';


export default async function customJUnitFormatter(results, document, spectral) {
const allRules = spectral.ruleset.rules;
const failedRules = new Set(results.map((result) => result.code));

// Create a new XML document
const xml = create({ version: '1.0' }) // Specify XML version
.ele('testsuite', {
name: 'SpectralLint',
tests: Object.keys(allRules).length,
});

// Add test cases for each rule
for (const ruleName of Object.keys(allRules)) {
const rule = allRules[ruleName];

// Collect all results for this specific rule
const ruleResults = results.filter((result) => result.code === ruleName);

const testCase = xml.ele('testcase', {
classname: ruleName,
name: rule.description || 'No description available',
time: '0',
});

if (failedRules.has(ruleName)) {
// Add detailed failure information including components
ruleResults.forEach((result) => {
const failureDetails = testCase.ele('failure', {
type: ruleName,
path: result.path.join(".") || 'Unknown path'
});

// Include component and specific location details
failureDetails.txt(JSON.stringify({
message: result.message,
component: result.path || 'Unknown',
location: result.range
? `Line ${result.range.start.line}, Column ${result.range.start.character}`
: 'No specific location',
}, null, 2));
});
} else {
testCase.ele('success', {
description: 'All checks passed for this rule',
type: ruleName
});
}
}

// Convert the XML structure to a string
return xml.end({ prettyPrint: true });
}
2 changes: 2 additions & 0 deletions tools/ipa/ipa-spectral.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
extends:
- ./rulesets/IPA-104.yaml
15 changes: 15 additions & 0 deletions tools/ipa/rulesets/IPA-104.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# IPA-104: Get
# http://go/ipa/104

functions:
- "eachResourceHasGetMethod"

rules:
xgen-IPA-104-resource-has-GET:
description: "APIs must provide a get method for resources. http://go/ipa/104"
message: "{{error}} http://go/ipa/117"
severity: error
given: "$.paths"
then:
field: "@key"
function: "eachResourceHasGetMethod"
47 changes: 47 additions & 0 deletions tools/ipa/rulesets/functions/eachResourceHasGetMethod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { hasException } from './utils/exemptions.js';
import {
hasGetMethod,
isChild,
isCustomMethod,
isStandardResource,
isSingletonResource,
getResourcePaths,
} from './utils/resourceEvaluation.js';

const RULE_NAME = 'xgen-IPA-104-resource-has-GET';
const ERROR_MESSAGE = 'APIs must provide a get method for resources.';

export default (input, _, context) => {
if (isChild(input) || isCustomMethod(input)) {
return;
}

const oas = context.documentInventory.resolved;
const resourceObject = oas.paths[input];

if (hasException(RULE_NAME, resourceObject, context)) {
return;
}

const resourcePaths = getResourcePaths(input, Object.keys(oas.paths));

if (isSingletonResource(resourcePaths)) {
// Singleton resource, may have custom methods
if (!hasGetMethod(oas.paths[resourcePaths[0]])) {
return [
{
message: ERROR_MESSAGE,
},
];
}
} else if (isStandardResource(resourcePaths)) {
// Normal resource, may have custom methods
if (!hasGetMethod(oas.paths[resourcePaths[1]])) {
return [
{
message: ERROR_MESSAGE,
},
];
}
}
};
24 changes: 24 additions & 0 deletions tools/ipa/rulesets/functions/utils/exemptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import exemptionCollector from 'tools/ipa/ExemptionCollector';

const EXEMPTION_EXTENSION = 'x-xgen-IPA-exception';

/**
* Checks if the object has an exemption set for the passed rule name by checking
* if the object has a field "x-xgen-IPA-exception" containing the rule as a
* field.
*
* @param ruleName the name of the exemption
* @param object the object to evaluate
* @param context the context of the rule function
* @returns {boolean}
*/
export function hasException(ruleName, object, context) {
const exemptions = object[EXEMPTION_EXTENSION];
const hasException = exemptions !== undefined && Object.keys(exemptions).includes(ruleName);
if (hasException) {
exemptionCollector.log(ruleName, context, exemptions[ruleName]);
console.log('Exception\t', ruleName, '\t', context.path.join('.'));
}
return hasException;
}

63 changes: 63 additions & 0 deletions tools/ipa/rulesets/functions/utils/resourceEvaluation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export function isChild(path) {
return path.endsWith('}');
}

export function isCustomMethod(path) {
return path.includes(':');
}

/**
* Checks if a resource is a singleton resource based on the paths for the
* resource. The resource may have custom methods.
*
* @param resourcePaths all paths for the resource as an array of strings
* @returns {boolean}
*/
export function isSingletonResource(resourcePaths) {
if (resourcePaths.length === 1) {
return true;
}
const additionalPaths = resourcePaths.slice(1);
return !additionalPaths.some((p) => !isCustomMethod(p));
}

/**
* Checks if a resource is a standard resource based on the paths for the
* resource. The resource may have custom methods.
*
* @param resourcePaths all paths for the resource as an array of strings
* @returns {boolean}
*/
export function isStandardResource(resourcePaths) {
if (resourcePaths.length === 2 && isChild(resourcePaths[1])) {
return true;
}
if (resourcePaths.length < 3 || !isChild(resourcePaths[1])) {
return false;
}
const additionalPaths = resourcePaths.slice(2);
return !additionalPaths.some((p) => !isCustomMethod(p));
}

/**
* Checks if a path object has a GET method
*
* @param pathObject the path object to evaluate
* @returns {boolean}
*/
export function hasGetMethod(pathObject) {
return Object.keys(pathObject).some((o) => o === 'get');
}

/**
* Get all paths for a resource based on the parent path
*
* @param parent the parent path string
* @param allPaths all paths as an array of strings
* @returns {*} a string array of all paths for a resource, including the parent
*/
export function getResourcePaths(parent, allPaths) {
const childPathPattern = new RegExp(`^${parent}/{[a-zA-Z]+}$`);
const customMethodPattern = new RegExp(`^${parent}/{[a-zA-Z]+}:+[a-zA-Z]+$`);
return allPaths.filter((p) => parent === p || childPathPattern.test(p) || customMethodPattern.test(p));
}
Loading