Ctrl K
ComunidadGuías…

server/http/middleware/authz.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import {
  type AuthorizationContext,
  type AuthorizationTenant,
  type AuthorizedOwner,
  authorize,
  authorizeOwner,
  canAuthorize,
} from '@server/domain/authorization-context'
import { forbidden, unauthorized } from '@server/domain/errors'
import { resolveOrganizationUserAuthorizationContext } from '@server/usecases/organization-membership-scopes'
import { requirePlatformOrganization } from '@server/usecases/system-resources'
import { type ProtectedResource, requiredResourceScope } from '@shared/authz'
import type { RealmrootOrganizationScope } from '@shared/scope-registry'
import type { Context, MiddlewareHandler } from 'hono'
import { getPrincipal } from './authn'
import { getDeps } from './deps'

export function authz(resource: ProtectedResource): MiddlewareHandler {
  return async (c, next) => {
    const { user, agent, application } = getPrincipal(c)
    if (!user && !agent && !application) throw unauthorized()
    if (agent || application) {
      const required = requiredResourceScope(c.req.method, resource)
      if (!required || !(agent ?? application)!.scopes.includes(required)) {
        throw forbidden(required ? `OAuth scope "${required}" is required.` : 'This resource is read-only.')
      }
    }
    await next()
  }
}

export function requireAgentScope(c: Context, requiredScope: string) {
  const agent = getPrincipal(c).agent
  if (!agent) throw unauthorized('An OAuth-authenticated Agent is required.')
  if (!agent.scopes.includes(requiredScope)) {
    throw forbidden(`OAuth scope "${requiredScope}" is required.`)
  }
}

export async function authorizeOrganization(
  c: Context,
  organizationId: string,
  requiredScope: RealmrootOrganizationScope,
) {
  const target = organizationBoundary(organizationId)
  authorize(await resolveAuthorizationContext(c, target), target, requiredScope)
}

export async function authorizeOrganizationOwner(
  c: Context,
  organizationId: string,
  requiredScope: RealmrootOrganizationScope,
): Promise<AuthorizedOwner> {
  const target = organizationBoundary(organizationId)
  return authorizeOwner(await resolveAuthorizationContext(c, target), target, requiredScope)
}

export function authorizedOrganizationOwnerId(owner: AuthorizedOwner) {
  if (owner.type === 'organization') return owner.id
  throw new Error('Only an Organization tenant can own an Organization-owned resource.')
}

export async function authorizedOrganizationIds(
  c: Context,
  requiredScope: RealmrootOrganizationScope,
): Promise<string[] | undefined> {
  const tenants = await authorizedTenantInventory(c, requiredScope)
  if (!tenants) return undefined
  return [...new Set(tenants.filter((tenant) => tenant.type === 'organization').map((tenant) => tenant.id))].sort()
}

export async function authorizedTenantInventory(
  c: Context,
  requiredScope: RealmrootOrganizationScope,
): Promise<AuthorizationTenant[] | undefined> {
  const principal = getPrincipal(c)
  const platform = await requirePlatformOrganization(getDeps(c))
  if (
    canAuthorize(await resolvePlatformOrganizationContext(c, platform.id), platformBoundary(platform.id), requiredScope)
  ) {
    return undefined
  }
  if (principal.user) {
    const memberships = await getDeps(c).authorization.listUserMemberships(principal.user.id)
    const userTenant = { type: 'user' as const, id: principal.user.id }
    const userContext = await resolveAuthorizationContext(c, userTenant)
    const tenants: AuthorizationTenant[] = userContext.scopes.has(requiredScope) ? [userTenant] : []
    for (const membership of memberships) {
      const target = { type: 'organization' as const, id: membership.organizationId }
      const context = await resolveAuthorizationContext(c, target)
      if (context.scopes.has(requiredScope)) tenants.push(target)
    }
    return tenants
  }
  if (principal.application) {
    if (!principal.application.scopes.includes(requiredScope)) {
      throw forbidden(`OAuth scope "${requiredScope}" is required.`)
    }
    return [{ type: 'organization', id: principal.application.ownerOrganizationId }]
  }
  const agent = principal.agent
  if (!agent?.scopes.includes(requiredScope)) {
    throw forbidden(`OAuth scope "${requiredScope}" is required.`)
  }
  if (agent.authority?.kind === 'organization') {
    return [{ type: 'organization', id: agent.authority.organizationId }]
  }
  if (agent.authority?.kind === 'user') return [{ type: 'user', id: agent.authority.userId }]
  return []
}

export async function authorizePlatformOrganization(c: Context, requiredScope: RealmrootOrganizationScope) {
  await authorizeOrganization(c, (await requirePlatformOrganization(getDeps(c))).id, requiredScope)
}

