142 lines
4.4 KiB
JavaScript
142 lines
4.4 KiB
JavaScript
|
|
/**
|
||
|
|
* Alibaba Cloud STS (Security Token Service) integration.
|
||
|
|
*
|
||
|
|
* Issues temporary OSS credentials scoped to a specific user's prefix,
|
||
|
|
* so clients can upload directly to OSS without long-term AccessKeys.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const crypto = require("node:crypto");
|
||
|
|
|
||
|
|
const STS_ACCESS_KEY_ID = process.env.STS_ACCESS_KEY_ID || "";
|
||
|
|
const STS_ACCESS_KEY_SECRET = process.env.STS_ACCESS_KEY_SECRET || "";
|
||
|
|
const OSS_ROLE_ARN = process.env.OSS_ROLE_ARN || "";
|
||
|
|
const OSS_BUCKET = process.env.OSS_BUCKET || "";
|
||
|
|
const OSS_REGION = process.env.OSS_REGION || "";
|
||
|
|
|
||
|
|
const STS_ENDPOINT = "sts.aliyuncs.com";
|
||
|
|
const STS_API_VERSION = "2015-04-01";
|
||
|
|
const DEFAULT_DURATION_SECONDS = 900; // 15 minutes
|
||
|
|
const MAX_DURATION_SECONDS = 3600;
|
||
|
|
|
||
|
|
function isSTSConfigured() {
|
||
|
|
return !!(STS_ACCESS_KEY_ID && STS_ACCESS_KEY_SECRET && OSS_ROLE_ARN && OSS_BUCKET && OSS_REGION);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Build a session policy that restricts the assumed role to only
|
||
|
|
* access objects under users/<userId>/ in the target bucket.
|
||
|
|
*/
|
||
|
|
function buildSessionPolicy(userId) {
|
||
|
|
return JSON.stringify({
|
||
|
|
Version: "1",
|
||
|
|
Statement: [
|
||
|
|
{
|
||
|
|
Effect: "Allow",
|
||
|
|
Action: ["oss:PutObject", "oss:PutObjectAcl", "oss:GetObject", "oss:DeleteObject"],
|
||
|
|
Resource: [
|
||
|
|
`acs:oss:*:*:${OSS_BUCKET}/users/${userId}/*`,
|
||
|
|
`acs:oss:*:*:${OSS_BUCKET}/tmp/${userId}/*`,
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Effect: "Allow",
|
||
|
|
Action: ["oss:ListObjects"],
|
||
|
|
Resource: [`acs:oss:*:*:${OSS_BUCKET}`],
|
||
|
|
Condition: {
|
||
|
|
StringLike: {
|
||
|
|
"oss:Prefix": [`users/${userId}/*`, `tmp/${userId}/*`],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Percent-encode per RFC 3986 (same as AWS/Alibaba signing).
|
||
|
|
*/
|
||
|
|
function percentEncode(str) {
|
||
|
|
return encodeURIComponent(str)
|
||
|
|
.replace(/!/g, "%21")
|
||
|
|
.replace(/'/g, "%27")
|
||
|
|
.replace(/\(/g, "%28")
|
||
|
|
.replace(/\)/g, "%29")
|
||
|
|
.replace(/\*/g, "%2A");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Compute HMAC-SHA1 signature for STS API request.
|
||
|
|
*/
|
||
|
|
function computeSignature(method, params, accessKeySecret) {
|
||
|
|
const sortedKeys = Object.keys(params).sort();
|
||
|
|
const canonicalQuery = sortedKeys
|
||
|
|
.map((k) => `${percentEncode(k)}=${percentEncode(params[k])}`)
|
||
|
|
.join("&");
|
||
|
|
|
||
|
|
const stringToSign = `${method.toUpperCase()}&${percentEncode("/")}&${percentEncode(canonicalQuery)}`;
|
||
|
|
return crypto.createHmac("sha1", `${accessKeySecret}&`).update(stringToSign).digest("base64");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Call STS AssumeRole API to get temporary credentials for a user.
|
||
|
|
*
|
||
|
|
* @param {string|number} userId - The user ID to scope credentials to
|
||
|
|
* @param {number} [durationSeconds] - Credential lifetime (900-3600s)
|
||
|
|
* @returns {Promise<{accessKeyId, accessKeySecret, securityToken, expiration, bucket, region, ossPathPrefix}>}
|
||
|
|
*/
|
||
|
|
async function assumeRole(userId, durationSeconds = DEFAULT_DURATION_SECONDS) {
|
||
|
|
if (!isSTSConfigured()) {
|
||
|
|
throw new Error("STS is not configured on the server");
|
||
|
|
}
|
||
|
|
|
||
|
|
const safeUserId = String(userId).replace(/[^a-zA-Z0-9_-]/g, "");
|
||
|
|
if (!safeUserId) {
|
||
|
|
throw new Error("Invalid userId for STS token");
|
||
|
|
}
|
||
|
|
|
||
|
|
const duration = Math.min(Math.max(durationSeconds, 900), MAX_DURATION_SECONDS);
|
||
|
|
|
||
|
|
const params = {
|
||
|
|
Action: "AssumeRole",
|
||
|
|
Version: STS_API_VERSION,
|
||
|
|
RoleArn: OSS_ROLE_ARN,
|
||
|
|
RoleSessionName: `omniai-user-${safeUserId}-${Date.now()}`,
|
||
|
|
DurationSeconds: String(duration),
|
||
|
|
Policy: buildSessionPolicy(safeUserId),
|
||
|
|
Format: "JSON",
|
||
|
|
AccessKeyId: STS_ACCESS_KEY_ID,
|
||
|
|
SignatureMethod: "HMAC-SHA1",
|
||
|
|
SignatureVersion: "1.0",
|
||
|
|
SignatureNonce: crypto.randomUUID(),
|
||
|
|
Timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
||
|
|
};
|
||
|
|
|
||
|
|
params.Signature = computeSignature("GET", params, STS_ACCESS_KEY_SECRET);
|
||
|
|
|
||
|
|
const queryString = Object.entries(params)
|
||
|
|
.map(([k, v]) => `${percentEncode(k)}=${percentEncode(String(v))}`)
|
||
|
|
.join("&");
|
||
|
|
|
||
|
|
const url = `https://${STS_ENDPOINT}/?${queryString}`;
|
||
|
|
|
||
|
|
const response = await fetch(url, { method: "GET" });
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (data.RequestId && data.Credentials) {
|
||
|
|
const cred = data.Credentials;
|
||
|
|
return {
|
||
|
|
accessKeyId: cred.AccessKeyId,
|
||
|
|
accessKeySecret: cred.AccessKeySecret,
|
||
|
|
securityToken: cred.SecurityToken,
|
||
|
|
expiration: cred.Expiration,
|
||
|
|
bucket: OSS_BUCKET,
|
||
|
|
region: OSS_REGION,
|
||
|
|
ossPathPrefix: `users/${safeUserId}/assets/`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error(data.Message || `STS AssumeRole failed: ${JSON.stringify(data)}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { assumeRole, isSTSConfigured };
|