Support data plane RBAC for Gremlin API (#2182)

* Refactor logic for determining if we should use data plane RBAC to a
common function.

* Support RBAC for gremlin API.

* Refactor to use common function.

* Fix unit tests.

* Move test function inside test scope.

* Minor clean ups.

* Reinstate utf8ToB64 function in case this breaks a corner case.
This commit is contained in:
jawelton74 2025-07-09 16:23:09 -07:00 committed by GitHub
parent f370507a27
commit 30a3b5c7a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 23 additions and 22 deletions

View File

@ -1,3 +1,4 @@
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
import { createCollection } from "../../Common/dataAccess/createCollection"; import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient"; import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
@ -90,12 +91,13 @@ export class ContainerSampleGenerator {
} }
const { databaseAccount: account } = userContext; const { databaseAccount: account } = userContext;
const databaseId = collection.databaseId; const databaseId = collection.databaseId;
const gremlinClient = new GremlinClient(); const gremlinClient = new GremlinClient();
gremlinClient.initialize({ gremlinClient.initialize({
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`, endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
databaseId: databaseId, databaseId: databaseId,
collectionId: collection.id(), collectionId: collection.id(),
masterKey: userContext.masterKey || "", password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
maxResultSize: 100, maxResultSize: 100,
}); });

View File

@ -163,8 +163,7 @@ describe("GraphExplorer", () => {
graphBackendEndpoint: "graphBackendEndpoint", graphBackendEndpoint: "graphBackendEndpoint",
databaseId: "databaseId", databaseId: "databaseId",
collectionId: "collectionId", collectionId: "collectionId",
masterKey: "masterKey", password: "password",
onLoadStartKey: 0, onLoadStartKey: 0,
onLoadStartKeyChange: (newKey: number): void => {}, onLoadStartKeyChange: (newKey: number): void => {},
resourceId: "resourceId", resourceId: "resourceId",

View File

@ -59,7 +59,7 @@ export interface GraphExplorerProps {
graphBackendEndpoint: string; graphBackendEndpoint: string;
databaseId: string; databaseId: string;
collectionId: string; collectionId: string;
masterKey: string; password: string;
onLoadStartKey: number; onLoadStartKey: number;
onLoadStartKeyChange: (newKey: number) => void; onLoadStartKeyChange: (newKey: number) => void;
@ -1300,7 +1300,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
endpoint: `wss://${this.props.graphBackendEndpoint}`, endpoint: `wss://${this.props.graphBackendEndpoint}`,
databaseId: this.props.databaseId, databaseId: this.props.databaseId,
collectionId: this.props.collectionId, collectionId: this.props.collectionId,
masterKey: this.props.masterKey, password: this.props.password,
maxResultSize: GraphExplorer.MAX_RESULT_SIZE, maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
}); });
} }

View File

