apiServer = {
const app = Router();
app.use(async (req, res, next) => {
try {
req.auth = await verifyIdToken(firebase, req.headers["idtoken"]);
next();
} catch (err) {
res.status(403).send("Auth: " + err.message);
}
});
app.get("/", (req, res) => {
res.status(200).end();
});
app.all("/subdomains/:subdomain/secrets**", async (req, res, next) => {
const service_account =
req.context.secrets["endpointservices_secretadmin_service_account_key"];
const access_token = await getAccessTokenFromServiceAccount(
service_account
);
await signinWithAccessToken(firebase, access_token);
if (req.auth.uid.startsWith("observablehq|")) {
if (req.auth["observablehq.com"][req.params.subdomain] !== "admin") {
res.status(403).send("Not owner of subdomain");
}
} else {
const ownersQuery = await firebase
.firestore()
.collection("/services/ownership/owners")
.where("subdomain", "==", req.params.subdomain)
.where("uid", "==", req.auth.uid)
.limit(1)
.get();
if (ownersQuery.empty) {
res.status(403).send("Not owner of subdomain");
}
}
req.secretManagerClient = new SecretManagerServiceClient({
projectId: firebaseConfig.projectId,
access_token: access_token
});
next();
});
app.get("/subdomains/:subdomain/secrets", async (req, res) => {
try {
// This won't scale but we have no simple way of searching
// https://stackoverflow.com/questions/65044519/is-there-a-way-of-querying-google-secrets-manager
const secrets = (
await req.secretManagerClient.listSecrets({
parent: "projects/1986724398",
pageSize: 25000
})
)[0];
// Only return results prefixed by the subdomain the user has permission to
const results = secrets
.map((s) => ({ name: s.name.split("/")[3] }))
.filter((secret) => secret.name.startsWith(`${req.params.subdomain}_`));
res.json(results);
} catch (err) {
res.status(500).send(err.message);
}
});
app.put("/subdomains/:subdomain/secrets/:secret", async (req, res) => {
try {
// This won't scale but we have no simple way of searching
// https://stackoverflow.com/questions/65044519/is-there-a-way-of-querying-google-secrets-manager
const secretId = req.params.secret;
if (!secretId.startsWith(req.params.subdomain + "_")) {
res.status(400).send("secret name must be prefixed by subdomain");
}
const value = JSON.parse(req.body);
try {
// It might already been created in which case we really just want to create a new version
const secret = (
await req.secretManagerClient.createSecret({
parent: "projects/1986724398",
secretId,
secret: {
labels: { subdomain: req.params.subdomain },
replication: { automatic: {} }
}
})
)[0];
} catch (err) {
// It's ok to fail this one
}
const version = await req.secretManagerClient.addSecretVersion({
parent: `projects/1986724398/secrets/${secretId}`,
payload: {
data: value
}
});
// TODO, we should delete the previous version too
res.status(204).end();
} catch (err) {
res.status(500).send(err.message);
}
});
app.delete("/subdomains/:subdomain/secrets/:secret", async (req, res) => {
try {
// This won't scale but we have no simple way of searching
// https://stackoverflow.com/questions/65044519/is-there-a-way-of-querying-google-secrets-manager
const secretId = req.params.secret;
if (!secretId.startsWith(req.params.subdomain + "_")) {
res.status(400).send("secret name must be prefixed by subdomain");
}
await req.secretManagerClient.deleteSecret({
name: `projects/1986724398/secrets/${secretId}`
});
res.status(204).end();
} catch (err) {
res.status(500).send(err.message);
}
});
app.use((req, res) => {
res
.status(404)
.send(
"No route found in the https://observablehq.com/@endpointservices/secrets API"
);
});
return app;
}