Initial commit: OmniAI backend server
This commit is contained in:
+141
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user