Skip to content
Closed
Show file tree
Hide file tree
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
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions openapi/v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47360,6 +47360,9 @@ paths:
- 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
Expand Down Expand Up @@ -47485,6 +47488,9 @@ paths:
- 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
Expand Down Expand Up @@ -51065,6 +51071,9 @@ paths:
- 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
Expand Down Expand Up @@ -51226,6 +51235,9 @@ paths:
- 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
Expand Down
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",
Expand Down
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