OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / middleware / require-auth.js

189 lines · javascript ref:
/**
 * Authentication Middleware
 *
 * Provides middleware functions for route authentication
 */

import { unauthorized, forbidden, json } from '../response.js'

/**
 * Require authentication (any valid identity)
 * Attaches auth result to req.auth
 * Returns 401 if not authenticated
 */
export async function requireAuth(req, res, server) {
  if (!server.authEnabled) {
    // Auth disabled - allow all requests
    req.auth = null
    return true
  }

  const auth = await server.authManager.authenticate(req)
  if (!auth) {
    unauthorized(res, 'Authentication required')
    return false
  }

  req.auth = auth
  return true
}

/**
 * Optional authentication - attaches auth if available, continues either way
 * Always returns true (never blocks request)
 */
export async function optionalAuth(req, res, server) {
  if (!server.authEnabled) {
    req.auth = null
    return true
  }

  try {
    req.auth = await server.authManager.authenticate(req)
  } catch (err) {
    // Silently fail - optional auth
    req.auth = null
  }
  return true
}

/**
 * Require repo access for a specific action
 * Must be used after requireAuth or optionalAuth
 *
 * @param {string} action - 'read' or 'write'
 */
export function requireRepoAccess(action) {
  return async function (req, res, server) {
    const { repoName } = req.params

    if (!repoName) {
      // No repo in route - skip check
      return true
    }

    // Get repo info
    const repo = server.repos.find(r => r.name === repoName)
    if (!repo) {
      json(res, { error: 'Repository not found' }, 404)
      return false
    }

    // Read access is always allowed (federation = full read)
    if (action === 'read') {
      req.repo = repo
      return true
    }

    // Write access requires auth
    if (!req.auth) {
      unauthorized(res, 'Authentication required')
      return false
    }

    // Check permissions — pass registry context for grant-based access
    const serverKey = server.serverIdentity
      ? server.serverIdentity.getPublicKeyHex()
      : null
    const hasAccess = await server.authManager.hasRepoAccess(req.auth, repo, action, {
      namespaceRegistry: server.namespaceRegistry,
      serverKey
    })
    if (!hasAccess) {
      forbidden(res, `${action} access denied for repository: ${repoName}`)
      return false
    }

    req.repo = repo
    return true
  }
}

/**
 * Require write permission (any valid write access)
 * For operations that modify data
 */
export async function requireWrite(req, res, server) {
  if (!server.authEnabled) {
    req.auth = null
    return true
  }

  const auth = await server.authManager.authenticate(req)
  if (!auth) {
    unauthorized(res, 'Authentication required')
    return false
  }

  if (!auth.permissions.write && !auth.permissions.admin) {
    forbidden(res, 'Write permission required')
    return false
  }

  req.auth = auth
  return true
}

/**
 * Load user info from auth
 * Must be used after requireAuth
 * Attaches full user info to req.user
 */
export async function loadUser(req, res, server) {
  if (!req.auth) {
    return true
  }

  try {
    const userResult = await server.authManager.getUserWithIdentities(req.auth.identityId)
    if (userResult) {
      req.user = userResult.user
      req.identities = userResult.identities
    }
  } catch (err) {
    console.error('Error loading user:', err)
  }

  return true
}

/**
 * Check if user has specific permission
 * @param {string|string[]} permissions - Permission(s) to check
 */
export function requirePermission(...permissions) {
  return async function (req, res, server) {
    if (!server.authEnabled) {
      return true
    }

    if (!req.auth) {
      unauthorized(res, 'Authentication required')
      return false
    }

    // Load user if not already loaded
    if (!req.user) {
      const userResult = await server.authManager.getUserWithIdentities(req.auth.identityId)
      if (!userResult) {
        forbidden(res, 'User not found')
        return false
      }
      req.user = userResult.user
    }

    // Check global permissions
    const hasPermission = permissions.some(perm =>
      req.user.globalPermissions.includes(perm) ||
      req.user.globalPermissions.includes('admin')
    )

    if (!hasPermission) {
      forbidden(res, `Permission required: ${permissions.join(' or ')}`)
      return false
    }

    return true
  }
}