Files
sso-manager-node/nodejs/models/access_request.js
T
wmantly 4a592f9795 Release 1.11.0: end-user catalog, access requests, nested groups
Closes the end-user half of the directory and adds nested LDAP groups.

The directory could describe the lab but could not tell anyone what they had
or how to reach it, and several of the paths meant to do so were silently
returning nothing:

  - GET /api/discovery/me resolved groups from req.user.groups, which does not
    exist (req.user carries memberOf), so it returned only isPublic resources
    for every human caller -- "My Services" was blank for everyone. The same
    read made isDirectoryAdmin() false for real admins.
  - The portal's "Discover More Services" called the admin-gated endpoint and
    swallowed the 403, so it never rendered for non-admins at all.
  - Services reported no address, because /me had reimplemented getMyAccess
    without its parent-walking resolution.

Adds the catalog at /, self-service access requests, and admin access
visibility (per-resource counts, and the reverse "what can this user reach").

Nested groups come in two halves. groupOfNames.member already accepts a group
DN, so nesting needs no schema -- what it needs is resolution, which no
released OpenLDAP performs. The all-in-one image therefore builds slapd from a
pinned master commit for the nestgroup overlay, and the app computes the
closure itself when pointed at a server without it. Both paths are covered.

member-values is deliberately left out of nestgroup-flags: it expands `member`
when reading a group, which destroys the distinction between "listed here" and
"reachable through a nested group" and is not recoverable afterwards.

Full suite green in both resolution modes: 215 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 01:22:08 -04:00

58 lines
2.1 KiB
JavaScript

'use strict';
// Self-service access requests: the "request" half of the directory catalog.
//
// A request is a *proposal to join an LDAP group*. Approving one does exactly
// what an admin would have done by hand -- add the user to `groupCn` -- so LDAP
// remains the single access-control truth and this table is only the paper
// trail of who asked, who decided, and when. Nothing here grants anything on
// its own; a row with status 'approved' whose LDAP write failed is a row that
// grants no access, which is the safe direction.
const { Model } = require('@simpleworkjs/orm');
const STATUS = {
PENDING: 'pending',
APPROVED: 'approved',
DENIED: 'denied',
CANCELLED: 'cancelled',
};
class AccessRequest extends Model {
static fields = {
id: { type: 'uuid', primaryKey: true },
// The requesting user's uid (not dn): dn changes if the directory is
// restructured, uid is the stable handle used everywhere else in the app.
uid: { type: 'string', isRequired: true },
resource: { type: 'hasOne', model: 'Resource' }, // creates resourceId
// The group joining which satisfies this request. Captured at request time
// so a later re-link of the resource's groups can't silently redirect a
// pending approval at a different group than the one that was reviewed.
groupCn: { type: 'string', isRequired: true },
status: { type: 'string', isRequired: true, default: STATUS.PENDING },
note: { type: 'text' },
requestedOn: { type: 'integer' },
decidedBy: { type: 'string' },
decidedOn: { type: 'integer' },
decisionNote: { type: 'text' },
};
// The one request that blocks a new one: same user, same group, still open.
// Denied/cancelled requests deliberately do not block -- circumstances change
// and a user may ask again.
static async findOpen(uid, groupCn) {
const rows = await this.list({ where: { uid, groupCn, status: STATUS.PENDING } });
return rows[0] || null;
}
static async listForUser(uid) {
return this.list({ where: { uid } });
}
static async listPending() {
return this.list({ where: { status: STATUS.PENDING } });
}
}
module.exports = { AccessRequest, STATUS };