1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
|
const express = require("express");
const {
S3Client,
HeadObjectCommand,
GetObjectCommand
} = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
const { createRemoteJWKSet, jwtVerify } = require("jose");
const app = express();
app.use(express.json({ limit: "1mb" }));
const PORT = Number(process.env.PORT || 8081);
const OIDC_ISSUER = process.env.OIDC_ISSUER;
const OIDC_AUDIENCE = process.env.OIDC_AUDIENCE;
const OIDC_JWKS_URL = process.env.OIDC_JWKS_URL;
const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || OIDC_AUDIENCE;
const RBAC_ROLE_PREFIX = process.env.RBAC_ROLE_PREFIX || "media_reader:folder:";
const RBAC_ROLE_ALL = process.env.RBAC_ROLE_ALL || "media_reader:all";
const S3_BUCKET = process.env.S3_BUCKET;
const PRESIGN_TTL_SECONDS = Number(process.env.PRESIGN_TTL_SECONDS || 120);
if (!OIDC_ISSUER || !OIDC_AUDIENCE || !OIDC_JWKS_URL || !S3_BUCKET) {
// eslint-disable-next-line no-console
console.error("Configuration manquante: OIDC_ISSUER/OIDC_AUDIENCE/OIDC_JWKS_URL/S3_BUCKET");
process.exit(1);
}
const jwks = createRemoteJWKSet(new URL(OIDC_JWKS_URL));
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || "us-east-1",
forcePathStyle: String(process.env.S3_FORCE_PATH_STYLE || "true") === "true",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY || "",
secretAccessKey: process.env.S3_SECRET_KEY || ""
}
});
function extractBearerToken(req) {
const h = req.headers.authorization || "";
if (!h.startsWith("Bearer ")) return null;
return h.slice("Bearer ".length).trim();
}
function getClientRoles(payload) {
const byClient = payload?.resource_access?.[OIDC_CLIENT_ID]?.roles;
if (Array.isArray(byClient)) return byClient;
const byAudience = payload?.resource_access?.[OIDC_AUDIENCE]?.roles;
if (Array.isArray(byAudience)) return byAudience;
return [];
}
function computePermissions(roles) {
const allowAll = roles.includes(RBAC_ROLE_ALL);
const allowedPrefixes = allowAll
? []
: roles
.filter((r) => r.startsWith(RBAC_ROLE_PREFIX))
.map((r) => `${r.slice(RBAC_ROLE_PREFIX.length).replace(/^\/+/, "")}/`);
return { allowAll, allowedPrefixes };
}
function isObjectAllowed(objectKey, permissions) {
if (permissions.allowAll) return true;
return permissions.allowedPrefixes.some((p) => objectKey.startsWith(p));
}
async function requireAuth(req, res, next) {
const token = extractBearerToken(req);
if (!token) {
return res.status(401).json({ message: "Bearer token requis" });
}
try {
const { payload } = await jwtVerify(token, jwks, {
issuer: OIDC_ISSUER,
audience: OIDC_AUDIENCE
});
const roles = getClientRoles(payload);
const permissions = computePermissions(roles);
req.user = {
sub: payload.sub || "unknown",
roles,
permissions
};
return next();
} catch (_err) {
return res.status(401).json({ message: "Token invalide" });
}
}
app.get("/health", (_req, res) => {
res.json({
status: "ok",
service: "media-access-api",
audience: OIDC_AUDIENCE
});
});
app.get("/v1/permissions", requireAuth, (req, res) => {
res.json({
subject: req.user.sub,
allowAll: req.user.permissions.allowAll,
allowedPrefixes: req.user.permissions.allowedPrefixes
});
});
app.post("/v1/presign", requireAuth, async (req, res) => {
const objectKey = req.body?.objectKey;
if (!objectKey || typeof objectKey !== "string") {
return res.status(400).json({ message: "Champ objectKey requis" });
}
if (!isObjectAllowed(objectKey, req.user.permissions)) {
return res.status(403).json({ message: "Object non autorise" });
}
try {
await s3.send(
new HeadObjectCommand({
Bucket: S3_BUCKET,
Key: objectKey
})
);
} catch (_err) {
return res.status(404).json({ message: "Object introuvable" });
}
try {
const command = new GetObjectCommand({
Bucket: S3_BUCKET,
Key: objectKey
});
const url = await getSignedUrl(s3, command, {
expiresIn: PRESIGN_TTL_SECONDS
});
return res.json({ url, expiresIn: PRESIGN_TTL_SECONDS });
} catch (_err) {
return res.status(500).json({ message: "Erreur generation URL signee" });
}
});
app.listen(PORT, () => {
// eslint-disable-next-line no-console
console.log(`media-access-api listening on ${PORT}`);
});
|