feat(device-manager): trust Hub owner scopes
This commit is contained in:
@@ -8,6 +8,7 @@ export function createDeviceManagerAuth({
|
||||
env = process.env,
|
||||
fetchImpl = fetch,
|
||||
now = Date.now,
|
||||
internalToken: providedInternalToken,
|
||||
} = {}) {
|
||||
const authRequired = booleanValue(
|
||||
env.NODEDC_DEVICE_MANAGER_AUTH_REQUIRED,
|
||||
@@ -17,7 +18,9 @@ export function createDeviceManagerAuth({
|
||||
const launcherBaseUrl = baseUrl(env.NODEDC_LAUNCHER_BASE_URL, "http://127.0.0.1:5173");
|
||||
const launcherInternalUrl = baseUrl(env.NODEDC_LAUNCHER_INTERNAL_URL, launcherBaseUrl);
|
||||
const internalToken = textValue(
|
||||
env.NODEDC_INTERNAL_ACCESS_TOKEN || env.NODEDC_PLATFORM_SERVICE_TOKEN,
|
||||
providedInternalToken
|
||||
|| env.NODEDC_INTERNAL_ACCESS_TOKEN
|
||||
|| env.NODEDC_PLATFORM_SERVICE_TOKEN,
|
||||
"",
|
||||
);
|
||||
const sessionCookie = textValue(
|
||||
@@ -66,6 +69,7 @@ export function createDeviceManagerAuth({
|
||||
sessions.set(id, {
|
||||
id,
|
||||
user: handoff.user,
|
||||
access: handoff.access,
|
||||
launcherSessionId: handoff.launcherSessionId,
|
||||
expiresAt: createdAt + sessionTtlMs,
|
||||
validatedAt: createdAt,
|
||||
@@ -116,6 +120,7 @@ export function createDeviceManagerAuth({
|
||||
}
|
||||
createSession(response, {
|
||||
user: result.body.user,
|
||||
access: result.body.access,
|
||||
launcherSessionId: result.body.launcherSessionId ?? null,
|
||||
});
|
||||
return redirect(response, nextPath);
|
||||
@@ -156,6 +161,7 @@ export function createDeviceManagerAuth({
|
||||
const { response: upstream, body } = await session.validationInFlight;
|
||||
if (upstream.ok && body?.ok === true && body.active === true) {
|
||||
session.user = body.user || session.user;
|
||||
session.access = body.access;
|
||||
session.validatedAt = now();
|
||||
return attachSession(request, session);
|
||||
}
|
||||
@@ -199,7 +205,7 @@ export function createDeviceManagerAuth({
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const access = resolveAccess(session.user);
|
||||
const access = resolveAccess(session.user, session.access, { allowLegacy: !authRequired });
|
||||
if (!access.allowed) {
|
||||
sendJson(response, 403, {
|
||||
ok: false,
|
||||
@@ -222,7 +228,9 @@ export function createDeviceManagerAuth({
|
||||
|
||||
function currentContext(request) {
|
||||
const user = request.nodedcDeviceManagerSession?.user;
|
||||
const access = request.nodedcDeviceManagerAccess ?? resolveAccess(user);
|
||||
const trustedAccess = request.nodedcDeviceManagerSession?.access;
|
||||
const access = request.nodedcDeviceManagerAccess
|
||||
?? resolveAccess(user, trustedAccess, { allowLegacy: !authRequired });
|
||||
if (!user || !access.allowed) return null;
|
||||
const id = cleanOpaque(user.id || user.subject || user.sub);
|
||||
if (!id) return null;
|
||||
@@ -232,9 +240,6 @@ export function createDeviceManagerAuth({
|
||||
.slice(0, 240);
|
||||
const avatar = String(user.avatarUrl || user.avatar_url || user.picture || "").trim();
|
||||
const userRef = `user:${id}`;
|
||||
const ownerScopes = ["admin", "owner"].includes(access.hubRole)
|
||||
? [{ scopeKind: "personal", ownerRef: userRef, displayName }]
|
||||
: [];
|
||||
return {
|
||||
user: {
|
||||
id,
|
||||
@@ -247,7 +252,7 @@ export function createDeviceManagerAuth({
|
||||
userRef,
|
||||
hubRole: access.hubRole,
|
||||
groupRefs: access.groups.map((group) => `group:${group}`),
|
||||
ownerScopes,
|
||||
ownerScopes: access.ownerScopes,
|
||||
},
|
||||
profileUrl: new URL("/profile", launcherBaseUrl).toString(),
|
||||
};
|
||||
@@ -266,6 +271,7 @@ export function createDeviceManagerAuth({
|
||||
|
||||
return {
|
||||
authRequired,
|
||||
internalAccessConfigured: Boolean(internalToken),
|
||||
serviceSlug,
|
||||
authorize,
|
||||
currentContext,
|
||||
@@ -274,16 +280,19 @@ export function createDeviceManagerAuth({
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAccess(user) {
|
||||
function resolveAccess(user, trustedAccess, { allowLegacy = false } = {}) {
|
||||
if (!user || typeof user !== "object") {
|
||||
return { allowed: false, blocked: false, hubRole: "viewer", groups: [] };
|
||||
return deniedAccess();
|
||||
}
|
||||
const groups = normalizedGroups(user);
|
||||
if (groups.includes("nodedc:device-core:blocked")) {
|
||||
return { allowed: false, blocked: true, hubRole: "viewer", groups };
|
||||
return { ...deniedAccess(groups), blocked: true };
|
||||
}
|
||||
const id = cleanOpaque(user.id || user.subject || user.sub);
|
||||
if (!id) return { allowed: false, blocked: false, hubRole: "viewer", groups };
|
||||
if (!id) return deniedAccess(groups);
|
||||
const claims = normalizeTrustedAccess(trustedAccess, id);
|
||||
if (claims) return { ...claims, blocked: false, groups };
|
||||
if (!allowLegacy) return deniedAccess(groups);
|
||||
const hubRole = id === "user_root" || groups.includes("nodedc:superadmin")
|
||||
? "owner"
|
||||
: groups.includes("nodedc:device-core:admin") || groups.includes("nodedc:launcher:admin")
|
||||
@@ -291,7 +300,57 @@ function resolveAccess(user) {
|
||||
: groups.includes("nodedc:device-core:viewer")
|
||||
? "viewer"
|
||||
: "member";
|
||||
return { allowed: true, blocked: false, hubRole, groups };
|
||||
const ownerScopes = ["admin", "owner"].includes(hubRole)
|
||||
? [{
|
||||
scopeKind: "personal",
|
||||
ownerRef: `user:${id}`,
|
||||
displayName: String(user.name || user.displayName || user.email || id).trim().slice(0, 240),
|
||||
}]
|
||||
: [];
|
||||
return { allowed: true, blocked: false, hubRole, groups, ownerScopes };
|
||||
}
|
||||
|
||||
function normalizeTrustedAccess(input, userId) {
|
||||
if (!input || typeof input !== "object" || input.allowed !== true) return null;
|
||||
const hubRole = ["viewer", "member", "admin", "owner"].includes(input.hubRole)
|
||||
? input.hubRole
|
||||
: null;
|
||||
if (!hubRole || !Array.isArray(input.ownerScopes)) return null;
|
||||
const ownerScopes = [];
|
||||
for (const item of input.ownerScopes) {
|
||||
if (!item || typeof item !== "object") return null;
|
||||
const scopeKind = item.scopeKind === "company" || item.scopeKind === "personal"
|
||||
? item.scopeKind
|
||||
: null;
|
||||
const ownerRef = cleanOpaque(item.ownerRef);
|
||||
const validOwner = scopeKind === "personal"
|
||||
? ownerRef === `user:${userId}`
|
||||
: ownerRef?.startsWith("client:") && ownerRef.length > "client:".length;
|
||||
if (!scopeKind || !validOwner) return null;
|
||||
ownerScopes.push({
|
||||
scopeKind,
|
||||
ownerRef,
|
||||
displayName: String(item.displayName || ownerRef).trim().slice(0, 240),
|
||||
});
|
||||
}
|
||||
return {
|
||||
allowed: true,
|
||||
hubRole,
|
||||
ownerScopes: [...new Map(ownerScopes.map((scope) => [
|
||||
`${scope.scopeKind}\0${scope.ownerRef}`,
|
||||
scope,
|
||||
])).values()],
|
||||
};
|
||||
}
|
||||
|
||||
function deniedAccess(groups = []) {
|
||||
return {
|
||||
allowed: false,
|
||||
blocked: false,
|
||||
hubRole: "viewer",
|
||||
groups,
|
||||
ownerScopes: [],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizedGroups(user) {
|
||||
|
||||
Reference in New Issue
Block a user