export async function hasPlatformOrganizationAccess(c: Context, requiredScope: RealmrootOrganizationScope) {
  const platform = await requirePlatformOrganization(getDeps(c))
  const target = platformBoundary(platform.id)
  return canAuthorize(await resolvePlatformOrganizationContext(c, platform.id), target, requiredScope)
}

export async function authorizeUser(c: Context, userId: string, requiredScope: string) {
  const target = { type: 'user' as const, id: userId }
  authorize(await resolveAuthorizationContext(c, target), target, requiredScope)
}

export async function resolveAuthorizationContext(
  c: Context,
  targetTenant: AuthorizationTenant,
): Promise<AuthorizationContext> {
  const principal = getPrincipal(c)
  if (principal.user) {
    const platformContext = await resolvePlatformOrganizationContext(c)
    if (targetTenant.type === 'user') {
      const scopes = new Set(platformContext.scopes)
      if (principal.user.id === targetTenant.id) {
        for (const scope of ['self:read', 'self:write', 'agents:read', 'agents:write', 'audit-events:read']) {
          scopes.add(scope)
        }
      }
      return restrictUserContextToApplicationScopes(principal, {
        subject: { type: 'user', id: principal.user.id },
        tenant: platformContext.scopes.size > 0 ? targetTenant : { type: 'user', id: principal.user.id },
        scopes,
      })
    }
    if (platformContext.scopes.size > 0) return { ...platformContext, tenant: targetTenant }
    return restrictUserContextToApplicationScopes(
      principal,
      await resolveOrganizationUserAuthorizationContext(getDeps(c), targetTenant.id, principal.user.id),
    )
  }
  if (principal.application) {
    return {
      subject: { type: 'application', id: principal.application.id },
      tenant: {
        type: 'organization',
        id: principal.application.ownerOrganizationId,
      },
      scopes: new Set(principal.application.scopes),
    }
  }
  const agent = principal.agent
  if (!agent) throw unauthorized()
  const authority = agent.authority
  const platform = await requirePlatformOrganization(getDeps(c))
  if (authority?.kind === 'organization' && authority.organizationId === platform.id) {
    return {
      subject: { type: 'agent', id: agent.identityId },
      tenant: targetTenant,
      scopes: new Set(agent.scopes),
    }
  }
  const tenant: AuthorizationTenant =
    authority?.kind === 'organization'
      ? { type: 'organization', id: authority.organizationId }
      : authority?.kind === 'user'
        ? { type: 'user', id: authority.userId }
        : { type: 'user', id: '' }
  return {
    subject: { type: 'agent', id: agent.identityId },
    tenant,
    scopes: new Set(agent.scopes),
  }
}

function organizationBoundary(organizationId: string): AuthorizationTenant {
  return { type: 'organization', id: organizationId }
}

function platformBoundary(platformOrganizationId: string): AuthorizationTenant {
  return organizationBoundary(platformOrganizationId)
}

async function resolvePlatformOrganizationContext(
  c: Context,
  platformOrganizationId?: string,
): Promise<AuthorizationContext> {
  const platformId = platformOrganizationId ?? (await requirePlatformOrganization(getDeps(c))).id
  const principal = getPrincipal(c)
  if (principal.user) {
    return restrictUserContextToApplicationScopes(
      principal,
      await resolveOrganizationUserAuthorizationContext(getDeps(c), platformId, principal.user.id),
    )
  }
  if (principal.application) {
    return {
      subject: { type: 'application', id: principal.application.id },
      tenant: {
        type: 'organization',
        id: principal.application.ownerOrganizationId,
      },
      scopes:
        principal.application.ownerOrganizationId === platformId ? new Set(principal.application.scopes) : new Set(),
    }
  }
  const agent = principal.agent
  if (agent?.authority?.kind === 'organization' && agent.authority.organizationId === platformId) {
    return {
      subject: { type: 'agent', id: agent.identityId },
      tenant: platformBoundary(platformId),
      scopes: new Set(agent.scopes),
    }
  }
  if (!agent) throw unauthorized()
  return {
    subject: { type: 'agent', id: agent.identityId },
    tenant: platformBoundary(platformId),
    scopes: new Set(),
  }
}

function restrictUserContextToApplicationScopes(
  principal: ReturnType<typeof getPrincipal>,
  context: AuthorizationContext,
): AuthorizationContext {
  if (!principal.application) return context
  const applicationScopes = new Set(principal.application.scopes)
  return {
    ...context,
    scopes: new Set([...context.scopes].filter((scope) => applicationScopes.has(scope))),
  }
}

export function authenticatedUser(): MiddlewareHandler {
  return async (c, next) => {
    if (!getPrincipal(c).user) throw unauthorized()
    await next()
  }
}