Files
zooqueenandHanzo Dev 44c7574c28 A person seated under a name is re-keyed to their IAM subject
The guard once asserted a name where it now asserts the IAM subject, so a row
seated then is keyed by uuid5 of the name. The subject misses that row, the
create collides on the email, and ensureUser answered the collision with
success, leaving GET /users/me and every row keyed to the person on an id no
session asserts.

On that collision ensureUser now calls user.Setter.RekeyUser: in one
transaction it finds the org's row by email and moves it, its user_role rows and
every other row that references users.id onto the subject. SQLite checks the
foreign keys at COMMIT (defer_foreign_keys), so a reference left behind refuses
the whole move. The role grants follow the rows once they commit.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-24 05:58:43 -07:00

69 lines
3.3 KiB
Go

package user
import (
"context"
"net/http"
"github.com/hanzoai/o11y/pkg/statsreporter"
"github.com/hanzoai/o11y/pkg/types"
"github.com/hanzoai/o11y/pkg/types/authtypes"
"github.com/hanzoai/o11y/pkg/valuer"
)
// The user module is a PROJECTION of a Hanzo IAM identity, not an identity.
//
// Nothing here authenticates. There is no password, no invite, no reset token,
// no session — those were deleted with pkg/authn, pkg/tokenizer and
// pkg/modules/session, because o11y asserting who you are is a second answer to
// a question IAM already answered, and two answers is one too many.
//
// What remains exists for one reason: o11y's OWN rows — a dashboard's author, a
// saved view, a preference, a role grant — are keyed by user id, and a foreign
// key needs something to point at. iamidentn writes that row on first sight of
// an IAM subject, keyed by IAM's own subject id. Everything below is either
// that write, a read of it by o11y's own bookkeeping (stats, the role-deletion
// guard), or the one read the console makes of ITSELF — and that one is served
// from the CLAIMS, not from the row.
type Setter interface {
// CreateUser writes the projection row and grants it the roles named in
// opts. The ONLY caller is identn/iamidentn on first sight of an IAM subject;
// the grant and the durable user_role entries happen in this one call, which
// is what localauthz rehydrates from after a restart.
CreateUser(ctx context.Context, user *types.User, opts ...CreateUserOption) error
// RekeyUser moves the org's row for an email — with its user_role rows and
// every other row keyed to it — onto userID, in one transaction, and moves
// its role grants with it. A row already keyed userID is left as it is.
RekeyUser(ctx context.Context, orgID valuer.UUID, email valuer.Email, userID valuer.UUID) error
statsreporter.StatsCollector
}
type Getter interface {
// GetRootUserByOrgID resolves the org's root row — the single principal the
// impersonation identN resolver stands in for in local single-user mode.
GetRootUserByOrgID(context.Context, valuer.UUID) (*types.User, []*authtypes.UserRole, error)
// GetUserByOrgIDAndID answers whether iamidentn has already seated this
// subject, so the founding write happens once.
GetUserByOrgIDAndID(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) (*types.User, error)
// ListUsersByOrgID, CountByOrgID and CountByOrgIDAndStatuses feed the stats
// reporter — how many members a tenant has, not who they are.
ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error)
CountByOrgID(context.Context, valuer.UUID) (int64, error)
CountByOrgIDAndStatuses(context.Context, valuer.UUID, []string) (map[valuer.String]int64, error)
// GetUsersByOrgIDAndRoleID backs OnBeforeRoleDelete.
GetUsersByOrgIDAndRoleID(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) ([]*types.User, error)
// OnBeforeRoleDelete refuses to delete a role that principals still hold.
OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, roleName string) error
}
type Handler interface {
// GetMyUser answers GET /v1/o11y/users/me from the request's CLAIMS. It is
// the console's identity provider and the only user route o11y still serves.
GetMyUser(http.ResponseWriter, *http.Request)
}