mirror of
https://github.com/zhigang1992/firebase-tools.git
synced 2026-04-30 04:45:27 +08:00
HTTP Invoker for v2 functions (#3683)
* adding invoker to replace allUsers * fixing build issues * adding tests * update function names, clean up comments * add invoker option when parsing function from SDK * fix service account format for IAM API * fixing ts issues and added check for full service account * cleaning up code for readability * merged iam policy creation and set policy into setInvoker * cleaning up old function and adding comments * added unit tests * fixing api version & removing import * adding setInvoker to cloud run * cleaning up * lint fix * fix reference * added v2 invoker functions * fixing pr comments
This commit is contained in:
@@ -128,7 +128,7 @@ export function createFunctionTask(
|
|||||||
await gcf.setInvokerCreate(params.projectId, fnName, invoker);
|
await gcf.setInvokerCreate(params.projectId, fnName, invoker);
|
||||||
} else {
|
} else {
|
||||||
const serviceName = (cloudFunction as gcfV2.CloudFunction).serviceConfig.service!;
|
const serviceName = (cloudFunction as gcfV2.CloudFunction).serviceConfig.service!;
|
||||||
cloudrun.setIamPolicy(serviceName, cloudrun.DEFAULT_PUBLIC_POLICY);
|
cloudrun.setInvokerCreate(params.projectId, serviceName, invoker);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
params.errorHandler.record("error", fnName, "set invoker", err.message);
|
params.errorHandler.record("error", fnName, "set invoker", err.message);
|
||||||
@@ -197,7 +197,8 @@ export function updateFunctionTask(
|
|||||||
if (fn.platform === "gcfv1") {
|
if (fn.platform === "gcfv1") {
|
||||||
await gcf.setInvokerUpdate(params.projectId, fnName, fn.invoker);
|
await gcf.setInvokerUpdate(params.projectId, fnName, fn.invoker);
|
||||||
} else {
|
} else {
|
||||||
// TODO: gcfv2
|
const serviceName = (cloudFunction as gcfV2.CloudFunction).serviceConfig.service!;
|
||||||
|
cloudrun.setInvokerUpdate(params.projectId, serviceName, fn.invoker);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
params.errorHandler.record("error", fnName, "set invoker", err.message);
|
params.errorHandler.record("error", fnName, "set invoker", err.message);
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ export async function setInvokerUpdate(
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
currentInvokerBinding &&
|
currentInvokerBinding &&
|
||||||
_.isEqual(new Set(currentInvokerBinding.members), new Set(invokerMembers))
|
JSON.stringify(currentInvokerBinding.members.sort()) === JSON.stringify(invokerMembers.sort())
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/gcp/run.ts
114
src/gcp/run.ts
@@ -2,6 +2,8 @@ import { Client } from "../apiv2";
|
|||||||
import { FirebaseError } from "../error";
|
import { FirebaseError } from "../error";
|
||||||
import { runOrigin } from "../api";
|
import { runOrigin } from "../api";
|
||||||
import * as proto from "./proto";
|
import * as proto from "./proto";
|
||||||
|
import * as iam from "./iam";
|
||||||
|
import * as _ from "lodash";
|
||||||
|
|
||||||
const API_VERSION = "v1";
|
const API_VERSION = "v1";
|
||||||
|
|
||||||
@@ -105,22 +107,12 @@ export interface TrafficTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IamPolicy {
|
export interface IamPolicy {
|
||||||
version: number;
|
version?: number;
|
||||||
bindings: Record<string, unknown>[];
|
bindings?: iam.Binding[];
|
||||||
auditConfigs?: Record<string, unknown>[];
|
auditConfigs?: Record<string, unknown>[];
|
||||||
etag?: string;
|
etag?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_PUBLIC_POLICY = {
|
|
||||||
version: 3,
|
|
||||||
bindings: [
|
|
||||||
{
|
|
||||||
role: "roles/run.invoker",
|
|
||||||
members: ["allUsers"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getService(name: string): Promise<Service> {
|
export async function getService(name: string): Promise<Service> {
|
||||||
try {
|
try {
|
||||||
const response = await client.get<Service>(name);
|
const response = await client.get<Service>(name);
|
||||||
@@ -148,16 +140,20 @@ export async function replaceService(name: string, service: Service): Promise<Se
|
|||||||
* @param name Fully qualified name of the Service.
|
* @param name Fully qualified name of the Service.
|
||||||
* @param policy The [policy](https://cloud.google.com/run/docs/reference/rest/v1/projects.locations.services/setIamPolicy) to set.
|
* @param policy The [policy](https://cloud.google.com/run/docs/reference/rest/v1/projects.locations.services/setIamPolicy) to set.
|
||||||
*/
|
*/
|
||||||
export async function setIamPolicy(name: string, policy: IamPolicy): Promise<void> {
|
export async function setIamPolicy(
|
||||||
|
name: string,
|
||||||
|
policy: iam.Policy,
|
||||||
|
httpClient: Client = client
|
||||||
|
): Promise<void> {
|
||||||
// Cloud Run has an atypical REST binding for SetIamPolicy. Instead of making the body a policy and
|
// Cloud Run has an atypical REST binding for SetIamPolicy. Instead of making the body a policy and
|
||||||
// the update mask a query parameter (e.g. Cloud Functions v1) the request body is the literal
|
// the update mask a query parameter (e.g. Cloud Functions v1) the request body is the literal
|
||||||
// proto.
|
// proto.
|
||||||
interface Request {
|
interface Request {
|
||||||
policy: IamPolicy;
|
policy: iam.Policy;
|
||||||
updateMask: string;
|
updateMask: string;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.post<Request, IamPolicy>(`${name}:setIamPolicy`, {
|
await httpClient.post<Request, IamPolicy>(`${name}:setIamPolicy`, {
|
||||||
policy,
|
policy,
|
||||||
updateMask: proto.fieldMasks(policy).join(","),
|
updateMask: proto.fieldMasks(policy).join(","),
|
||||||
});
|
});
|
||||||
@@ -167,3 +163,91 @@ export async function setIamPolicy(name: string, policy: IamPolicy): Promise<voi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getIamPolicy(
|
||||||
|
serviceName: string,
|
||||||
|
httpClient: Client = client
|
||||||
|
): Promise<IamPolicy> {
|
||||||
|
try {
|
||||||
|
const response = await httpClient.get<IamPolicy>(`${serviceName}:getIamPolicy`);
|
||||||
|
return response.body;
|
||||||
|
} catch (err) {
|
||||||
|
throw new FirebaseError(`Failed to get the IAM Policy on the Service ${serviceName}`, {
|
||||||
|
original: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current IAM policy for the run service and overrides the invoker role with the supplied invoker members
|
||||||
|
* @param projectId id of the project
|
||||||
|
* @param serviceName cloud run service
|
||||||
|
* @param invoker an array of invoker strings
|
||||||
|
*
|
||||||
|
* @throws {@link FirebaseError} on an empty invoker, when the IAM Polciy fails to be grabbed or set
|
||||||
|
*/
|
||||||
|
export async function setInvokerCreate(
|
||||||
|
projectId: string,
|
||||||
|
serviceName: string,
|
||||||
|
invoker: string[],
|
||||||
|
httpClient: Client = client // for unit testing
|
||||||
|
) {
|
||||||
|
if (invoker.length == 0) {
|
||||||
|
throw new FirebaseError("Invoker cannot be an empty array");
|
||||||
|
}
|
||||||
|
const invokerMembers = proto.getInvokerMembers(invoker, projectId);
|
||||||
|
const invokerRole = "roles/run.invoker";
|
||||||
|
const bindings = [{ role: invokerRole, members: invokerMembers }];
|
||||||
|
|
||||||
|
const policy: iam.Policy = {
|
||||||
|
bindings: bindings,
|
||||||
|
etag: "",
|
||||||
|
version: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
await setIamPolicy(serviceName, policy, httpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current IAM policy for the run service and overrides the invoker role with the supplied invoker members
|
||||||
|
* @param projectId id of the project
|
||||||
|
* @param serviceName cloud run service
|
||||||
|
* @param invoker an array of invoker strings
|
||||||
|
*
|
||||||
|
* @throws {@link FirebaseError} on an empty invoker, when the IAM Polciy fails to be grabbed or set
|
||||||
|
*/
|
||||||
|
export async function setInvokerUpdate(
|
||||||
|
projectId: string,
|
||||||
|
serviceName: string,
|
||||||
|
invoker: string[],
|
||||||
|
httpClient: Client = client // for unit testing
|
||||||
|
) {
|
||||||
|
if (invoker.length == 0) {
|
||||||
|
throw new FirebaseError("Invoker cannot be an empty array");
|
||||||
|
}
|
||||||
|
const invokerMembers = proto.getInvokerMembers(invoker, projectId);
|
||||||
|
const invokerRole = "roles/run.invoker";
|
||||||
|
const currentPolicy = await getIamPolicy(serviceName, httpClient);
|
||||||
|
const currentInvokerBinding = currentPolicy.bindings?.find(
|
||||||
|
(binding) => binding.role === invokerRole
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
currentInvokerBinding &&
|
||||||
|
JSON.stringify(currentInvokerBinding.members.sort()) === JSON.stringify(invokerMembers.sort())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindings = (currentPolicy.bindings || []).filter((binding) => binding.role !== invokerRole);
|
||||||
|
bindings.push({
|
||||||
|
role: invokerRole,
|
||||||
|
members: invokerMembers,
|
||||||
|
});
|
||||||
|
|
||||||
|
const policy: iam.Policy = {
|
||||||
|
bindings: bindings,
|
||||||
|
etag: currentPolicy.etag || "",
|
||||||
|
version: 3,
|
||||||
|
};
|
||||||
|
await setIamPolicy(serviceName, policy, httpClient);
|
||||||
|
}
|
||||||
|
|||||||
273
src/test/gcp/run.spec.ts
Normal file
273
src/test/gcp/run.spec.ts
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
import { expect } from "chai";
|
||||||
|
import * as sinon from "sinon";
|
||||||
|
import * as run from "../../gcp/run";
|
||||||
|
import { Client } from "../../apiv2";
|
||||||
|
|
||||||
|
describe("run", () => {
|
||||||
|
describe("setInvokerCreate", () => {
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
let apiRequestStub: sinon.SinonStub;
|
||||||
|
let client: Client;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new Client({
|
||||||
|
urlPrefix: "origin",
|
||||||
|
auth: true,
|
||||||
|
apiVersion: "v1",
|
||||||
|
});
|
||||||
|
sandbox = sinon.createSandbox();
|
||||||
|
apiRequestStub = sandbox.stub(client, "post").throws("Unexpected API post call");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject on emtpy invoker array", async () => {
|
||||||
|
await expect(run.setInvokerCreate("project", "service", [], client)).to.be.rejected;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject if the setting the IAM policy fails", async () => {
|
||||||
|
apiRequestStub.onFirstCall().throws("Error calling set api.");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
run.setInvokerCreate("project", "service", ["public"], client)
|
||||||
|
).to.be.rejectedWith("Failed to set the IAM Policy on the Service service");
|
||||||
|
expect(apiRequestStub).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set a private policy on a function", async () => {
|
||||||
|
apiRequestStub.onFirstCall().callsFake((path: string, json: any) => {
|
||||||
|
expect(json.policy).to.deep.eq({
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
role: "roles/run.invoker",
|
||||||
|
members: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
etag: "",
|
||||||
|
version: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(run.setInvokerCreate("project", "service", ["private"], client)).to.not.be
|
||||||
|
.rejected;
|
||||||
|
expect(apiRequestStub).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set a public policy on a function", async () => {
|
||||||
|
apiRequestStub.onFirstCall().callsFake((path: string, json: any) => {
|
||||||
|
expect(json.policy).to.deep.eq({
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
role: "roles/run.invoker",
|
||||||
|
members: ["allUsers"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
etag: "",
|
||||||
|
version: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(run.setInvokerCreate("project", "service", ["public"], client)).to.not.be
|
||||||
|
.rejected;
|
||||||
|
expect(apiRequestStub).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the policy with a set of invokers with active policies", async () => {
|
||||||
|
apiRequestStub.onFirstCall().callsFake((path: string, json: any) => {
|
||||||
|
json.policy.bindings[0].members.sort();
|
||||||
|
expect(json.policy.bindings[0].members).to.deep.eq([
|
||||||
|
"serviceAccount:service-account1@project.iam.gserviceaccount.com",
|
||||||
|
"serviceAccount:service-account2@project.iam.gserviceaccount.com",
|
||||||
|
"serviceAccount:service-account3@project.iam.gserviceaccount.com",
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
run.setInvokerCreate(
|
||||||
|
"project",
|
||||||
|
"service",
|
||||||
|
[
|
||||||
|
"service-account1@",
|
||||||
|
"service-account2@project.iam.gserviceaccount.com",
|
||||||
|
"service-account3@",
|
||||||
|
],
|
||||||
|
client
|
||||||
|
)
|
||||||
|
).to.not.be.rejected;
|
||||||
|
expect(apiRequestStub).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setInvokerUpdate", () => {
|
||||||
|
describe("setInvokerCreate", () => {
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
let apiPostStub: sinon.SinonStub;
|
||||||
|
let apiGetStub: sinon.SinonStub;
|
||||||
|
let client: Client;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new Client({
|
||||||
|
urlPrefix: "origin",
|
||||||
|
auth: true,
|
||||||
|
apiVersion: "v1",
|
||||||
|
});
|
||||||
|
sandbox = sinon.createSandbox();
|
||||||
|
apiPostStub = sandbox.stub(client, "post").throws("Unexpected API post call");
|
||||||
|
apiGetStub = sandbox.stub(client, "get").throws("Unexpected API get call");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject on emtpy invoker array", async () => {
|
||||||
|
await expect(run.setInvokerUpdate("project", "service", [])).to.be.rejected;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject if the getting the IAM policy fails", async () => {
|
||||||
|
apiGetStub.onFirstCall().throws("Error calling get api.");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
run.setInvokerUpdate("project", "service", ["public"], client)
|
||||||
|
).to.be.rejectedWith("Failed to get the IAM Policy on the Service service");
|
||||||
|
|
||||||
|
expect(apiGetStub).to.be.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject if the setting the IAM policy fails", async () => {
|
||||||
|
apiGetStub.resolves({ body: {} });
|
||||||
|
apiPostStub.throws("Error calling set api.");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
run.setInvokerUpdate("project", "service", ["public"], client)
|
||||||
|
).to.be.rejectedWith("Failed to set the IAM Policy on the Service service");
|
||||||
|
expect(apiGetStub).to.be.calledOnce;
|
||||||
|
expect(apiPostStub).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set a basic policy on a function without any polices", async () => {
|
||||||
|
apiGetStub.onFirstCall().resolves({ body: {} });
|
||||||
|
apiPostStub.onFirstCall().callsFake((path: string, json: any) => {
|
||||||
|
expect(json.policy).to.deep.eq({
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
role: "roles/run.invoker",
|
||||||
|
members: ["allUsers"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
etag: "",
|
||||||
|
version: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(run.setInvokerUpdate("project", "service", ["public"], client)).to.not.be
|
||||||
|
.rejected;
|
||||||
|
expect(apiGetStub).to.be.calledOnce;
|
||||||
|
expect(apiPostStub).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the policy with private invoker with active policies", async () => {
|
||||||
|
apiGetStub.onFirstCall().resolves({
|
||||||
|
body: {
|
||||||
|
bindings: [
|
||||||
|
{ role: "random-role", members: ["user:pineapple"] },
|
||||||
|
{ role: "roles/run.invoker", members: ["some-service-account"] },
|
||||||
|
],
|
||||||
|
etag: "1234",
|
||||||
|
version: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
apiPostStub.onFirstCall().callsFake((path: string, json: any) => {
|
||||||
|
expect(json.policy).to.deep.eq({
|
||||||
|
bindings: [
|
||||||
|
{ role: "random-role", members: ["user:pineapple"] },
|
||||||
|
{ role: "roles/run.invoker", members: [] },
|
||||||
|
],
|
||||||
|
etag: "1234",
|
||||||
|
version: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(run.setInvokerUpdate("project", "service", ["private"], client)).to.not.be
|
||||||
|
.rejected;
|
||||||
|
expect(apiGetStub).to.be.calledOnce;
|
||||||
|
expect(apiPostStub).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the policy with a set of invokers with active policies", async () => {
|
||||||
|
apiGetStub.onFirstCall().resolves({ body: {} });
|
||||||
|
apiPostStub.onFirstCall().callsFake((path: string, json: any) => {
|
||||||
|
json.policy.bindings[0].members.sort();
|
||||||
|
expect(json.policy.bindings[0].members).to.deep.eq([
|
||||||
|
"serviceAccount:service-account1@project.iam.gserviceaccount.com",
|
||||||
|
"serviceAccount:service-account2@project.iam.gserviceaccount.com",
|
||||||
|
"serviceAccount:service-account3@project.iam.gserviceaccount.com",
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
run.setInvokerUpdate(
|
||||||
|
"project",
|
||||||
|
"service",
|
||||||
|
[
|
||||||
|
"service-account1@",
|
||||||
|
"service-account2@project.iam.gserviceaccount.com",
|
||||||
|
"service-account3@",
|
||||||
|
],
|
||||||
|
client
|
||||||
|
)
|
||||||
|
).to.not.be.rejected;
|
||||||
|
expect(apiGetStub).to.be.calledOnce;
|
||||||
|
expect(apiPostStub).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not set the policy if the set of invokers is the same as the current invokers", async () => {
|
||||||
|
apiGetStub.onFirstCall().resolves({
|
||||||
|
body: {
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
role: "roles/run.invoker",
|
||||||
|
members: [
|
||||||
|
"serviceAccount:service-account1@project.iam.gserviceaccount.com",
|
||||||
|
"serviceAccount:service-account3@project.iam.gserviceaccount.com",
|
||||||
|
"serviceAccount:service-account2@project.iam.gserviceaccount.com",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
etag: "1234",
|
||||||
|
version: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
run.setInvokerUpdate(
|
||||||
|
"project",
|
||||||
|
"service",
|
||||||
|
[
|
||||||
|
"service-account2@project.iam.gserviceaccount.com",
|
||||||
|
"service-account3@",
|
||||||
|
"service-account1@",
|
||||||
|
],
|
||||||
|
client
|
||||||
|
)
|
||||||
|
).to.not.be.rejected;
|
||||||
|
expect(apiGetStub).to.be.calledOnce;
|
||||||
|
expect(apiPostStub).to.not.be.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user