[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/bourgeoa/JavaScriptSolidServer/patchAppend/src/wac/parser.js [Back]  [Original]

/**
 * WAC (Web Access Control) Parser
 * Parses ACL files (JSON-LD or Turtle) into authorization rules
 */

import { turtleToJsonLd } from '../rdf/turtle.js';
import { safeJsonParse } from '../utils/url.js';

const ACL = 'http://www.w3.org/ns/auth/acl#';
const FOAF = 'http://xmlns.com/foaf/0.1/';

// Access modes
export const AccessMode = {
  READ: `${ACL}Read`,
  WRITE: `${ACL}Write`,
  APPEND: `${ACL}Append`,
  CONTROL: `${ACL}Control`
};

// Agent classes
export const AgentClass = {
  AGENT: `${FOAF}Agent`,           // Everyone (public)
  AUTHENTICATED: `${ACL}AuthenticatedAgent`  // Any authenticated user
};

/**
 * Parse an ACL document (JSON-LD or Turtle)
 * @param {string|object} content - ACL content (JSON-LD string/object or Turtle string)
 * @param {string} aclUrl - URL of the ACL document
 * @returns {Promise} List of authorization rules
 */
export async function parseAcl(content, aclUrl) {
  let doc;

  // If already an object, use it directly
  if (typeof content === 'object' && content !== null) {
    doc = content;
  } else if (typeof content === 'string') {
    // Try JSON-LD first (with size limit for DoS protection)
    try {
      doc = safeJsonParse(content);
    } catch {
      // Not JSON, try Turtle
      try {
        doc = await turtleToJsonLd(content, aclUrl);
      } catch (turtleError) {
        // Neither JSON-LD nor valid Turtle
        return [];
      }
    }
  } else {
    return [];
  }

  const authorizations = [];

  // Handle @graph array or single object
  const nodes = Array.isArray(doc) ? doc : (doc['@graph'] || [doc]);

  for (const node of nodes) {
    if (isAuthorization(node)) {
      const auth = parseAuthorization(node, aclUrl);
      if (auth) {
        authorizations.push(auth);
      }
    }
  }

  return authorizations;
}

/**
 * Check if node is an Authorization
 */
function isAuthorization(node) {
  const type = node['@type'];
  if (!type) return false;

  const types = Array.isArray(type) ? type : [type];
  return types.some(t =>
    t === 'acl:Authorization' ||
    t === `${ACL}Authorization` ||
    t === 'Authorization'
  );
}

/**
 * Get base URL from ACL URL (the container the ACL applies to)
 * e.g., https://example.com/foo/.acl -> https://example.com/foo/
 *       https://example.com/foo/bar.acl -> https://example.com/foo/
 */
function getBaseUrl(aclUrl) {
  if (!aclUrl) return null;
  // Remove .acl suffix and get the directory
  const withoutAcl = aclUrl.replace(/\.acl$/, '');
  // If it was a container ACL (ended with /.acl), withoutAcl ends with /
  // If it was a resource ACL (foo.acl), we need the parent directory
  if (withoutAcl.endsWith('/')) {
    return withoutAcl;
  }
  // Get parent directory
  const lastSlash = withoutAcl.lastIndexOf('/');
  return lastSlash > 0 ? withoutAcl.substring(0, lastSlash + 1) : withoutAcl;
}

/**
 * Resolve a URI against a base URL
 */
function resolveUri(uri, baseUrl) {
  if (!uri || !baseUrl) return uri;
  // Already absolute
  if (uri.startsWith('http://') || uri.startsWith('https://')) return uri;
  // Fragment-only (like #owner) - not a resource URL
  if (uri.startsWith('#')) return uri;
  // Resolve relative URL
  try {
    return new URL(uri, baseUrl).href;
  } catch {
    return uri;
  }
}

/**
 * Parse a single Authorization node
 */
function parseAuthorization(node, aclUrl) {
  const baseUrl = getBaseUrl(aclUrl);

  const auth = {
    id: node['@id'],
    accessTo: [],      // Resources this applies to
    default: [],       // Default for contained resources
    agents: [],        // Specific WebIDs
    agentClasses: [],  // Agent classes (public, authenticated)
    agentGroups: [],   // Groups
    modes: [],         // Access modes
    conditions: []     // Access conditions (e.g. PaymentCondition)
  };

  // Parse accessTo - resolve relative URLs
  auth.accessTo = parseUriArray(node['acl:accessTo'] || node['accessTo'])
    .map(uri => resolveUri(uri, baseUrl));

  // Parse default (for containers) - resolve relative URLs
  auth.default = parseUriArray(node['acl:default'] || node['default'])
    .map(uri => resolveUri(uri, baseUrl));

  // Parse agents (WebIDs can be relative too) - resolve against ACL URL
  auth.agents = parseUriArray(node['acl:agent'] || node['agent'])
    .map(uri => resolveUri(uri, aclUrl));

  // Parse agentClass
  auth.agentClasses = parseUriArray(node['acl:agentClass'] || node['agentClass']);

  // Parse agentGroup
  auth.agentGroups = parseUriArray(node['acl:agentGroup'] || node['agentGroup']);

  // Parse modes
  auth.modes = parseUriArray(node['acl:mode'] || node['mode']).map(normalizeMode);

  // Parse conditions
  auth.conditions = parseConditions(node['acl:condition'] || node['condition']);

  return auth;
}

/**
 * Parse conditions from an authorization node
 */
function parseConditions(value) {
  if (!value) return [];
  const values = Array.isArray(value) ? value : [value];
  return values.map(v => {
    if (typeof v !== 'object' || v === null) return null;
    const type = v['@type'] || v.type;
    if (!type) return null;
    return { ...v, type };
  }).filter(Boolean);
}

/**
 * Parse a value that could be a URI, @id object, or array of either
 */
function parseUriArray(value) {
  if (!value) return [];

  const values = Array.isArray(value) ? value : [value];

  return values.map(v => {
    if (typeof v === 'string') return v;
    if (v && typeof v === 'object' && v['@id']) return v['@id'];
    return null;
  }).filter(Boolean);
}

/**
 * Normalize mode URIs to full form
 */
function normalizeMode(mode) {
  const modeMap = {
    'Read': AccessMode.READ,
    'Write': AccessMode.WRITE,
    'Append': AccessMode.APPEND,
    'Control': AccessMode.CONTROL,
    'acl:Read': AccessMode.READ,
    'acl:Write': AccessMode.WRITE,
    'acl:Append': AccessMode.APPEND,
    'acl:Control': AccessMode.CONTROL
  };
  return modeMap[mode] || mode;
}

/**
 * Generate a default public read ACL
 * @param {string} resourceUrl - URL of the resource
 * @returns {object} JSON-LD ACL document
 */
export function generatePublicReadAcl(resourceUrl) {
  return {
    '@context': {
      'acl': ACL,
      'foaf': FOAF
    },
    '@graph': [
      {
        '@id': '#public',
        '@type': 'acl:Authorization',
        'acl:agentClass': { '@id': 'foaf:Agent' },
        'acl:accessTo': { '@id': resourceUrl },
        'acl:mode': [
          { '@id': 'acl:Read' }
        ]
      }
    ]
  };
}

/**
 * Generate a full owner ACL (owner has full control, public read)
 * @param {string} resourceUrl - URL of the resource
 * @param {string} ownerWebId - WebID of the owner
 * @param {boolean} isContainer - Whether this is a container
 * @returns {object} JSON-LD ACL document
 */
export function generateOwnerAcl(resourceUrl, ownerWebId, isContainer = false) {
  const graph = [
    {
      '@id': '#owner',
      '@type': 'acl:Authorization',
      'acl:agent': { '@id': ownerWebId },
      'acl:accessTo': { '@id': resourceUrl },
      'acl:mode': [
        { '@id': 'acl:Read' },
        { '@id': 'acl:Write' },
        { '@id': 'acl:Control' }
      ]
    },
    {
      '@id': '#public',
      '@type': 'acl:Authorization',
      'acl:agentClass': { '@id': 'foaf:Agent' },
      'acl:accessTo': { '@id': resourceUrl },
      'acl:mode': [
        { '@id': 'acl:Read' }
      ]
    }
  ];

  // Add default rules for containers
  // Only owner gets default - children don't inherit public read
  if (isContainer) {
    graph[0]['acl:default'] = { '@id': resourceUrl };
    // Note: intentionally not adding default to #public
    // so child resources require authentication by default
  }

  return {
    '@context': {
      'acl': ACL,
      'foaf': FOAF
    },
    '@graph': graph
  };
}

/**
 * Generate a private ACL (owner only, no public access)
 * @param {string} resourceUrl - URL of the resource
 * @param {string} ownerWebId - WebID of the owner
 * @param {boolean} isContainer - Whether this is a container
 * @returns {object} JSON-LD ACL document
 */
export function generatePrivateAcl(resourceUrl, ownerWebId, isContainer = true) {
  const auth = {
    '@id': '#owner',
    '@type': 'acl:Authorization',
    'acl:agent': { '@id': ownerWebId },
    'acl:accessTo': { '@id': resourceUrl },
    'acl:mode': [
      { '@id': 'acl:Read' },
      { '@id': 'acl:Write' },
      { '@id': 'acl:Control' }
    ]
  };

  if (isContainer) {
    auth['acl:default'] = { '@id': resourceUrl };
  }

  return {
    '@context': {
      'acl': ACL,
      'foaf': FOAF
    },
    '@graph': [auth]
  };
}

/**
 * Generate an inbox ACL (owner full control, public append)
 * @param {string} resourceUrl - URL of the inbox
 * @param {string} ownerWebId - WebID of the owner
 * @returns {object} JSON-LD ACL document
 */
export function generateInboxAcl(resourceUrl, ownerWebId) {
  return {
    '@context': {
      'acl': ACL,
      'foaf': FOAF
    },
    '@graph': [
      {
        '@id': '#owner',
        '@type': 'acl:Authorization',
        'acl:agent': { '@id': ownerWebId },
        'acl:accessTo': { '@id': resourceUrl },
        'acl:default': { '@id': resourceUrl },
        'acl:mode': [
          { '@id': 'acl:Read' },
          { '@id': 'acl:Write' },
          { '@id': 'acl:Control' }
        ]
      },
      {
        '@id': '#public',
        '@type': 'acl:Authorization',
        'acl:agentClass': { '@id': 'foaf:Agent' },
        'acl:accessTo': { '@id': resourceUrl },
        'acl:default': { '@id': resourceUrl },
        'acl:mode': [
          { '@id': 'acl:Append' }
        ]
      }
    ]
  };
}

/**
 * Generate a public folder ACL (owner full control, public read with inheritance)
 * Used for /public/ folders where content should be publicly readable
 * @param {string} resourceUrl - URL of the folder
 * @param {string} ownerWebId - WebID of the owner
 * @returns {object} JSON-LD ACL document
 */
export function generatePublicFolderAcl(resourceUrl, ownerWebId) {
  return {
    '@context': {
      'acl': ACL,
      'foaf': FOAF
    },
    '@graph': [
      {
        '@id': '#owner',
        '@type': 'acl:Authorization',
        'acl:agent': { '@id': ownerWebId },
        'acl:accessTo': { '@id': resourceUrl },
        'acl:default': { '@id': resourceUrl },
        'acl:mode': [
          { '@id': 'acl:Read' },
          { '@id': 'acl:Write' },
          { '@id': 'acl:Control' }
        ]
      },
      {
        '@id': '#public',
        '@type': 'acl:Authorization',
        'acl:agentClass': { '@id': 'foaf:Agent' },
        'acl:accessTo': { '@id': resourceUrl },
        'acl:default': { '@id': resourceUrl },
        'acl:mode': [
          { '@id': 'acl:Read' }
        ]
      }
    ]
  };
}

/**
 * Serialize ACL to JSON string
 */
export function serializeAcl(acl) {
  return JSON.stringify(acl, null, 2);
}

Web Proxy Viewer  |  New URL  |  Original Page