All files / services/shared/validation/src org-auth.js

17.58% Statements 16/91
100% Branches 2/2
0% Functions 0/5
17.58% Lines 16/91

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 924x 4x 4x 4x                             4x                           4x       4x                                           4x                                                 4x 4x 4x 4x 4x 4x 4x 4x  
const axios = require('axios');
 
const CONFIG_SERVICE_URL = process.env.CONFIG_SERVICE_URL || 'http://config-service:8000';
 
async function getUserOrganizations(userId, token) {
  try {
    const response = await axios.get(
      `${CONFIG_SERVICE_URL}/users/${userId}/organizations`,
      { headers: { 'Authorization': `Bearer ${token}` }, timeout: 5000 }
    );
    if (response.data?.success) {
      return response.data.data.organizations.map(o => o.id);
    }
  } catch (error) {
    console.error(`[org-auth] Error fetching orgs for user ${userId}:`, error.message);
  }
  return [];
}
 
function extractUserId(req) {
  const authHeader = req.headers['authorization'];
  if (!authHeader) return null;
  const token = authHeader.split(' ')[1];
  if (!token) return null;
  try {
    const base64Payload = token.split('.')[1];
    const padding = 4 - base64Payload.length % 4;
    const base64 = padding < 4 ? base64Payload + '='.repeat(padding) : base64Payload;
    const payload = JSON.parse(Buffer.from(base64, 'base64').toString('utf-8'));
    return payload.userId || null;
  } catch { return null; }
}
 
function extractOrganizationId(req) {
  return req.params.organizationId || req.body?.organizationId || null;
}
 
async function resolveOrgFromRequest(req) {
  const authHeader = req.headers['authorization'];

  // Cas 1 (JWT) prioritaire : l'identité provient du token utilisateur vérifié.
  if (authHeader) {
    const userId = extractUserId(req);
    if (!userId) return { method: null, userId: null, organizationId: null };
    const token = authHeader.split(' ')[1];
    const orgs = await getUserOrganizations(userId, token);
    return { method: 'jwt', userId, organizationId: null, organizations: orgs };
  }

  // Cas 2 (API-key via gateway) : contexte d'organisation posé par la gateway.
  // Ignoré si un JWT est présent (cf. spec/prd-remove-xconfig-id.md, D8).
  const orgIdHeader = req.headers['x-organization-id'];
  if (orgIdHeader) {
    return { method: 'api-key', userId: null, organizationId: String(orgIdHeader) };
  }

  return { method: null, userId: null, organizationId: null };
}
 
async function checkOrgAccess(resourceOrgId, req) {
  const authHeader = req.headers['authorization'];

  if (!resourceOrgId) return false;

  // Cas 1 (JWT) prioritaire : appartenance de l'utilisateur à l'organisation.
  if (authHeader) {
    const userId = extractUserId(req);
    if (!userId) return false;
    const token = authHeader.split(' ')[1];
    const orgs = await getUserOrganizations(userId, token);
    // Normalisation en chaînes : les ObjectId Mongoose des modèles et les ids
    // sérialisés renvoyés par l'API ne sont jamais égaux via includes/=== .
    return orgs.map(String).includes(String(resourceOrgId));
  }

  // Cas 2 (API-key via gateway) : l'organisation est celle du contexte gateway.
  const orgIdHeader = req.headers['x-organization-id'];
  if (orgIdHeader) {
    return String(orgIdHeader) === String(resourceOrgId);
  }

  return false;
}
 
module.exports = {
  getUserOrganizations,
  extractUserId,
  extractOrganizationId,
  resolveOrgFromRequest,
  checkOrgAccess
};