Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
- Added extensions replacement registry and scraper tool to track migrations for deprecated extensions ahead of the March 2027 decommission date.
- [Added] Loads existing `.env` files and passes environment variables to functions discovery in `runtimeDelegate`.
- Fixed issues in the default TypeScript functions template.
- Esure the user has proper permission and roles to send request to OneMCP servers.
343 changes: 343 additions & 0 deletions src/ensurePermissions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
import { expect } from "chai";
import * as sinon from "sinon";
import { FirebaseError } from "./error";
import { configstore } from "./configstore";
import * as resourceManager from "./gcp/resourceManager";
import * as iam from "./gcp/iam";
import { ensurePermissionsOrSetRole } from "./ensurePermissions";
import * as utils from "./utils";

describe("ensurePermissionsOrSetRole", () => {
let sandbox: sinon.SinonSandbox;
let getIamPolicyStub: sinon.SinonStub;
let setIamPolicyStub: sinon.SinonStub;
let testIamPermissionsStub: sinon.SinonStub;
let cacheStore: Record<string, any>;

Check warning on line 15 in src/ensurePermissions.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

const mockPermissions = [
"mcp.tools.call",
"resourcemanager.projects.get",
"resourcemanager.projects.list",
];
const mockRole = "roles/mcp.toolUser";

beforeEach(() => {
sandbox = sinon.createSandbox();
getIamPolicyStub = sandbox.stub(resourceManager, "getIamPolicy");
setIamPolicyStub = sandbox.stub(resourceManager, "setIamPolicy");
testIamPermissionsStub = sandbox.stub(iam, "testIamPermissions");
sandbox.stub(utils, "sleep").resolves();
sandbox.stub(Date, "now").returns(1710000000000);

cacheStore = {};
sandbox.stub(configstore, "get").callsFake((key: string) => cacheStore[key]);

Check warning on line 33 in src/ensurePermissions.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe return of an `any` typed value
(sandbox.stub(configstore, "set") as any).callsFake((key: string, val: any) => {

Check warning on line 34 in src/ensurePermissions.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

Check warning on line 34 in src/ensurePermissions.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

Check warning on line 34 in src/ensurePermissions.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe call of an `any` typed value

Check warning on line 34 in src/ensurePermissions.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .callsFake on an `any` value
cacheStore[key] = val;

Check warning on line 35 in src/ensurePermissions.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe assignment of an `any` value
});
});

afterEach(() => {
sandbox.restore();
});

it("should succeed and cache positive check if permissions are already held", async () => {
testIamPermissionsStub.resolves({
passed: true,
allowed: mockPermissions,
missing: [],
});

await ensurePermissionsOrSetRole("test-project", "test@example.com", mockPermissions, mockRole);

expect(testIamPermissionsStub).to.have.been.calledOnceWith("test-project", mockPermissions);
expect(getIamPolicyStub).to.not.have.been.called;
expect(cacheStore["iamPermissionCache"]).to.deep.equal({
"test-project": {
"test@example.com": {
"mcp.tools.call": 1710000000000,
"resourcemanager.projects.get": 1710000000000,
"resourcemanager.projects.list": 1710000000000,
},
},
});
});

it("should skip API call and succeed if permissions are cached and not expired", async () => {
cacheStore["iamPermissionCache"] = {
"test-project": {
"test@example.com": {
"mcp.tools.call": 1710000000000,
"resourcemanager.projects.get": 1710000000000,
"resourcemanager.projects.list": 1710000000000,
},
},
};

await ensurePermissionsOrSetRole("test-project", "test@example.com", mockPermissions, mockRole);

expect(testIamPermissionsStub).to.not.have.been.called;
expect(getIamPolicyStub).to.not.have.been.called;
});

it("should query API if cached permissions have expired", async () => {
const now = 1710000000000;
const oneDayAgo = now - 24 * 60 * 60 * 1000 - 1000; // expired by 1 second
cacheStore["iamPermissionCache"] = {
"test-project": {
"test@example.com": {
"mcp.tools.call": oneDayAgo,
"resourcemanager.projects.get": 1710000000000,
"resourcemanager.projects.list": 1710000000000,
},
},
};
testIamPermissionsStub.resolves({
passed: true,
allowed: mockPermissions,
missing: [],
});

await ensurePermissionsOrSetRole("test-project", "test@example.com", mockPermissions, mockRole);

expect(testIamPermissionsStub).to.have.been.calledOnceWith("test-project", mockPermissions);
});

it("should query API even if cached when force is true", async () => {
cacheStore["iamPermissionCache"] = {
"test-project": {
"test@example.com": {
"mcp.tools.call": 1710000000000,
"resourcemanager.projects.get": 1710000000000,
"resourcemanager.projects.list": 1710000000000,
},
},
};
testIamPermissionsStub.resolves({
passed: true,
allowed: mockPermissions,
missing: [],
});

await ensurePermissionsOrSetRole(
"test-project",
"test@example.com",
mockPermissions,
mockRole,
true,
);

expect(testIamPermissionsStub).to.have.been.calledOnceWith("test-project", mockPermissions);
});

it("should attempt to bind role and succeed/cache if testIamPermissions fails but setIamPolicy succeeds", async () => {
testIamPermissionsStub.resolves({
passed: false,
allowed: [],
missing: mockPermissions,
});
getIamPolicyStub.resolves({
bindings: [
{
role: "roles/viewer",
members: ["user:test@example.com"],
},
],
});
setIamPolicyStub.resolves({} as any);

Check warning on line 146 in src/ensurePermissions.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

await ensurePermissionsOrSetRole("test-project", "test@example.com", mockPermissions, mockRole);

expect(setIamPolicyStub).to.have.been.calledOnce;
expect(cacheStore["iamPermissionCache"]).to.deep.equal({
"test-project": {
"test@example.com": {
"mcp.tools.call": 1710000000000,
"resourcemanager.projects.get": 1710000000000,
"resourcemanager.projects.list": 1710000000000,
},
},
});
});

it("should throw FirebaseError with instructions if testIamPermissions fails and setIamPolicy fails", async () => {
testIamPermissionsStub.resolves({
passed: false,
allowed: [],
missing: mockPermissions,
});
getIamPolicyStub.resolves({
bindings: [
{
role: "roles/viewer",
members: ["user:test@example.com"],
},
],
});
setIamPolicyStub.rejects(new Error("Permission denied"));

await expect(
ensurePermissionsOrSetRole("test-project", "test@example.com", mockPermissions, mockRole),
).to.be.rejectedWith(
FirebaseError,
/Attempted to automatically bind the role roles\/mcp\.toolUser but failed/,
);

expect(cacheStore["iamPermissionCache"]).to.be.undefined;
});

it("should resolve serviceAccount prefix correctly when checking policy bindings", async () => {
testIamPermissionsStub.resolves({
passed: false,
allowed: [],
missing: mockPermissions,
});
getIamPolicyStub.resolves({
bindings: [
{
role: mockRole,
members: ["serviceAccount:sa@proj.iam.gserviceaccount.com"],
},
],
});

await ensurePermissionsOrSetRole(
"test-project",
"sa@proj.iam.gserviceaccount.com",
mockPermissions,
mockRole,
);

expect(getIamPolicyStub).to.have.been.calledOnce;
expect(setIamPolicyStub).to.not.have.been.called;
});

it("should resolve user prefix correctly even if email contains gserviceaccount.com as a substring", async () => {
testIamPermissionsStub.resolves({
passed: false,
allowed: [],
missing: mockPermissions,
});
getIamPolicyStub.resolves({
bindings: [
{
role: mockRole,
members: ["user:sa@proj.iam.gserviceaccount.com.fake.com"],
},
],
});

await ensurePermissionsOrSetRole(
"test-project",
"sa@proj.iam.gserviceaccount.com.fake.com",
mockPermissions,
mockRole,
);

expect(getIamPolicyStub).to.have.been.calledOnce;
expect(setIamPolicyStub).to.not.have.been.called;
});

it("should use customLogger for debugging if provided", async () => {
testIamPermissionsStub.resolves({
passed: true,
allowed: mockPermissions,
missing: [],
});
const customLogger = {
debug: sandbox.stub(),
};

await ensurePermissionsOrSetRole(
"test-project",
"test@example.com",
mockPermissions,
mockRole,
false,
customLogger,
);

expect(customLogger.debug).to.have.been.calledWith(
sinon.match(/ensurePermissionsOrSetRole called/),
);
expect(customLogger.debug).to.have.been.calledWith(
sinon.match(/Caching positive permissions check/),
);
});

it("should revoke/remove missing permissions from cache if check fails", async () => {
cacheStore["iamPermissionCache"] = {
"test-project": {
"test@example.com": {
"mcp.tools.call": 1710000000000,
"resourcemanager.projects.get": 1710000000000,
"resourcemanager.projects.list": 1710000000000,
},
},
};
testIamPermissionsStub.resolves({
passed: false,
allowed: [],
missing: ["mcp.tools.call"],
});
getIamPolicyStub.resolves({
bindings: [
{
role: "roles/viewer",
members: ["user:test@example.com"],
},
],
});
setIamPolicyStub.rejects(new Error("Permission denied"));

await expect(
ensurePermissionsOrSetRole(
"test-project",
"test@example.com",
mockPermissions,
mockRole,
true, // force to query API even though they are cached
),
).to.be.rejectedWith(FirebaseError);

// mcp.tools.call should be removed from cache since it was reported missing
expect(cacheStore["iamPermissionCache"]["test-project"]["test@example.com"]["mcp.tools.call"])

Check warning on line 303 in src/ensurePermissions.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access ["test-project"] on an `any` value
.to.be.undefined;
// other permissions that weren't reported missing should remain
expect(
cacheStore["iamPermissionCache"]["test-project"]["test@example.com"][

Check warning on line 307 in src/ensurePermissions.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access ["test-project"] on an `any` value
"resourcemanager.projects.get"
],
).to.equal(1710000000000);
});

it("should cache allowed permissions even if testIamPermissions fails and setIamPolicy fails", async () => {
testIamPermissionsStub.resolves({
passed: false,
allowed: ["resourcemanager.projects.get"],
missing: ["mcp.tools.call", "resourcemanager.projects.list"],
});
getIamPolicyStub.resolves({
bindings: [
{
role: "roles/viewer",
members: ["user:test@example.com"],
},
],
});
setIamPolicyStub.rejects(new Error("Permission denied"));

await expect(
ensurePermissionsOrSetRole("test-project", "test@example.com", mockPermissions, mockRole),
).to.be.rejectedWith(FirebaseError);

// allowed permission should be cached
expect(
cacheStore["iamPermissionCache"]["test-project"]["test@example.com"][
"resourcemanager.projects.get"
],
).to.equal(1710000000000);
// missing permissions should not be cached
expect(cacheStore["iamPermissionCache"]["test-project"]["test@example.com"]["mcp.tools.call"])
.to.be.undefined;
});
});
Loading
Loading