danny-avila/LibreChat · error · Error

Invalid role ID: ${principalId}

Error message

Invalid role ID: ${principalId}

What it means

grantPermission() in PermissionService.js:88 throws this when principalType is ROLE and principalId is present but is not a non-empty string. Role principals use the role name as their identifier (not an ObjectId), so the service validates that it is a non-empty trimmed string.

Source

Thrown at api/server/services/PermissionService.js:88

  resourceId,
  accessRoleId,
  grantedBy,
  session,
}) => {
  try {
    if (!Object.values(PrincipalType).includes(principalType)) {
      throw new Error(`Invalid principal type: ${principalType}`);
    }

    if (principalType !== PrincipalType.PUBLIC && !principalId) {
      throw new Error('Principal ID is required for user, group, and role principals');
    }

    // Validate principalId based on type
    if (principalId && principalType === PrincipalType.ROLE) {
      // Role IDs are strings (role names)
      if (typeof principalId !== 'string' || principalId.trim().length === 0) {
        throw new Error(`Invalid role ID: ${principalId}`);
      }
    } else if (
      principalType &&
      principalType !== PrincipalType.PUBLIC &&
      !mongoose.Types.ObjectId.isValid(principalId)
    ) {
      // User and Group IDs must be valid ObjectIds
      throw new Error(`Invalid principal ID: ${principalId}`);
    }

    if (!resourceId || !mongoose.Types.ObjectId.isValid(resourceId)) {
      throw new Error(`Invalid resource ID: ${resourceId}`);
    }

    validateResourceType(resourceType);

    // Get the role to determine permission bits
    const role = await db.findRoleByIdentifier(accessRoleId);

View on GitHub (pinned to 5ff282f900)

Solutions

  1. For ROLE principals, pass the role name string (e.g. SystemRoles.ADMIN) as principalId.
  2. Separate the role-name field from the accessRoleId field in the calling form.
  3. Trim and validate non-empty on the client before submitting.

Example fix

// before
grantPermission({ principalType: PrincipalType.ROLE, principalId: '  ', ... });
// after
grantPermission({ principalType: PrincipalType.ROLE, principalId: roleName.trim(), ... });
Defensive patterns

Strategy: validation

Validate before calling

function assertRolePrincipalId(principalId) {
  if (typeof principalId !== 'string' || principalId.trim().length === 0) {
    throw new Error('ROLE principalId must be a non-empty role-name string');
  }
}

Type guard

const isRoleName = (id) => typeof id === 'string' && id.trim().length > 0;

Prevention

When it happens

Trigger: A caller passes principalType=ROLE with a principalId that is a number, an empty string, whitespace-only, null coerced to '', or an ObjectId. The role-name contract is violated.

Common situations: Frontend reused the user/group ObjectId field for a role grant. A form defaulted principalId to '' for role selection. Confusion between accessRoleId (the permission level) and the role principalId (the role name).

Related errors


AI-assisted analysis of danny-avila/LibreChat@5ff282f900 (2026-08-12). Data as JSON: /api/errors/f0e3cd2fd5e71b8a. Report an issue: GitHub.