@ -8,28 +8,28 @@ describe("Gremlin Client", () => {
endpoint: null, endpoint: null,
collectionId: null, collectionId: null,
databaseId: null, databaseId: null,
masterKey: null,
maxResultSize: 10000, maxResultSize: 10000,
password: null,
}; };
it("should use databaseId, collectionId and masterKey to authenticate", () => { it("should use databaseId, collectionId and password to authenticate", () => {
const collectionId = "collectionId"; const collectionId = "collectionId";
const databaseId = "databaseId"; const databaseId = "databaseId";
const masterKey = "masterKey"; const testPassword = "password";
const gremlinClient = new GremlinClient(); const gremlinClient = new GremlinClient();
gremlinClient.initialize({ gremlinClient.initialize({
endpoint: null, endpoint: null,
collectionId, collectionId,
databaseId, databaseId,
masterKey,
maxResultSize: 0, maxResultSize: 0,
password: testPassword,
}); });
// User must includes these values // User must includes these values
expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1); expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1);
expect(gremlinClient.client.params.user.indexOf(databaseId)).not.toBe(-1); expect(gremlinClient.client.params.user.indexOf(databaseId)).not.toBe(-1);
expect(gremlinClient.client.params.password).toEqual(masterKey); expect(gremlinClient.client.params.password).toEqual(testPassword);
}); });
it("should aggregate RU charges across multiple responses", (done) => { it("should aggregate RU charges across multiple responses", (done) => {

View File

@ -11,8 +11,8 @@ export interface GremlinClientParameters {
endpoint: string; endpoint: string;
databaseId: string; databaseId: string;
collectionId: string; collectionId: string;
masterKey: string;
maxResultSize: number; maxResultSize: number;
password: string;
} }
export interface GremlinRequestResult { export interface GremlinRequestResult {
@ -43,7 +43,7 @@ export class GremlinClient {
this.client = new GremlinSimpleClient({ this.client = new GremlinSimpleClient({
endpoint: params.endpoint, endpoint: params.endpoint,
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`, user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
password: params.masterKey, password: params.password,
successCallback: (result: Result) => { successCallback: (result: Result) => {
this.storePendingResult(result); this.storePendingResult(result);
this.flushResult(result.requestId); this.flushResult(result.requestId);

View File

@ -5,11 +5,11 @@
import * as sinon from "sinon"; import * as sinon from "sinon";
import { import {
GremlinRequestMessage,
GremlinResponseMessage,
GremlinSimpleClient, GremlinSimpleClient,
GremlinSimpleClientParameters, GremlinSimpleClientParameters,
Result, Result,
GremlinRequestMessage,
GremlinResponseMessage,
} from "./GremlinSimpleClient"; } from "./GremlinSimpleClient";
describe("Gremlin Simple Client", () => { describe("Gremlin Simple Client", () => {

View File

@ -45,7 +45,7 @@ export interface IGraphConfig {
interface GraphTabOptions extends ViewModels.TabOptions { interface GraphTabOptions extends ViewModels.TabOptions {
account: DatabaseAccount; account: DatabaseAccount;
masterKey: string; password: string;
collectionId: string; collectionId: string;
databaseId: string; databaseId: string;
collectionPartitionKeyProperty: string; collectionPartitionKeyProperty: string;
@ -107,7 +107,7 @@ export default class GraphTab extends TabsBase {
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account), graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
databaseId: options.databaseId, databaseId: options.databaseId,
collectionId: options.collectionId, collectionId: options.collectionId,
masterKey: options.masterKey, password: options.password,
onLoadStartKey: options.onLoadStartKey, onLoadStartKey: options.onLoadStartKey,
onLoadStartKeyChange: (onLoadStartKey: number): void => { onLoadStartKeyChange: (onLoadStartKey: number): void => {
if (onLoadStartKey === undefined) { if (onLoadStartKey === undefined) {

View File

@ -8,6 +8,7 @@ import {
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
@ -479,9 +480,8 @@ export default class Collection implements ViewModels.Collection {
node: this, node: this,
title: title, title: title,
tabPath: "", tabPath: "",
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
collection: this, collection: this,
masterKey: userContext.masterKey || "",
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0], collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
collectionId: this.id(), collectionId: this.id(),
databaseId: this.databaseId, databaseId: this.databaseId,
@ -737,7 +737,7 @@ export default class Collection implements ViewModels.Collection {
title: title, title: title,
tabPath: "", tabPath: "",
collection: this, collection: this,
masterKey: userContext.masterKey || "", password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0], collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
collectionId: this.id(), collectionId: this.id(),
databaseId: this.databaseId, databaseId: this.databaseId,

View File

@ -91,5 +91,5 @@ export const getItemName = (): string => {
}; };
export const isDataplaneRbacSupported = (apiType: string): boolean => { export const isDataplaneRbacSupported = (apiType: string): boolean => {
return apiType === "SQL" || apiType === "Tables"; return apiType === "SQL" || apiType === "Tables" || apiType === "Gremlin";
}; };

View File

@ -104,7 +104,7 @@ describe("AuthorizationUtils", () => {
it("should return true if dataPlaneRbacEnabled is set to true and API supports RBAC", () => { it("should return true if dataPlaneRbacEnabled is set to true and API supports RBAC", () => {
setAadDataPlane(false); setAadDataPlane(false);
["SQL", "Tables"].forEach((type) => { ["SQL", "Tables", "Gremlin"].forEach((type) => {
updateUserContext({ updateUserContext({
dataPlaneRbacEnabled: true, dataPlaneRbacEnabled: true,
apiType: type as ApiType, apiType: type as ApiType,
@ -115,7 +115,7 @@ describe("AuthorizationUtils", () => {
it("should return false if dataPlaneRbacEnabled is set to true and API does not support RBAC", () => { it("should return false if dataPlaneRbacEnabled is set to true and API does not support RBAC", () => {
setAadDataPlane(false); setAadDataPlane(false);
["Mongo", "Gremlin", "Cassandra", "Postgres", "VCoreMongo"].forEach((type) => { ["Mongo", "Cassandra", "Postgres", "VCoreMongo"].forEach((type) => {
updateUserContext({ updateUserContext({
dataPlaneRbacEnabled: true, dataPlaneRbacEnabled: true,
apiType: type as ApiType, apiType: type as ApiType,