Ctrl K
ComunidadGuías…

coderd/aibridgedserver/orgresolver.go

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

Archivo original ↗Preguntar a KiBot
package aibridgedserver

import (
	"context"
	"database/sql"
	"errors"
	"slices"

	"github.com/google/uuid"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/coderd/aibridge/budget"
	"github.com/coder/coder/v2/coderd/database"
)

// ErrAmbiguousOrganizationInput is returned when a request carries more than
// one organization input. The login paths are mutually exclusive.
var ErrAmbiguousOrganizationInput = xerrors.New("workspace and delegated organization inputs are mutually exclusive")

// orgResolverInput holds one input per login path. At most one is set. Every
// input is validated: resolveOrganization only returns an organization the
// user is a live member of.
type orgResolverInput struct {
	// WorkspaceID is set on the direct path, for a workspace session token.
	WorkspaceID uuid.NullUUID
	// DelegatedOrganizationID is set on the delegated path, by an in-process
	// caller such as chatd, which sets it from the chat. Clients cannot supply
	// it.
	DelegatedOrganizationID uuid.NullUUID
}

// resolvedOrganization is the organization a request acts in.
type resolvedOrganization struct {
	ID     uuid.UUID
	Source database.AIBridgeInterceptionOrganizationSource
}

// resolveOrganization determines the organization an AI Gateway request acts
// in. All org resolution for AI Gateway requests lives here. It runs once per
// request, in IsAuthorized. The budget check, the interception record and
// cost reuse the result.
//
// The inputs are tried in order:
//  1. A workspace session token resolves to the workspace's organization. The
//     workspace must still exist and userID must still be a member of its
//     organization.
//  2. A delegated request resolves to the organization its in-process caller
//     acts in, the chat's organization for chatd. userID must still be a
//     member of it. The source is chatd, the only delegated caller.
//  3. Otherwise, the organization of the user's site-wide effective group.
//
// Every AI Gateway request is scoped to an organization, so it returns
// [ErrNoOrganization] when none resolves, for example when the user belongs to
// no live organization.
func (s *Server) resolveOrganization(ctx context.Context, userID uuid.UUID, in orgResolverInput) (resolvedOrganization, error) {
	if in.WorkspaceID.Valid && in.DelegatedOrganizationID.Valid {
		return resolvedOrganization{}, ErrAmbiguousOrganizationInput
	}

	if in.WorkspaceID.Valid {
		orgID, err := s.workspaceOrganization(ctx, userID, in.WorkspaceID.UUID)
		if err != nil {
			return resolvedOrganization{}, err
		}
		return resolvedOrganization{ID: orgID, Source: database.AibridgeInterceptionOrganizationSourceWorkspace}, nil
	}

	if in.DelegatedOrganizationID.Valid {
		orgID := in.DelegatedOrganizationID.UUID
		if err := s.requireOrganizationMember(ctx, userID, orgID); err != nil {
			return resolvedOrganization{}, err
		}
		return resolvedOrganization{ID: orgID, Source: database.AibridgeInterceptionOrganizationSourceChatd}, nil
	}

	group, ok, err := budget.ResolveUserEffectiveGroup(ctx, s.store, userID, uuid.NullUUID{}, s.budgetPolicy)
	if err != nil {
		return resolvedOrganization{}, xerrors.Errorf("resolve effective AI group for user %s: %w", userID, err)
	}
	if !ok {
		return resolvedOrganization{}, ErrNoOrganization
	}
	return resolvedOrganization{ID: group.OrganizationID, Source: database.AibridgeInterceptionOrganizationSourceEffectiveGroup}, nil
}

// workspaceOrganization returns the organization of workspaceID. It rejects the
// request when the workspace is missing or deleted, or when userID is not a
// member of its organization.
func (s *Server) workspaceOrganization(ctx context.Context, userID, workspaceID uuid.UUID) (uuid.UUID, error) {
	workspace, err := s.store.GetWorkspaceByID(ctx, workspaceID)
	if errors.Is(err, sql.ErrNoRows) {
		return uuid.Nil, ErrUnknownWorkspace
	}
	if err != nil {
		return uuid.Nil, xerrors.Errorf("get workspace %s: %w", workspaceID, err)
	}
	// Workspaces are soft-deleted, so a deleted row still resolves by ID.
	if workspace.Deleted {
		return uuid.Nil, ErrUnknownWorkspace
	}

	if err := s.requireOrganizationMember(ctx, userID, workspace.OrganizationID); err != nil {
		return uuid.Nil, err
	}
	return workspace.OrganizationID, nil
}

// requireOrganizationMember returns ErrUnknownOrganizationMember unless userID
// is a member of orgID and orgID is not deleted.
func (s *Server) requireOrganizationMember(ctx context.Context, userID, orgID uuid.UUID) error {
	orgs, err := s.store.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{
		UserID:  userID,
		Deleted: sql.NullBool{Bool: false, Valid: true},
	})
	if err != nil {
		return xerrors.Errorf("get organizations for user %s: %w", userID, err)
	}
	if !slices.ContainsFunc(orgs, func(org database.Organization) bool {
		return org.ID == orgID
	}) {
		return ErrUnknownOrganizationMember
	}
	return nil
}