Documentation
¶
Index ¶
- Constants
- Variables
- func BeeLiveness(b *tenant.Bee) string
- func CeilingFor(base int, trust SourceTrust, coarse bool) int
- func HashToken(raw string) string
- func IsFirewallValidation(err error) bool
- func MapProxmoxResources(rs proxmox.ClusterResources) []tenant.ResourceRow
- func NewEnrolmentToken() (raw, hash string, err error)
- func ObservationFresh(b *tenant.Bee, now time.Time) bool
- type APIKeyService
- type AcceptInvitationInput
- type AcceptInvitationResult
- type AccessRuleResolver
- type AccountDeletionService
- func (s *AccountDeletionService) DeleteAccount(ctx context.Context, userID uuid.UUID, now time.Time) error
- func (s *AccountDeletionService) DeleteOrganization(ctx context.Context, orgID uuid.UUID, confirmName string, now time.Time) error
- func (s *AccountDeletionService) SoleOwnerships(ctx context.Context, userID uuid.UUID) ([]control.Organization, error)
- type AdmissionDecision
- type AdmissionInput
- type AdmissionService
- type AgentKeyMinter
- type AuthClaims
- type Authenticated
- type BeeSession
- type BeginLoginResult
- type BeginRegistrationResult
- type ChangePasswordInput
- type ConfiguredSignInAlternatives
- type ConfirmResult
- type DirectEmailSender
- type EffectiveResult
- type EmailSender
- type EmailService
- type EmailVerificationService
- type EnrolInput
- type EnrolResult
- type EnrolmentRedeemer
- type EstateService
- func (s *EstateService) Delete(ctx context.Context, orgID, id uuid.UUID) error
- func (s *EstateService) Get(ctx context.Context, orgID, id uuid.UUID) (*EstateView, error)
- func (s *EstateService) List(ctx context.Context, orgID uuid.UUID) ([]EstateView, error)
- func (s *EstateService) Register(ctx context.Context, in RegisterEstateInput) (*EstateView, error)
- func (s *EstateService) RotateCredential(ctx context.Context, orgID, estateID uuid.UUID, identity string, ...) (*EstateView, error)
- type EstateView
- type FeatureFlagService
- func (s *FeatureFlagService) ClearOrg(ctx context.Context, orgID uuid.UUID, key string) error
- func (s *FeatureFlagService) ClearPlatform(ctx context.Context, key string) error
- func (s *FeatureFlagService) Effective(ctx context.Context, orgID uuid.UUID, key string) (*control.EffectiveFlag, error)
- func (s *FeatureFlagService) Enabled(ctx context.Context, orgID uuid.UUID, key string) (bool, error)
- func (s *FeatureFlagService) ListEffective(ctx context.Context, orgID uuid.UUID) ([]control.EffectiveFlag, error)
- func (s *FeatureFlagService) ListPublicEffective(ctx context.Context) ([]control.EffectiveFlag, error)
- func (s *FeatureFlagService) OverridesFor(ctx context.Context, key string) ([]repo.FlagOverride, error)
- func (s *FeatureFlagService) PlatformEnabled(ctx context.Context, key string) (bool, error)
- func (s *FeatureFlagService) SetOrg(ctx context.Context, orgID uuid.UUID, key string, value bool, by uuid.UUID) error
- func (s *FeatureFlagService) SetPlatform(ctx context.Context, key string, value bool, by uuid.UUID, ...) error
- func (s *FeatureFlagService) WithSignInAlternatives(a SignInAlternatives) *FeatureFlagService
- type FinishLoginInput
- type FinishRegistrationInput
- type FirewallService
- func (s *FirewallService) AddSetElement(ctx context.Context, orgID, setID uuid.UUID, value, comment, label string) (*tenant.FirewallSetElement, error)
- func (s *FirewallService) Bind(ctx context.Context, orgID uuid.UUID, hiveID, swarmID *uuid.UUID, ...) (*tenant.FirewallBinding, error)
- func (s *FirewallService) CreateRule(ctx context.Context, orgID, rulesetID uuid.UUID, in RuleInput) (*tenant.FirewallRule, error)
- func (s *FirewallService) CreateRuleset(ctx context.Context, orgID uuid.UUID, name, description string) (*tenant.FirewallRuleset, error)
- func (s *FirewallService) CreateSet(ctx context.Context, orgID uuid.UUID, ...) (*tenant.FirewallSet, error)
- func (s *FirewallService) DeleteRule(ctx context.Context, orgID, id uuid.UUID) error
- func (s *FirewallService) DeleteRuleset(ctx context.Context, orgID, id uuid.UUID) error
- func (s *FirewallService) DeleteSet(ctx context.Context, orgID, id uuid.UUID) error
- func (s *FirewallService) GetSet(ctx context.Context, orgID, id uuid.UUID) (*tenant.FirewallSet, error)
- func (s *FirewallService) ListBindings(ctx context.Context, orgID uuid.UUID) ([]tenant.FirewallBinding, error)
- func (s *FirewallService) ListRules(ctx context.Context, orgID, rulesetID uuid.UUID) ([]tenant.FirewallRule, error)
- func (s *FirewallService) ListRulesets(ctx context.Context, orgID uuid.UUID) ([]tenant.FirewallRuleset, error)
- func (s *FirewallService) ListSets(ctx context.Context, orgID uuid.UUID) ([]tenant.FirewallSet, error)
- func (s *FirewallService) ManagementSets(ctx context.Context, orgID uuid.UUID) ([]tenant.FirewallSet, error)
- func (s *FirewallService) Preview(ctx context.Context, orgID uuid.UUID, hiveID, swarmID *uuid.UUID) ([]tenant.FirewallRule, error)
- func (s *FirewallService) RemoveSetElement(ctx context.Context, orgID, setID, elementID uuid.UUID) error
- func (s *FirewallService) Unbind(ctx context.Context, orgID, id uuid.UUID) error
- func (s *FirewallService) UpdateBinding(ctx context.Context, orgID, id uuid.UUID, priority int, enabled bool) error
- func (s *FirewallService) UpdateBindingScope(ctx context.Context, orgID, id uuid.UUID, priority int, enabled bool, ...) error
- func (s *FirewallService) UpdateRule(ctx context.Context, orgID, id uuid.UUID, in RuleInput) (*tenant.FirewallRule, error)
- func (s *FirewallService) UpdateRuleset(ctx context.Context, orgID, id uuid.UUID, name, description string) (*tenant.FirewallRuleset, error)
- func (s *FirewallService) UpdateSet(ctx context.Context, orgID, id uuid.UUID, name, description string) (*tenant.FirewallSet, error)
- type FleetService
- func (s *FleetService) AppendLogs(ctx context.Context, sess *BeeSession, taskID uuid.UUID, lines []LogLine) (int, error)
- func (s *FleetService) Authenticate(ctx context.Context, public, secret string) (*BeeSession, error)
- func (s *FleetService) DesiredFirewallFor(ctx context.Context, sess *BeeSession) (*tenant.FirewallSnapshot, error)
- func (s *FleetService) DesiredMembershipFor(ctx context.Context, sess *BeeSession) (*tenant.MembershipDesired, error)
- func (s *FleetService) Enrol(ctx context.Context, in EnrolInput) (*EnrolResult, error)
- func (s *FleetService) FinaliseDepartures(ctx context.Context, sess *BeeSession) error
- func (s *FleetService) Heartbeat(ctx context.Context, in HeartbeatInput) (*HeartbeatResult, error)
- func (s *FleetService) NewEnrolmentToken() (raw, hash string, err error)
- func (s *FleetService) PublishMembership(ctx context.Context, db *gorm.DB, hiveID uuid.UUID) error
- func (s *FleetService) RecordAppliedFirewall(ctx context.Context, sess *BeeSession, revision int, digest string) error
- func (s *FleetService) RecordAppliedMembership(ctx context.Context, sess *BeeSession, revision int, digest string) error
- func (s *FleetService) RecordObservedGuests(ctx context.Context, sess *BeeSession, guests *int, inACluster bool) error
- func (s *FleetService) RecordObservedMembership(ctx context.Context, sess *BeeSession, obs *tenant.Observed, ...) error
- func (s *FleetService) RecordObservedNodeName(ctx context.Context, sess *BeeSession, node string) error
- func (s *FleetService) Report(ctx context.Context, in ReportInput) (*tenant.FleetTask, error)
- type HeartbeatInput
- type HeartbeatResult
- type ImpersonationService
- func (s *ImpersonationService) End(ctx context.Context, accessJTI string, targetUserID, actorID, orgID uuid.UUID, ...) error
- func (s *ImpersonationService) Start(ctx context.Context, in StartImpersonationInput) (*StartImpersonationResult, error)
- func (s *ImpersonationService) ToggleSupport(ctx context.Context, in ToggleSupportInput) error
- type InvitationPreview
- type InvitationService
- func (s *InvitationService) Accept(ctx context.Context, in AcceptInvitationInput) (*AcceptInvitationResult, error)
- func (s *InvitationService) Invite(ctx context.Context, in InviteInput) (*InvitationView, error)
- func (s *InvitationService) List(ctx context.Context, orgID uuid.UUID) ([]InvitationView, error)
- func (s *InvitationService) Preview(ctx context.Context, rawToken string) (*InvitationPreview, error)
- func (s *InvitationService) Revoke(ctx context.Context, orgID, id, actorID uuid.UUID) error
- type InvitationView
- type InviteInput
- type IssueOpts
- type IssueTrackedInput
- type IssuedTokens
- type LogLine
- type LoginInput
- type LoginResult
- type LoginService
- type MFAService
- func (s *MFAService) DisableTOTP(ctx context.Context, userID uuid.UUID) error
- func (s *MFAService) SetAudit(r repo.AuthAuditRepo)
- func (s *MFAService) SetupTOTP(ctx context.Context, userID uuid.UUID, email string) (*TOTPSetupResult, error)
- func (s *MFAService) UserHasMFA(ctx context.Context, userID uuid.UUID) (bool, error)
- func (s *MFAService) ValidateTOTP(ctx context.Context, userID uuid.UUID, code string) error
- func (s *MFAService) VerifyAndActivateTOTP(ctx context.Context, userID uuid.UUID, code string) error
- type MeResult
- type MemberView
- type MembershipService
- type MintInput
- type MintResult
- type MyOrganizationResult
- type NetworkClassifier
- type OrgBootstrapLimits
- type OrgBootstrapService
- type PasskeyCounter
- type PasskeyService
- func (s *PasskeyService) BeginLogin(ctx context.Context) (*BeginLoginResult, error)
- func (s *PasskeyService) BeginRegistration(ctx context.Context, userID uuid.UUID) (*BeginRegistrationResult, error)
- func (s *PasskeyService) Delete(ctx context.Context, id, userID uuid.UUID) error
- func (s *PasskeyService) FinishLogin(ctx context.Context, in FinishLoginInput) (*IssuedTokens, string, error)
- func (s *PasskeyService) FinishRegistration(ctx context.Context, userID uuid.UUID, in FinishRegistrationInput) (*control.UserPasskey, error)
- func (s *PasskeyService) List(ctx context.Context, userID uuid.UUID) ([]control.UserPasskey, error)
- func (s *PasskeyService) SetAudit(r repo.AuthAuditRepo)
- func (s *PasskeyService) SetChallenges(r repo.AuthChallengeRepo)
- type PasswordResetService
- type PolicyService
- func (s *PolicyService) CheckUserCompliance(ctx context.Context, userID, orgID uuid.UUID) ([]PolicyViolation, error)
- func (s *PolicyService) DeleteOrgPolicy(ctx context.Context, orgID uuid.UUID, kind control.PolicyKind) error
- func (s *PolicyService) DeletePlatformPolicy(ctx context.Context, kind control.PolicyKind) error
- func (s *PolicyService) EffectivePolicy(ctx context.Context, orgID uuid.UUID, kind control.PolicyKind) (*EffectiveResult, error)
- func (s *PolicyService) ListEffectivePolicies(ctx context.Context, orgID uuid.UUID) ([]EffectiveResult, error)
- func (s *PolicyService) ListOrgPolicies(ctx context.Context, orgID uuid.UUID) ([]control.OrgPolicy, error)
- func (s *PolicyService) ListPlatformPolicies(ctx context.Context) ([]control.PlatformPolicy, error)
- func (s *PolicyService) PreEnableCheck(ctx context.Context, orgID *uuid.UUID, kind control.PolicyKind) (int64, error)
- func (s *PolicyService) SetOrgPolicy(ctx context.Context, createdBy uuid.UUID, orgID uuid.UUID, ...) (*control.OrgPolicy, error)
- func (s *PolicyService) SetPlatformPolicy(ctx context.Context, createdBy uuid.UUID, kind control.PolicyKind, ...) (*control.PlatformPolicy, error)
- func (s *PolicyService) ValidatePassword(ctx context.Context, orgID uuid.UUID, password string) error
- type PolicyViolation
- type ProfileResult
- type ProfileService
- func (s *ProfileService) ChangePassword(ctx context.Context, userID uuid.UUID, in ChangePasswordInput) error
- func (s *ProfileService) ConfirmEmailChange(ctx context.Context, rawToken string) error
- func (s *ProfileService) GetProfile(ctx context.Context, userID, orgID uuid.UUID) (*ProfileResult, error)
- func (s *ProfileService) RequestEmailChange(ctx context.Context, userID uuid.UUID, newEmail string) error
- func (s *ProfileService) SetEmailChange(r repo.EmailChangeRepo, email EmailSender, baseURL string)
- func (s *ProfileService) UpdateProfile(ctx context.Context, userID uuid.UUID, in UpdateProfileInput) (*control.User, error)
- type ProxmoxInventoryService
- func (s *ProxmoxInventoryService) AgentProxmoxCredential(ctx context.Context, orgID, hypervisorID uuid.UUID) (tokenID, secret, apiURL string, err error)
- func (s *ProxmoxInventoryService) AgentProxmoxCredentialForHive(ctx context.Context, orgID, hiveID uuid.UUID) (tokenID, secret, apiURL string, err error)
- func (s *ProxmoxInventoryService) ClusterIdentity(ctx context.Context, orgID, hiveID uuid.UUID) (name string, clustered bool, err error)
- func (s *ProxmoxInventoryService) Inventory(ctx context.Context, orgID, hiveID uuid.UUID) ([]tenant.ResourceRow, error)
- func (s *ProxmoxInventoryService) NodeAddresses(ctx context.Context, orgID, hiveID uuid.UUID) (map[string]string, error)
- type QueuedEmailSender
- type RefreshInput
- type RefreshResult
- type RegisterEstateInput
- type RegisterInput
- type RegisterResult
- type RegistrationService
- type RemoveMemberInput
- type ReportInput
- type RuleInput
- type SSHKeyService
- func (s *SSHKeyService) ActiveFleetKey(ctx context.Context, orgID uuid.UUID) (*tenant.SshKey, error)
- func (s *SSHKeyService) List(ctx context.Context, orgID uuid.UUID) ([]tenant.SshKey, error)
- func (s *SSHKeyService) MintFleetKey(ctx context.Context, orgID uuid.UUID, name string) (*tenant.SshKey, error)
- func (s *SSHKeyService) MintJoinKey(ctx context.Context, orgID uuid.UUID, hostID, taskID uuid.UUID, ...) (*tenant.SshKey, error)
- func (s *SSHKeyService) PrivateKey(ctx context.Context, orgID, id uuid.UUID) ([]byte, error)
- func (s *SSHKeyService) Revoke(ctx context.Context, orgID, id uuid.UUID) error
- type SessionService
- func (s *SessionService) ListMyOrganizations(ctx context.Context, userID string, currentOrgID string) ([]MyOrganizationResult, error)
- func (s *SessionService) Refresh(ctx context.Context, in RefreshInput) (*RefreshResult, error)
- func (s *SessionService) SwitchOrganization(ctx context.Context, in SwitchOrganizationInput) (*SwitchOrganizationResult, error)
- type SignInAlternatives
- type SocialAuthInput
- type SocialAuthResult
- type SocialAuthService
- type SocialProviderConfig
- type SourceClassifier
- type SourceInput
- type SourceKey
- type SourceTrust
- type StartImpersonationInput
- type StartImpersonationResult
- type SwitchOrganizationInput
- type SwitchOrganizationResult
- type TOTPSetupResult
- type TenantResolver
- type ToggleSupportInput
- type TokenPair
- type TokenService
- func (s *TokenService) Issue(userID, orgID string, now time.Time, opts ...IssueOpts) (*IssuedTokens, error)
- func (s *TokenService) IssueMFAPending(userID, orgID string, now time.Time) (string, error)
- func (s *TokenService) IssueMFAPendingWithJTI(userID, orgID string, now time.Time) (string, uuid.UUID, error)
- func (s *TokenService) Parse(tokenStr, expectedType string) (*AuthClaims, error)
- type TokenSessionService
- func (s *TokenSessionService) DeleteExpired(ctx context.Context) (int64, error)
- func (s *TokenSessionService) IssueTracked(ctx context.Context, in IssueTrackedInput) (*IssuedTokens, error)
- func (s *TokenSessionService) ListActive(ctx context.Context, subjectID uuid.UUID, subjectKind string) ([]control.TokenSession, error)
- func (s *TokenSessionService) Refresh(ctx context.Context, refreshTokenRaw, ip, ua string, overrideOpts ...IssueOpts) (*IssuedTokens, string, error)
- func (s *TokenSessionService) Revoke(ctx context.Context, accessJTI, subjectID, ip, ua string) error
- func (s *TokenSessionService) RevokeAll(ctx context.Context, subjectID uuid.UUID, subjectKind, ip, ua string) error
- func (s *TokenSessionService) SetAccessRules(r repo.AuthAccessRuleRepo)
- func (s *TokenSessionService) Tokens() *TokenService
- type UpdateProfileInput
- type UpdateRoleInput
Constants ¶
const ( LoginMaxFailures = 10 LoginLockoutFor = 15 * time.Minute LoginEmailAlertThreshold = 50 )
const MFAPendingMaxAttempts = 5
const OAuthStateCookieName = "__Host-oauth_state"
Variables ¶
var ( // ErrSoleOwner refuses to delete an account that is the only owner of an // organization. Not a formality: the organization would be left with nobody // who can add an owner, change a role, or delete it — an unadministrable // tenant that only platform staff can rescue. ErrSoleOwner = errors.New("account: this account is the only owner of an organization") // ErrConfirmationMismatch refuses a destructive action whose confirmation // does not match. ErrConfirmationMismatch = errors.New("account: the confirmation does not match") ErrOrgNotFound = errors.New("account: no such organization") ErrAccountNotFound = errors.New("account: no such account") )
var ( // ErrUnprovenOrgCeiling is a 403, deliberately not a 429: the ceiling has no // time component, so "try again later" would be a lie. Waiting restores // nothing; only an organization becoming PROVEN frees capacity. ErrUnprovenOrgCeiling = errors.New("admission: this email domain already holds too many unproven organizations") // ErrUnresolvableDomain is a 422. The address cannot be reduced to an // eTLD+1, so there is no bucket to count it against and no domain a // registrar could ever act on. That is a refusal, never a fallthrough. ErrUnresolvableDomain = errors.New("admission: cannot determine a registrable domain for this address") // ErrAdmissionNotConfigured is a 500. It exists so that a gate that was // never wired denies instead of panicking or, far worse, waving everything // through: an unwired admission gate must not look like an empty rule set. ErrAdmissionNotConfigured = errors.New("admission: gate is not configured") )
var ( // ErrAPIKeyRejected covers unknown, revoked, expired and wrong-secret. One // error for all four: this is reached by anything that can present a // credential, so distinguishing them would turn the endpoint into an oracle // over keys the caller does not hold. ErrAPIKeyRejected = errors.New("api key: rejected") // ErrAPIKeyWrongSurface is separate because it is NOT an authentication // failure — the credential is genuine, it is simply not admitted here. An // agent key on an org endpoint is a misconfigured deployment, and telling // its operator "rejected" would send them looking for a credential problem // that does not exist. ErrAPIKeyWrongSurface = errors.New("api key: not valid on this surface") )
var ( // ErrVerificationTokenInvalid covers expired, already-used, unknown, and // issued-for-a-different-address. They are one error on purpose: telling a // caller which of those a token is turns the endpoint into an oracle over // tokens they do not hold. ErrVerificationTokenInvalid = errors.New("verification: link is invalid or has expired") // ErrEmailNotVerified is returned by the login path. It is separate from // ErrInvalidCredentials because the caller supplied a CORRECT password — the // UI has to be able to route them to "resend the link" rather than "you // typed it wrong". See the note in LoginService.Login on why that disclosure // is acceptable here and would not be at the identification step. ErrEmailNotVerified = errors.New("verification: this address has not been confirmed yet") )
var ( ErrEstateHasCredential = errors.New("estate: a credential is still bound to this estate") )
var ( ErrEnrolmentRefused = errors.New("fleet: enrolment refused") ErrLogBatchTooLarge = errors.New("fleet: log batch is too large") )
var ( ErrImpersonationNotAllowed = errors.New("impersonation not allowed") ErrTargetUserNotFound = errors.New("target user not found") ErrTargetUserInactive = errors.New("target user is inactive") ErrMembershipNotFound = errors.New("membership not found in organization") )
var ( // ErrInvitationInvalid covers unknown, expired, revoked and already // accepted. One error for all four: the endpoint is unauthenticated, so // distinguishing them would let anyone holding a guess probe the table. ErrInvitationInvalid = errors.New("invitation: this invitation is invalid or has expired") // ErrAlreadyMember is not an error the invitee sees — accepting when you are // already in is reported as success. It is returned to the INVITER, who is // asking for something that would do nothing. ErrAlreadyMember = errors.New("invitation: that address is already a member of this organization") // ErrPasswordRequired is returned when a brand-new invitee accepts without // choosing one. There is no account to fall back on. ErrPasswordRequired = errors.New("invitation: choose a password to finish setting up your account") // ErrPasswordNotAccepted is its inverse: an invitee who ALREADY has an // account may not set a password through this path. Accepting an invitation // proves control of a mailbox, not knowledge of an existing account's // password, and letting it rewrite one would make any invitation to a known // address a full account takeover. ErrPasswordNotAccepted = errors.New("invitation: this address already has an account — sign in to accept") ErrCannotInviteSelf = errors.New("invitation: you are already a member") )
var ( ErrInvalidCredentials = errors.New("invalid credentials") ErrUserInactive = errors.New("user inactive") ErrNoOrganization = errors.New("user has no organization membership") )
var ( // ErrMembershipNotInOrg is returned when a membership id names a row that // belongs to a different organization. It is reported as 404, identical to a // row that does not exist, so the endpoint cannot be used to discover which // membership ids are real. ErrMembershipNotInOrg = errors.New("membership: no such membership in this organization") // ErrLastOwner guards the one state an organization can never be in. ErrLastOwner = errors.New("membership: an organization must keep at least one owner") // ErrOwnerRoleRequiresOwner: admins manage members and admins. Granting or // revoking ownership is an owner's decision. Without this an admin could // promote themselves to owner, which makes the owner role decorative. ErrOwnerRoleRequiresOwner = errors.New("membership: only an owner may grant or revoke the owner role") )
var ( ErrTOTPAlreadySetup = errors.New("TOTP already configured") ErrTOTPNotSetup = errors.New("TOTP not configured") ErrTOTPNotVerified = errors.New("TOTP not verified") ErrInvalidTOTPCode = errors.New("invalid TOTP code") )
var ( ErrOrgQuotaExceeded = errors.New("organization quota exceeded") ErrOrgCreationRateExceeded = errors.New("organization creation rate exceeded") )
var ( ErrPasskeyNotFound = errors.New("passkey not found") ErrPasskeyChallenge = errors.New("invalid passkey challenge") ErrPasskeyRegistration = errors.New("passkey registration failed") ErrPasskeyLogin = errors.New("passkey login failed") )
var ( ErrResetTokenInvalid = errors.New("reset token is invalid or expired") ErrResetTokenUsed = errors.New("reset token has already been used") )
var ( ErrUserNotFound = errors.New("user not found") ErrWrongPassword = errors.New("current password is incorrect") ErrNothingToUpdate = errors.New("nothing to update") )
var ( ErrEmailTaken = errors.New("registration: that email is already registered") ErrRegistrationName = errors.New("registration: a name is required") ErrWeakPassword = errors.New("registration: password does not meet policy") )
var ( ErrUnsupportedProvider = errors.New("unsupported provider") ErrProviderNotConfig = errors.New("provider not configured") ErrMissingCode = errors.New("missing code") ErrEmailRequired = errors.New("verified email required") )
var ( ErrSSHKeyName = errors.New("ssh key: a name is required") ErrSSHKeyNotFound = errors.New("ssh key: no such key") // ErrNoUsableFleetKey is returned when nothing can reach the fleet. It is a // distinct error because the remedy is specific — mint one and whitelist it — // and reporting it as a generic failure sends an operator looking at hosts. ErrNoUsableFleetKey = errors.New("ssh key: this organization has no usable fleet key") )
var ErrAccountLocked = errors.New("account temporarily locked")
var ErrEmailChangeInvalid = errors.New("email change token is invalid or expired")
var ErrEmailChangeRequiresVerification = errors.New("email changes must be confirmed via the verification link")
var ErrInvalidOAuthState = errors.New("invalid or expired oauth state")
var ErrInvalidToken = errors.New("invalid token")
var ErrPasswordPolicy = errors.New("password does not meet policy")
var ErrPolicyUnknownKind = errors.New("policy: unknown policy kind")
ErrPolicyUnknownKind is a sentinel so handlers can answer 422 rather than 500 for a kind that does not exist. A refusal the caller caused is not a server fault, and reporting it as one puts an operator's typo on the 5xx dashboard.
var ErrTenantNotProvisioned = errors.New("invitation: this organization has no tenant database yet")
ErrTenantNotProvisioned means the organization has no tenant database yet, so there is nothing for a new member to be admitted into. It is distinct from ErrTenantUnavailable, which means a provisioned tenant could not be reached: one is "not yet", the other is "not right now", and only the second is worth paging anybody about.
Functions ¶
func BeeLiveness ¶
BeeLiveness is the read side of the declared-absence model, exposed so that handlers and any future sweep agree on what "offline" means.
func CeilingFor ¶
func CeilingFor(base int, trust SourceTrust, coarse bool) int
CeilingFor scales the unproven-organization allowance.
Two INDEPENDENT axes, which is easy to conflate and wrong to:
- Trust: how much the address tells you about who is asking.
- Coarseness: how many distinct actors share the bucket.
A SASE egress is coarse AND high-trust — thousands of one company's staff behind a handful of addresses. A cloud host is coarse and low-trust. A residential line is fine-grained and middling. Scaling on trust alone gives a bank's entire workforce the same allowance as one home broadband connection, which is the failure this function exists to avoid.
The multipliers are the knob most likely to need tuning against real traffic, which is why the ceiling ships in shadow mode.
func IsFirewallValidation ¶
IsFirewallValidation reports whether an error is something the operator typed, so a handler can answer 422 rather than 500. Every validation error is a deliberate refusal, and a refusal reported as a server fault puts an operator's own typo on a 5xx dashboard.
func MapProxmoxResources ¶
func MapProxmoxResources(rs proxmox.ClusterResources) []tenant.ResourceRow
MapProxmoxResources translates Proxmox's inventory into the neutral shape.
Exported and free of any client, so the mapping is testable against literal API rows — which is where the units matter. Proxmox reports memory and disk in BYTES and CPU as a core count in MaxCPU (the `CPU` field is a utilisation ratio, not a count, and using it would report every idle node as having no cores).
func NewEnrolmentToken ¶
NewEnrolmentToken mints a token and the hash to store for it.
Package-level as well as a method because the onboarding WORKER needs one and has no FleetService — and nothing about minting depends on service state. The method is kept so existing callers are unchanged.
Types ¶
type APIKeyService ¶
type APIKeyService struct {
// contains filtered or unexported fields
}
func NewAPIKeyService ¶
func NewAPIKeyService(keys repo.APIKeyRepo) *APIKeyService
func (*APIKeyService) Authenticate ¶
func (s *APIKeyService) Authenticate(ctx context.Context, public, secret string, wantScopes ...string) (*Authenticated, error)
func (*APIKeyService) Mint ¶
func (s *APIKeyService) Mint(ctx context.Context, in MintInput) (*MintResult, error)
func (*APIKeyService) Revoke ¶
Authenticate verifies a presented credential and confirms it is admitted on the surface being called.
wantScope is the surface, not a preference: the agent endpoints pass APIKeyScopeAgent and everything else passes the org/user pair, and the exclusion runs both ways. An org key must not reach the agent surface (a human or an automation forging an agent) and an agent key must not reach the org surface (a compromised hypervisor turning its credential on the rest of the tenant's API). Revoke retires a key by id.
Idempotent, inherited from the repository: revoking an already-revoked key is the state the caller wanted, and erroring would make a retried request look like a failure. Takes no scope or organization, because the id already names exactly one key — the authorization to revoke it belongs to whoever resolved that id, not to this call.
type AcceptInvitationInput ¶
type AcceptInvitationResult ¶
type AccessRuleResolver ¶
type AccessRuleResolver interface {
CheckAccessFor(ctx context.Context, email string, purpose repo.AccessPurpose) error
}
AccessRuleResolver is the area boundary between the gate and the rules layer. The gate never reads auth_access_rules itself and never re-implements the email-beats-domain precedence; if it did, the rule an operator wrote and the rule the gate enforced could silently drift apart.
type AccountDeletionService ¶
type AccountDeletionService struct {
// contains filtered or unexported fields
}
AccountDeletionService owns the two irreversible-looking operations: deleting a person, and deleting an organization.
Both are SOFT deletes, for different reasons that happen to point the same way — see Organization.DeletedAt and User.DeletedAt. Neither destroys key material or a database; purging is a separate operator step, which is what leaves a way back from a mistake made at three in the morning.
func NewAccountDeletionService ¶
func NewAccountDeletionService(db *gorm.DB) *AccountDeletionService
func (*AccountDeletionService) DeleteAccount ¶
func (s *AccountDeletionService) DeleteAccount(ctx context.Context, userID uuid.UUID, now time.Time) error
DeleteAccount soft-deletes a user.
It refuses while the account is the sole owner of any organization. The way out is to promote another owner or delete the organization — both deliberate acts, and both leaving the tenant administrable either way.
One transaction: the checks and the write must not be separated, or an ownership change landing between them lets a sole owner delete themselves anyway.
func (*AccountDeletionService) DeleteOrganization ¶
func (s *AccountDeletionService) DeleteOrganization(ctx context.Context, orgID uuid.UUID, confirmName string, now time.Time) error
DeleteOrganization soft-deletes an organization.
confirmName must equal the organization's name exactly. Not decoration: this is the single call that takes a customer's entire tenant out of service, and typing the name is the difference between meaning it and clicking the wrong row. Compared case-sensitively after trimming, because "acme" and "Acme" may be two different organizations.
func (*AccountDeletionService) SoleOwnerships ¶
func (s *AccountDeletionService) SoleOwnerships(ctx context.Context, userID uuid.UUID) ([]control.Organization, error)
SoleOwnerships lists the organizations where this account is the ONLY owner.
Returned rather than merely counted so the caller can name them: "you are the only owner of Acme and Foo" is actionable, "you cannot delete your account" is not.
type AdmissionDecision ¶
AdmissionDecision is populated on the allow path AND on the ErrUnprovenOrgCeiling path, so the denial can be logged with the numbers that produced it.
type AdmissionInput ¶
type AdmissionInput struct {
Email string
}
AdmissionInput is the identity asking to create an organization. Registration passes the submitted email; org bootstrap passes user.Email.
type AdmissionService ¶
type AdmissionService struct {
// contains filtered or unexported fields
}
AdmissionService is the single gate in front of both org-creation paths. It holds no *gorm.DB: the transaction is handed to Admit, so the gate can be constructed at wiring time with dependencies that are not open yet.
func NewAdmissionService ¶
func NewAdmissionService(rules AccessRuleResolver, limits OrgBootstrapLimits) *AdmissionService
func (*AdmissionService) Admit ¶
func (s *AdmissionService) Admit(ctx context.Context, tx *gorm.DB, in AdmissionInput) (AdmissionDecision, error)
Admit is THE GATE. It MUST be called on the transaction that will create the organization, before any row is inserted and before any provisioning job is enqueued — creating a Postgres DATABASE and ROLE for a caller who is then refused leaves an orphan nothing ever cleans up.
Evaluation order: registrable domain, create_org access rules, the unproven-organization ceiling, the total organization quota, the hourly creation rate. On success the caller MUST write decision.RegistrableDomain to organizations.registrable_domain in this same transaction, or the org it creates is invisible to every future ceiling check.
func (*AdmissionService) EnsureConfigured ¶
func (s *AdmissionService) EnsureConfigured() error
EnsureConfigured lets a caller refuse before it opens a transaction or reads anything. It is nil-receiver safe on purpose: api.Build constructs handlers with nil dependencies, and a nil gate must produce an error rather than a panic there — and a refusal rather than an allow everywhere else.
type AgentKeyMinter ¶
type AgentKeyMinter interface {
Mint(ctx context.Context, in MintInput) (*MintResult, error)
// Revoke retires a credential a re-enrolment has superseded. Without it the
// displaced key stays valid in the control database for the life of the
// organization, with nothing pointing at it and nothing able to find it.
Revoke(ctx context.Context, id uuid.UUID) error
Authenticate(ctx context.Context, public, secret string, wantScopes ...string) (*Authenticated, error)
}
AgentKeyMinter issues the agent-scoped credential a bee authenticates with. Control-side, because that credential is what resolves the tenant.
type AuthClaims ¶
type AuthClaims struct {
OrgID string `json:"org_id,omitempty"`
Type string `json:"typ"`
AMR []string `json:"amr,omitempty"`
MFA bool `json:"mfa,omitempty"`
MFAEnabled bool `json:"mfa_enabled,omitempty"`
ActorID string `json:"act_id,omitempty"`
Impersonate bool `json:"imp,omitempty"`
jwt.RegisteredClaims
}
type Authenticated ¶
Authenticated is what a verified credential establishes.
type BeeSession ¶
BeeSession is an authenticated bee together with the tenancy it was resolved through.
The organization is carried explicitly rather than read off the bee, because a tenant row has no organization column — the database IS the organization. Everything downstream needs it to reopen that database, so losing it here would mean re-authenticating to get it back.
type BeginLoginResult ¶
type BeginLoginResult struct {
Options *protocol.CredentialAssertion
Challenge string
}
type BeginRegistrationResult ¶
type BeginRegistrationResult struct {
Options *protocol.CredentialCreation
Challenge string
}
type ChangePasswordInput ¶
type ConfiguredSignInAlternatives ¶
type ConfiguredSignInAlternatives struct {
// Providers are the (name, id, secret) triples the social service handles.
Providers []SocialProviderConfig
Passkeys PasskeyCounter
}
ConfiguredSignInAlternatives answers from the running configuration and the acting user's own credentials.
func (ConfiguredSignInAlternatives) Available ¶
func (c ConfiguredSignInAlternatives) Available( ctx context.Context, actingUserID uuid.UUID, ) (bool, string, error)
Available checks the installation first, then the caller.
The order is deliberate. A configured provider is a way in for EVERYONE, which is the thing actually being protected. The acting user's passkey is weaker — it only guarantees that one person can get back in — but it is enough to make the change reversible by the person making it, which is the difference between an inconvenience and an outage.
type ConfirmResult ¶
type ConfirmResult struct {
Email string
// AlreadyVerified distinguishes a second click on the same link from the
// first. Both are success; the UI says something different for each.
AlreadyVerified bool
// TenantProvisioning reports whether provisioning was actually enqueued.
TenantProvisioning bool
}
ConfirmResult tells the caller what happened without handing back a session.
type DirectEmailSender ¶
type DirectEmailSender struct {
// contains filtered or unexported fields
}
DirectEmailSender sends synchronously over SMTP, with no retry.
Kept for development and tests, where a queue and a worker are more ceremony than the situation deserves. Production wiring should use QueuedEmailSender: a synchronous send inside a request means a slow mail server becomes a slow API, and a failed one is simply lost.
func NewDirectEmailSender ¶
func NewDirectEmailSender(svc *EmailService) *DirectEmailSender
type EffectiveResult ¶
type EffectiveResult struct {
Kind control.PolicyKind `json:"kind"`
Enabled bool `json:"enabled"`
Config control.PolicyConfig `json:"config"`
}
type EmailSender ¶
EmailSender is what the services depend on. They render a message and hand it over; whether it goes straight to SMTP or onto a queue is not their business.
The seam exists because every caller previously spawned its own goroutine, logged whatever came back, and dropped the message — four copies of the same fire-and-forget mistake. Behind an interface there is one delivery policy, and changing it does not mean editing four services.
type EmailService ¶
type EmailService struct {
// contains filtered or unexported fields
}
func NewEmailService ¶
func NewEmailService(host string, port int, user, password, from string) *EmailService
func (*EmailService) Send ¶
func (s *EmailService) Send(to, subject, htmlBody string) error
type EmailVerificationService ¶
type EmailVerificationService struct {
// contains filtered or unexported fields
}
EmailVerificationService issues and redeems the proof that a registrant can read the mailbox they signed up with.
Redeeming is what releases tenant provisioning. Registration deliberately does not provision: a CREATE DATABASE and CREATE ROLE per unverified signup is a resource an unauthenticated caller can mint, and before this split every registration minted one before anyone had read a single email.
func NewEmailVerificationService ¶
func NewEmailVerificationService( tokens repo.EmailVerificationRepo, users repo.UserRepo, memberships repo.MembershipRepo, email EmailSender, jobs workflow.Enqueuer, audit repo.AuthAuditRepo, baseURL string, ) *EmailVerificationService
func (*EmailVerificationService) Confirm ¶
func (s *EmailVerificationService) Confirm(ctx context.Context, rawToken string) (*ConfirmResult, error)
Confirm redeems a token: it marks the address verified and releases tenant provisioning. It issues no session — a confirmed registrant signs in through the ordinary login path, so that every session in the system is minted by one piece of code with one set of checks.
func (*EmailVerificationService) Issue ¶
Issue mints a token for a user and mails the link.
It returns an error, and registration propagates it. That is deliberate: if the token cannot be written, the account exists with no way to ever confirm it, and reporting success would leave the registrant permanently unable to sign in with no indication why.
func (*EmailVerificationService) Resend ¶
func (s *EmailVerificationService) Resend(ctx context.Context, emailAddr string) error
Resend issues a fresh link. It reports success unconditionally.
Every branch below returns nil: unknown address, inactive user, already verified, and throttled all look identical from outside. This endpoint is unauthenticated and takes an arbitrary email, so any branch that answered differently would be a membership oracle — "is this address registered here, and has it been confirmed" — answerable by anyone.
type EnrolInput ¶
type EnrolInput struct {
EnrolmentToken string
MachineID string
Hostname string
AgentVersion string
// PeerAddress is where the enrolment request came from. See
// repo.preferredAddress: an agent knows its hostname and not how the control
// plane reaches it, and the address of the request that just arrived is the
// one candidate that is reachable by demonstration.
PeerAddress string
}
type EnrolResult ¶
type EnrolmentRedeemer ¶
type EnrolmentRedeemer interface {
RedeemToken(ctx context.Context, tokenHash string, now time.Time) (uuid.UUID, error)
}
EnrolmentRedeemer consumes an enrolment token and reports which organization it admits a host into. Control-side: the caller is unauthenticated and has no tenant yet.
type EstateService ¶
type EstateService struct {
// contains filtered or unexported fields
}
EstateService owns everything a request needs in order to touch an estate or a credential: resolving the caller's tenant database, deriving the key that seals credential material, and keeping the two records consistent with each other.
It exists because the repositories are deliberately narrow. They enforce the rules that must hold for a single row — AAD binding, scope canonicalisation, fingerprint over the identifying component — and know nothing about which tenant is asking or where the key came from. That is this layer's job.
func NewEstateService ¶
func NewEstateService(tenants TenantResolver, masterKey []byte, scopes repo.ProviderScopeRegistry) (*EstateService, error)
func (*EstateService) Delete ¶
Delete removes the estate and the credential it was bound to.
Both, deliberately. The binding is 1:1, so deleting only the estate would leave a sealed credential nothing references — unreachable through the API, invisible in any listing, and still a live secret for whatever provider it belongs to. The foreign key is ON DELETE RESTRICT, so the order is forced.
func (*EstateService) Get ¶
func (s *EstateService) Get(ctx context.Context, orgID, id uuid.UUID) (*EstateView, error)
func (*EstateService) List ¶
func (s *EstateService) List(ctx context.Context, orgID uuid.UUID) ([]EstateView, error)
func (*EstateService) Register ¶
func (s *EstateService) Register(ctx context.Context, in RegisterEstateInput) (*EstateView, error)
Register creates the credential and the estate that binds it, in one transaction.
Order is forced: Estate.CredentialID is NOT NULL, so the credential must exist first. That makes a partial failure possible — a credential with no estate is an orphaned secret nobody can see or reach — which is exactly why this runs in a transaction rather than as two calls from a handler.
func (*EstateService) RotateCredential ¶
func (s *EstateService) RotateCredential(ctx context.Context, orgID, estateID uuid.UUID, identity string, secret repo.Secret) (*EstateView, error)
RotateCredential replaces the sealed material on an estate's credential in place, keeping the binding and the estate untouched.
In place rather than create-and-swap because the binding is 1:1: a second credential for one estate is unrepresentable, so a rotation that created one could not be committed. The fingerprint changes with the material, which is how an operator confirms the rotation actually took.
type EstateView ¶
type EstateView struct {
ID uuid.UUID
Name string
Provider string
Scope map[string]string
ScopeVersion int
CredentialID uuid.UUID
CredentialName string
CredentialKind string
Fingerprint string
}
EstateView is what leaves this service. It never carries sealed material: nothing above this layer has a reason to see a credential's plaintext, and a field that does not exist cannot be serialised into a response by accident.
type FeatureFlagService ¶
type FeatureFlagService struct {
// contains filtered or unexported fields
}
FeatureFlagService resolves flags and is the only place a write is validated against the registry.
func NewFeatureFlagService ¶
func NewFeatureFlagService(flags repo.FeatureFlagRepo) *FeatureFlagService
func (*FeatureFlagService) ClearPlatform ¶
func (s *FeatureFlagService) ClearPlatform(ctx context.Context, key string) error
ClearPlatform and ClearOrg drop a row so the level below decides again.
func (*FeatureFlagService) Effective ¶
func (s *FeatureFlagService) Effective( ctx context.Context, orgID uuid.UUID, key string, ) (*control.EffectiveFlag, error)
Effective resolves one flag and says which level decided it.
func (*FeatureFlagService) Enabled ¶
func (s *FeatureFlagService) Enabled(ctx context.Context, orgID uuid.UUID, key string) (bool, error)
Enabled is the one call the rest of the codebase should need.
It takes the organization because almost every caller has one; pass uuid.Nil on the platform surface. An unregistered key returns the error rather than false: a caller asking about a flag that does not exist has a bug, and answering "off" would hide it behind behaviour that looks deliberate.
func (*FeatureFlagService) ListEffective ¶
func (s *FeatureFlagService) ListEffective( ctx context.Context, orgID uuid.UUID, ) ([]control.EffectiveFlag, error)
ListEffective resolves every declared flag for one organization.
Two queries rather than one per flag: the registry is small and fully known, so the rows are fetched in bulk and matched in memory. A per-flag round trip would make a settings screen's cost grow with the registry.
func (*FeatureFlagService) ListPublicEffective ¶
func (s *FeatureFlagService) ListPublicEffective(ctx context.Context) ([]control.EffectiveFlag, error)
ListPublicEffective resolves the flags an unauthenticated caller may read.
NO ORGANIZATION IS CONSULTED, and not because the caller happens not to have one — because a public flag is platform-scoped by construction, so there is no organization row that could apply. Passing uuid.Nil would reach the same answer today; naming it here means a future tenant-scoped public flag fails to compile a decision rather than silently resolving to whatever the platform happens to say.
func (*FeatureFlagService) OverridesFor ¶
func (s *FeatureFlagService) OverridesFor( ctx context.Context, key string, ) ([]repo.FlagOverride, error)
OverridesFor lists the organizations that have set this flag themselves, which is the question a platform operator asks before changing the default.
func (*FeatureFlagService) PlatformEnabled ¶
PlatformEnabled answers one platform-scoped flag with no organization in play.
For the server's own gates — the sign-in handler asking whether it is allowed to run at all — where there is no tenant yet and inventing uuid.Nil at each call site invites somebody passing a real one and getting a per-tenant answer to an installation-wide question.
func (*FeatureFlagService) SetOrg ¶
func (s *FeatureFlagService) SetOrg( ctx context.Context, orgID uuid.UUID, key string, value bool, by uuid.UUID, ) error
SetOrg stores one organization's override.
REFUSED FOR A PLATFORM-SCOPED FLAG, with an error that says so rather than "unknown". Naming a real flag at the wrong level and mistyping one are different mistakes with different fixes, and collapsing them sends an operator to check a spelling that was right.
func (*FeatureFlagService) SetPlatform ¶
func (s *FeatureFlagService) SetPlatform( ctx context.Context, key string, value bool, by uuid.UUID, acknowledgeLockout bool, ) error
SetPlatform stores the installation-wide value.
acknowledgeLockout is the operator saying, explicitly, that they intend to disable a way of signing in with nothing else configured. It is ignored for every flag that does not gate sign-in.
func (*FeatureFlagService) WithSignInAlternatives ¶
func (s *FeatureFlagService) WithSignInAlternatives(a SignInAlternatives) *FeatureFlagService
WithSignInAlternatives wires the lockout guard.
Separate from the constructor so every existing caller and test keeps working unchanged, and so the guard's absence is visible at the wiring site rather than hidden in a nil argument.
type FinishLoginInput ¶
type FinishLoginInput struct {
Challenge string
Response *protocol.ParsedCredentialAssertionData
IP string
UA string
}
type FinishRegistrationInput ¶
type FinishRegistrationInput struct {
Challenge string
Response *protocol.ParsedCredentialCreationData
Name string
}
type FirewallService ¶
type FirewallService struct {
// contains filtered or unexported fields
}
FirewallService owns authoring: rulesets, the rules inside them, and which fleets they are bound to.
It does NOT publish. Turning authored rules into a revision a bee converges on is a separate step with a separate failure mode — an edit that is merely saved changes nothing on any host, which is what makes it safe to edit in the middle of the day.
func NewFirewallService ¶
func NewFirewallService(tenants TenantResolver) *FirewallService
func (*FirewallService) AddSetElement ¶
func (s *FirewallService) AddSetElement( ctx context.Context, orgID, setID uuid.UUID, value, comment, label string, ) (*tenant.FirewallSetElement, error)
AddSetElement puts one address or prefix into a set.
Validated through the SET, not on its own, because whether 10.0.0.0/8 is acceptable depends on the family the set was declared with — an ipv6 element in an ipv4 set renders a declaration nft rejects, and nft rejects the whole ruleset over one bad line.
func (*FirewallService) Bind ¶
func (s *FirewallService) Bind( ctx context.Context, orgID uuid.UUID, hiveID, swarmID *uuid.UUID, rulesetID uuid.UUID, priority int, enabled bool, ) (*tenant.FirewallBinding, error)
Bind attaches a ruleset to a fleet, or to the whole organization when hiveID is nil.
The org-wide case is what covers a host between fleets. It is also how a base ruleset stops being copied per fleet, which is the duplication the whole structure exists to avoid.
func (*FirewallService) CreateRule ¶
func (s *FirewallService) CreateRule(ctx context.Context, orgID, rulesetID uuid.UUID, in RuleInput) (*tenant.FirewallRule, error)
func (*FirewallService) CreateRuleset ¶
func (s *FirewallService) CreateRuleset(ctx context.Context, orgID uuid.UUID, name, description string) (*tenant.FirewallRuleset, error)
func (*FirewallService) CreateSet ¶
func (s *FirewallService) CreateSet( ctx context.Context, orgID uuid.UUID, name, family, description, purpose string, ) (*tenant.FirewallSet, error)
CreateSet makes a named list rules can point at.
Purpose defaults to general — the harmless one — because a set created without thought must never widen the `from=` restriction that onboarding installs on the fleet key. Asking for management is a deliberate act.
func (*FirewallService) DeleteRule ¶
func (*FirewallService) DeleteRuleset ¶
func (*FirewallService) GetSet ¶
func (s *FirewallService) GetSet(ctx context.Context, orgID, id uuid.UUID) (*tenant.FirewallSet, error)
func (*FirewallService) ListBindings ¶
func (s *FirewallService) ListBindings(ctx context.Context, orgID uuid.UUID) ([]tenant.FirewallBinding, error)
func (*FirewallService) ListRules ¶
func (s *FirewallService) ListRules(ctx context.Context, orgID, rulesetID uuid.UUID) ([]tenant.FirewallRule, error)
func (*FirewallService) ListRulesets ¶
func (s *FirewallService) ListRulesets(ctx context.Context, orgID uuid.UUID) ([]tenant.FirewallRuleset, error)
func (*FirewallService) ListSets ¶
func (s *FirewallService) ListSets(ctx context.Context, orgID uuid.UUID) ([]tenant.FirewallSet, error)
func (*FirewallService) ManagementSets ¶
func (s *FirewallService) ManagementSets(ctx context.Context, orgID uuid.UUID) ([]tenant.FirewallSet, error)
ManagementSets returns the sets that feed the authorized_keys `from=` clause.
Purpose is read from the set rather than inferred from the ports a rule opens. Corosync peers are permitted on 22 so pvecm can distribute cluster configuration, so inferring would list every hypervisor in the fleet and make a stolen control-plane key usable from any of them.
func (*FirewallService) Preview ¶
func (s *FirewallService) Preview( ctx context.Context, orgID uuid.UUID, hiveID, swarmID *uuid.UUID, ) ([]tenant.FirewallRule, error)
Preview returns the flattened, ordered rules a fleet would receive.
It exists because composition is invisible from any single screen: an operator editing "base" cannot see it interleaving with "dc1", and binding priority silently outranks the positions they were just looking at. Answering "what does hv5 actually get" is the difference between authoring a policy and hoping about one.
A nil hiveID answers for a host that belongs to no fleet. A nil swarmID means "not in a swarm, or we do not know which" — both answer with hive and org scope, which is the same thing a real host in no swarm receives.
func (*FirewallService) RemoveSetElement ¶
func (*FirewallService) UpdateBinding ¶
func (*FirewallService) UpdateBindingScope ¶
func (s *FirewallService) UpdateBindingScope( ctx context.Context, orgID, id uuid.UUID, priority int, enabled bool, scope *repo.BindingScopeChange, ) error
UpdateBindingScope updates a binding and, when scope is non-nil, moves it.
The scope is validated with the SAME rules the create path uses, because a binding moved into an invalid scope is indistinguishable from one created there. In particular a swarm without its hive is refused: swarm names are unique only within a hive, and BindingsForScope matches swarm bindings on the swarm id ALONE — safe only because the pair is checked here. Accepting the half would let a binding written for one cluster's "prod" group resolve against another cluster's.
func (*FirewallService) UpdateRule ¶
func (s *FirewallService) UpdateRule(ctx context.Context, orgID, id uuid.UUID, in RuleInput) (*tenant.FirewallRule, error)
func (*FirewallService) UpdateRuleset ¶
func (s *FirewallService) UpdateRuleset(ctx context.Context, orgID, id uuid.UUID, name, description string) (*tenant.FirewallRuleset, error)
type FleetService ¶
type FleetService struct {
// contains filtered or unexported fields
}
FleetService owns the two-sided shape the fleet now has: control answers which tenant is calling, and every fleet row lives in that tenant's database.
It therefore holds no FleetRepo. One cannot exist before a request arrives, because which database it binds to is not known until the caller is authenticated.
func NewFleetService ¶
func NewFleetService(tenants TenantResolver, enrolment EnrolmentRedeemer, keys AgentKeyMinter, audit repo.AuthAuditRepo) (*FleetService, error)
NewFleetService accepts missing dependencies and denies at request time rather than refusing to build.
That is the convention here — see Deps.Admission — and it exists because api.Build is exercised with partially-populated Deps in the router tests, which assert routing and auth properties and have no interest in a tenant manager. Refusing here would make an unrelated test suite carry a database.
Denying is not the same as ignoring. Every entry point below checks before it acts, so a genuinely misconfigured deployment answers 503 on the fleet surface and nothing else, instead of panicking on the first agent heartbeat.
func (*FleetService) AppendLogs ¶
func (s *FleetService) AppendLogs(ctx context.Context, sess *BeeSession, taskID uuid.UUID, lines []LogLine) (int, error)
AppendLogs stores a batch of streamed output.
Lines are truncated rather than rejected: losing the tail of one enormous line is a far better outcome during a rotation than discarding a batch that also carried the error message explaining what went wrong.
func (*FleetService) Authenticate ¶
func (s *FleetService) Authenticate(ctx context.Context, public, secret string) (*BeeSession, error)
Authenticate resolves a bee from the agent credential it presents.
Two steps, in this order, because the first is what makes the second possible: the key is verified in the control database, which yields the organization, and only then can that organization's database be opened to find the bee.
func (*FleetService) DesiredFirewallFor ¶
func (s *FleetService) DesiredFirewallFor( ctx context.Context, sess *BeeSession, ) (*tenant.FirewallSnapshot, error)
DesiredFirewallFor returns what the host running this bee should be enforcing, or nil for HOLD.
Nil is a real answer with a specific meaning, and callers must not flatten it into an empty ruleset: a bee whose machine is between fleets, or whose hive has published nothing, is told to keep what it has. Flushing there would strip a mid-migration host of its management access at the moment it is least reachable.
func (*FleetService) DesiredMembershipFor ¶
func (s *FleetService) DesiredMembershipFor( ctx context.Context, sess *BeeSession, ) (*tenant.MembershipDesired, error)
DesiredMembershipFor returns the composition the host running this bee should converge on, or nil when there is nothing to converge on.
NIL MEANS HOLD, exactly as an absent firewall does. It is returned for a machine in no hive and for a hive that has published nothing, and neither is "you should be in no cluster" — reading it that way would make an unpublished hive dissolve itself node by node the first time every agent polled.
The composition is re-rendered here rather than read from a stored body, and the revision comes from the hive row. A published revision whose rendered digest no longer matches means a write changed membership without publishing; that is repaired by publishing rather than served, because serving a body whose digest disagrees with the one the heartbeat advertised would make the agent converge and immediately be judged diverged again — forever.
func (*FleetService) Enrol ¶
func (s *FleetService) Enrol(ctx context.Context, in EnrolInput) (*EnrolResult, error)
func (*FleetService) FinaliseDepartures ¶
func (s *FleetService) FinaliseDepartures(ctx context.Context, sess *BeeSession) error
FinaliseDepartures clears the machines this bee's hive has actually dropped.
Driven from the heartbeat because that is when the evidence arrives: a quorate member reporting a composition that no longer lists a departing node is the authority. The departing node itself is not — after `pvecm delnode` it still believes it is a member, which is precisely why it must not be powered back on.
func (*FleetService) Heartbeat ¶
func (s *FleetService) Heartbeat(ctx context.Context, in HeartbeatInput) (*HeartbeatResult, error)
Heartbeat records liveness, applies any declared absence, and returns this bee's one outstanding task — waiting for one to appear if asked.
Liveness and task delivery share a request on purpose. Two endpoints would mean two ways for a bee to be "seen", and a bee parked on a long poll would have to heartbeat separately to avoid looking dead — which is precisely the confusion this design exists to remove.
func (*FleetService) NewEnrolmentToken ¶
func (s *FleetService) NewEnrolmentToken() (raw, hash string, err error)
NewEnrolmentToken returns a token to hand to an operator and the hash to store. The raw value exists only in this return and in whatever the operator pastes onto a host.
func (*FleetService) PublishMembership ¶
PublishMembership commits a hive's composition, and is safe to call when nothing changed.
Exposed on the service because every write that changes membership intent has to call it, and those writes live in handlers and jobs that should not be reaching into repo directly.
func (*FleetService) RecordAppliedFirewall ¶
func (s *FleetService) RecordAppliedFirewall( ctx context.Context, sess *BeeSession, revision int, digest string, ) error
RecordAppliedFirewall stores what a bee reports it is enforcing.
Reported rather than assumed, because a publish only says what should be running. Marking the fleet converged when an operator clicks Publish would report a policy as enforced on every host that happened to be switched off.
func (*FleetService) RecordAppliedMembership ¶
func (s *FleetService) RecordAppliedMembership( ctx context.Context, sess *BeeSession, revision int, digest string, ) error
RecordAppliedMembership stores the composition a bee reports it has converged on.
func (*FleetService) RecordObservedGuests ¶
func (s *FleetService) RecordObservedGuests( ctx context.Context, sess *BeeSession, guests *int, inACluster bool, ) error
RecordObservedGuests stores what a host says it is running.
Written ONLY for a machine in no cluster, which is the gap: the capacity sync observes per cluster, so a fleetless machine is never observed by it at all, and fleetless is exactly the state a machine is in when it is about to join one. Restricting it to that case keeps a single writer per row rather than letting an agent report and a sync overwrite in turn, which would flap on every tick where the two disagree.
The condition lives in the UPDATE rather than in a read-then-write, so a machine that joined a cluster between the two cannot have its swept-up observation land afterwards.
func (*FleetService) RecordObservedMembership ¶
func (s *FleetService) RecordObservedMembership( ctx context.Context, sess *BeeSession, obs *tenant.Observed, fingerprint string, hostKeys []string, rootKey string, secrets *int, carriesConfig *bool, ) error
RecordObservedMembership stores what a host says about the cluster it can see.
UNCONDITIONAL, unlike the observed guest count, which is written only for a fleetless machine to keep a single writer per row. There is no competing writer here: the capacity sync records what the Proxmox API says about a CLUSTER, and this records what one HOST says about itself. They are allowed to disagree, and a disagreement between them is the useful signal — it is how a node that has silently fallen out of corosync while still answering its API becomes visible.
An agent that did not report is distinguished from one reporting a standalone host by observed_membership_at, which is why the caller passes a pointer: an older agent omits the field entirely, and reading that as "standalone" would invent an observation nobody made and hand it to a loop that acts on it.
func (*FleetService) RecordObservedNodeName ¶
func (s *FleetService) RecordObservedNodeName( ctx context.Context, sess *BeeSession, node string, ) error
RecordObservedNodeName stores the Proxmox node name a host reports for itself.
A machine has no node name until it is in a cluster, so an arrival records membership without one and the host supplies it once it has joined. Written only when the row has none — see the repo function for why a rename must not come from a heartbeat.
func (*FleetService) Report ¶
func (s *FleetService) Report(ctx context.Context, in ReportInput) (*tenant.FleetTask, error)
type HeartbeatInput ¶
type HeartbeatInput struct {
// Session rather than a bare bee: a tenant row carries no organization, so
// without it there is nothing to reopen the right database with.
Session *BeeSession
AgentVersion string
// AgentDigest is the sha256 of the binary this agent is running — what the
// upgrade decision is actually made on. See tenant.Bee.AgentDigest.
AgentDigest string
// UnavailableFor is the bee declaring, before it acts, roughly how long it
// will be gone. Zero leaves any standing declaration untouched.
// Back withdraws a standing declaration: the bee has returned.
Back bool
// Wait asks the handler to hold the request open until a task appears.
Wait time.Duration
}
type HeartbeatResult ¶
type ImpersonationService ¶
type ImpersonationService struct {
// contains filtered or unexported fields
}
func NewImpersonationService ¶
func NewImpersonationService( users repo.UserRepo, memberships repo.MembershipRepo, grants repo.ImpersonationGrantRepo, sessions *TokenSessionService, audit repo.AuthAuditRepo, ) *ImpersonationService
func (*ImpersonationService) Start ¶
func (s *ImpersonationService) Start(ctx context.Context, in StartImpersonationInput) (*StartImpersonationResult, error)
func (*ImpersonationService) ToggleSupport ¶
func (s *ImpersonationService) ToggleSupport(ctx context.Context, in ToggleSupportInput) error
type InvitationPreview ¶
type InvitationPreview struct {
Email string
Organization string
Role control.MemberRole
NeedsAccount bool
NeedsAccountName bool
}
InvitationPreview is what the accept page needs in order to render before the invitee commits to anything.
type InvitationService ¶
type InvitationService struct {
// contains filtered or unexported fields
}
func NewInvitationService ¶
func NewInvitationService( db *gorm.DB, invitations repo.InvitationRepo, users repo.UserRepo, memberships repo.MembershipRepo, orgs repo.OrganizationRepo, policies *PolicyService, accessRules AccessRuleResolver, email EmailSender, audit repo.AuthAuditRepo, baseURL string, ) *InvitationService
func (*InvitationService) Accept ¶
func (s *InvitationService) Accept(ctx context.Context, in AcceptInvitationInput) (*AcceptInvitationResult, error)
Accept redeems an invitation. It issues NO session, on either branch.
A new invitee sets a password here and then signs in through /auth/login like anyone else; an existing account gains the membership and signs in as it already could. Keeping session minting on the login path alone means the checks that guard it — lockout, MFA, the confirmation gate — are written once and cannot be skipped by arriving through this door.
func (*InvitationService) Invite ¶
func (s *InvitationService) Invite(ctx context.Context, in InviteInput) (*InvitationView, error)
Invite creates an invitation and mails the link.
The organization comes from the caller's principal, never from the request — the handler resolves it — so this method cannot be used to invite someone into a tenant the caller has no membership in.
func (*InvitationService) List ¶
func (s *InvitationService) List(ctx context.Context, orgID uuid.UUID) ([]InvitationView, error)
func (*InvitationService) Preview ¶
func (s *InvitationService) Preview(ctx context.Context, rawToken string) (*InvitationPreview, error)
Preview describes an invitation to whoever holds its token.
Possession of the token IS the authorization: it was mailed to the address it names. It reveals whether that address already has an account, which the page needs in order to ask for a password or to send them to sign in — and which a token holder is entitled to know about their own address.
type InvitationView ¶
type InvitationView struct {
ID uuid.UUID
Email string
Role control.MemberRole
ExpiresAt time.Time
CreatedAt time.Time
// Status is derived rather than stored: the row carries accepted_at,
// revoked_at and expires_at, and exactly one reading of those is correct.
// Deriving it in one place stops the API and the UI disagreeing about what
// an expired-but-unaccepted invitation is.
Status string
}
type InviteInput ¶
type IssueTrackedInput ¶
type IssuedTokens ¶
type LoginInput ¶
type LoginResult ¶
type LoginService ¶
type LoginService struct {
// contains filtered or unexported fields
}
func NewLoginService ¶
func NewLoginService(users repo.UserRepo, memberships repo.MembershipRepo, totps repo.UserTOTPRepo, tokens *TokenService, sessions *TokenSessionService, policies *PolicyService, challenges repo.AuthChallengeRepo, failures repo.AuthFailureRepo) *LoginService
func (*LoginService) Login ¶
func (s *LoginService) Login(ctx context.Context, in LoginInput) (*LoginResult, error)
type MFAService ¶
type MFAService struct {
// contains filtered or unexported fields
}
func NewMFAService ¶
func NewMFAService( totps repo.UserTOTPRepo, passkeys repo.UserPasskeyRepo, users repo.UserRepo, encryptionKey string, issuerName string, ) *MFAService
func (*MFAService) DisableTOTP ¶
func (*MFAService) SetAudit ¶
func (s *MFAService) SetAudit(r repo.AuthAuditRepo)
func (*MFAService) SetupTOTP ¶
func (s *MFAService) SetupTOTP(ctx context.Context, userID uuid.UUID, email string) (*TOTPSetupResult, error)
func (*MFAService) UserHasMFA ¶
func (*MFAService) ValidateTOTP ¶
func (*MFAService) VerifyAndActivateTOTP ¶
type MeResult ¶
type MeResult struct {
User struct {
ID string `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Role string `json:"role"`
IsActive bool `json:"is_active"`
}
Organization struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
} `json:"organization"`
}
type MemberView ¶
type MembershipService ¶
type MembershipService struct {
// contains filtered or unexported fields
}
func NewMembershipService ¶
func NewMembershipService(db *gorm.DB, memberships repo.MembershipRepo, audit repo.AuthAuditRepo) *MembershipService
func (*MembershipService) List ¶
func (s *MembershipService) List(ctx context.Context, orgID uuid.UUID) ([]MemberView, error)
func (*MembershipService) Remove ¶
func (s *MembershipService) Remove(ctx context.Context, in RemoveMemberInput) error
func (*MembershipService) UpdateRole ¶
func (s *MembershipService) UpdateRole(ctx context.Context, in UpdateRoleInput) error
type MintResult ¶
MintResult carries the plaintext halves. They exist here and nowhere else: only hashes are stored, so a key that is lost is replaced, never recovered.
type MyOrganizationResult ¶
type NetworkClassifier ¶
type NetworkClassifier struct {
// contains filtered or unexported fields
}
NetworkClassifier decides from operator-supplied CIDR ranges alone. It cannot distinguish corporate from residential — that needs allocation data — so it reports everything unmatched as SourceResidential, which is the conservative choice: residential gets per-address bucketing and the ordinary ceiling, neither the relaxation corroboration would earn nor the tightening a datacenter gets.
func NewNetworkClassifier ¶
func NewNetworkClassifier(datacenterCIDRs, anonymisedCIDRs, enterpriseCIDRs []string) *NetworkClassifier
NewNetworkClassifier builds a classifier. Unparseable entries are skipped rather than fatal: a typo in an operator's range list must not take registration down.
func (*NetworkClassifier) Classify ¶
func (c *NetworkClassifier) Classify(in SourceInput) SourceKey
type OrgBootstrapLimits ¶
type OrgBootstrapService ¶
type OrgBootstrapService struct {
// contains filtered or unexported fields
}
func NewOrgBootstrapService ¶
func NewOrgBootstrapService(db *gorm.DB, jobs workflow.Enqueuer, admission *AdmissionService) *OrgBootstrapService
func (*OrgBootstrapService) EnsureUserHasWorkspace ¶
func (s *OrgBootstrapService) EnsureUserHasWorkspace(ctx context.Context, user *control.User) (*control.Membership, error)
type PasskeyCounter ¶
type PasskeyCounter interface {
ListByUserID(ctx context.Context, userID uuid.UUID) ([]control.UserPasskey, error)
}
PasskeyCounter is the slice of the passkey store this needs.
type PasskeyService ¶
type PasskeyService struct {
// contains filtered or unexported fields
}
func NewPasskeyService ¶
func NewPasskeyService( passkeys repo.UserPasskeyRepo, users repo.UserRepo, memberships repo.MembershipRepo, tokens *TokenService, sessions *TokenSessionService, jwtSecret string, rpID, rpOrigin, rpName string, ) (*PasskeyService, error)
func (*PasskeyService) BeginLogin ¶
func (s *PasskeyService) BeginLogin(ctx context.Context) (*BeginLoginResult, error)
func (*PasskeyService) BeginRegistration ¶
func (s *PasskeyService) BeginRegistration(ctx context.Context, userID uuid.UUID) (*BeginRegistrationResult, error)
func (*PasskeyService) FinishLogin ¶
func (s *PasskeyService) FinishLogin(ctx context.Context, in FinishLoginInput) (*IssuedTokens, string, error)
func (*PasskeyService) FinishRegistration ¶
func (s *PasskeyService) FinishRegistration(ctx context.Context, userID uuid.UUID, in FinishRegistrationInput) (*control.UserPasskey, error)
func (*PasskeyService) List ¶
func (s *PasskeyService) List(ctx context.Context, userID uuid.UUID) ([]control.UserPasskey, error)
func (*PasskeyService) SetAudit ¶
func (s *PasskeyService) SetAudit(r repo.AuthAuditRepo)
func (*PasskeyService) SetChallenges ¶
func (s *PasskeyService) SetChallenges(r repo.AuthChallengeRepo)
type PasswordResetService ¶
type PasswordResetService struct {
// contains filtered or unexported fields
}
func NewPasswordResetService ¶
func NewPasswordResetService( resets repo.PasswordResetRepo, users repo.UserRepo, email EmailSender, baseURL string, sessions *TokenSessionService, policies *PolicyService, memberships repo.MembershipRepo, audit repo.AuthAuditRepo, ) *PasswordResetService
func (*PasswordResetService) RequestReset ¶
func (s *PasswordResetService) RequestReset(ctx context.Context, emailAddr string) error
func (*PasswordResetService) ResetPassword ¶
func (s *PasswordResetService) ResetPassword(ctx context.Context, rawToken, newPassword string) error
type PolicyService ¶
type PolicyService struct {
// contains filtered or unexported fields
}
func NewPolicyService ¶
func NewPolicyService( platform repo.PlatformPolicyRepo, org repo.OrgPolicyRepo, users repo.UserRepo, passkeys repo.UserPasskeyRepo, ) *PolicyService
func (*PolicyService) CheckUserCompliance ¶
func (s *PolicyService) CheckUserCompliance(ctx context.Context, userID, orgID uuid.UUID) ([]PolicyViolation, error)
func (*PolicyService) DeleteOrgPolicy ¶
func (s *PolicyService) DeleteOrgPolicy(ctx context.Context, orgID uuid.UUID, kind control.PolicyKind) error
func (*PolicyService) DeletePlatformPolicy ¶
func (s *PolicyService) DeletePlatformPolicy(ctx context.Context, kind control.PolicyKind) error
func (*PolicyService) EffectivePolicy ¶
func (s *PolicyService) EffectivePolicy(ctx context.Context, orgID uuid.UUID, kind control.PolicyKind) (*EffectiveResult, error)
EffectivePolicy merges the platform and organization rows for one policy kind.
A DISABLED row contributes nothing — neither its enabled bit nor its config. That second half was missing: the config was read from both rows unconditionally, so as long as ANY level had the kind switched on, a switched-off level's stale numbers went on tightening the result. A platform session_max_lifetime row with Enabled=false and MaxLifetimeMinutes=5 beat an enabled org row of 480, and every session in that organization expired after five minutes because of a policy an operator had explicitly turned off.
"Disabled" is the identity element of this merge, and the zero values already behave that way for every operator here — max() for a minimum length, min-of-positives for a lifetime ceiling, OR for the complexity flags. So gating the READ is the entire fix; nothing downstream had to change.
Note the merge can only ever TIGHTEN, which is why the defect was an availability and correctness problem rather than a privilege-escalation one. It is also why the restriction/permission split matters: restrictions merge with OR so an organization relying on its own strict setting does not lose it when the platform loosens.
func (*PolicyService) ListEffectivePolicies ¶
func (s *PolicyService) ListEffectivePolicies(ctx context.Context, orgID uuid.UUID) ([]EffectiveResult, error)
func (*PolicyService) ListOrgPolicies ¶
func (*PolicyService) ListPlatformPolicies ¶
func (s *PolicyService) ListPlatformPolicies(ctx context.Context) ([]control.PlatformPolicy, error)
func (*PolicyService) PreEnableCheck ¶
func (s *PolicyService) PreEnableCheck(ctx context.Context, orgID *uuid.UUID, kind control.PolicyKind) (int64, error)
func (*PolicyService) SetOrgPolicy ¶
func (s *PolicyService) SetOrgPolicy(ctx context.Context, createdBy uuid.UUID, orgID uuid.UUID, kind control.PolicyKind, enabled bool, config control.PolicyConfig) (*control.OrgPolicy, error)
func (*PolicyService) SetPlatformPolicy ¶
func (s *PolicyService) SetPlatformPolicy(ctx context.Context, createdBy uuid.UUID, kind control.PolicyKind, enabled bool, config control.PolicyConfig) (*control.PlatformPolicy, error)
func (*PolicyService) ValidatePassword ¶
type PolicyViolation ¶
type PolicyViolation struct {
Kind control.PolicyKind `json:"kind"`
Message string `json:"message"`
}
type ProfileResult ¶
type ProfileService ¶
type ProfileService struct {
// contains filtered or unexported fields
}
func NewProfileService ¶
func NewProfileService( users repo.UserRepo, organizations repo.OrganizationRepo, memberships repo.MembershipRepo, totps repo.UserTOTPRepo, policies *PolicyService, sessions *TokenSessionService, audit repo.AuthAuditRepo, ) *ProfileService
func (*ProfileService) ChangePassword ¶
func (s *ProfileService) ChangePassword(ctx context.Context, userID uuid.UUID, in ChangePasswordInput) error
func (*ProfileService) ConfirmEmailChange ¶
func (s *ProfileService) ConfirmEmailChange(ctx context.Context, rawToken string) error
func (*ProfileService) GetProfile ¶
func (s *ProfileService) GetProfile(ctx context.Context, userID, orgID uuid.UUID) (*ProfileResult, error)
func (*ProfileService) RequestEmailChange ¶
func (*ProfileService) SetEmailChange ¶
func (s *ProfileService) SetEmailChange(r repo.EmailChangeRepo, email EmailSender, baseURL string)
func (*ProfileService) UpdateProfile ¶
func (s *ProfileService) UpdateProfile(ctx context.Context, userID uuid.UUID, in UpdateProfileInput) (*control.User, error)
type ProxmoxInventoryService ¶
type ProxmoxInventoryService struct {
// contains filtered or unexported fields
}
ProxmoxInventoryService reads a hive's live inventory from its Proxmox API.
The ONLY place in this system that knows Proxmox's wire format. Everything downstream — the capacity fold, the drain state, the departure and arrival gates — works on tenant.ResourceRow, so adding a second provider is a second mapper here rather than a change to any of the logic that decides things.
func NewProxmoxInventoryService ¶
func NewProxmoxInventoryService(t TenantResolver, scopes repo.ProviderScopeRegistry) *ProxmoxInventoryService
func (*ProxmoxInventoryService) AgentProxmoxCredential ¶
func (s *ProxmoxInventoryService) AgentProxmoxCredential( ctx context.Context, orgID, hypervisorID uuid.UUID, ) (tokenID, secret, apiURL string, err error)
AgentProxmoxCredential returns the API token the agent on one machine should hold.
THE HIVE'S CREDENTIAL, and that is forced rather than chosen. Proxmox keeps API tokens in /etc/pve/priv/token.cfg, which lives on pmxcfs and is therefore replicated across the whole cluster: a token minted for one node exists on every node, a cluster join REPLACES that database and destroys whatever the joining node had, and a departure carries the cluster's copy away with it. A per-host credential cannot survive the lifecycle this system automates.
A machine in NO hive returns an empty credential and no error. That is a real state — every migrating host passes through it — and there is no cluster whose token would be the right one. The agent then refuses cluster tasks and says why, which is correct: there is no cluster to act on.
func (*ProxmoxInventoryService) AgentProxmoxCredentialForHive ¶
func (s *ProxmoxInventoryService) AgentProxmoxCredentialForHive( ctx context.Context, orgID, hiveID uuid.UUID, ) (tokenID, secret, apiURL string, err error)
AgentProxmoxCredentialForHive returns the token an agent in one fleet should hold.
Named by FLEET rather than by machine because arrival needs it before the machine belongs to that fleet. A join replaces the node's token database with the target cluster's — see the note on pmxcfs above — so the credential the agent must end up with is the one for the cluster it is JOINING, at a moment when its own row still says it is fleetless.
func (*ProxmoxInventoryService) ClusterIdentity ¶
func (s *ProxmoxInventoryService) ClusterIdentity( ctx context.Context, orgID, hiveID uuid.UUID, ) (name string, clustered bool, err error)
NodeAddresses returns each cluster member's name mapped to the address the CLUSTER knows it by.
A second call, because /cluster/resources — which Inventory reads — describes capacity and does not carry node addresses. /cluster/status does, and it is the only place this system can learn how to reach a node it did not onboard.
That gap had a visible cost. The capacity sync creates a Hypervisor row for any member nobody onboarded here, with the cluster's name for it and no address, because it had none to record. Onboarding then looks a host up by address, misses that row entirely, and creates a SECOND one for the same physical machine — observed on a live four-node cluster, where pve4 and pve5 each ended up with a name-only row from the sync and an address-only row from a later declaration.
Best-effort by contract: a hive whose API is unreachable returns an error the caller is expected to tolerate. An address is an improvement to a record, not a precondition for observing capacity, and failing a whole sync because one optional field could not be filled would trade a cosmetic gap for a real one. ClusterIdentity reports the cluster name behind a hive's API URL, and whether there is a cluster there at all.
A STANDALONE Proxmox host answers /cluster/status with a single entry of type "node" and no entry of type "cluster" — so it still lists a node, still answers /cluster/resources, and to everything downstream looks like a one-node cluster. It is not one. It has no corosync configuration, nothing can join it, and `pvecm add` pointed at it fails.
That distinction was not being made, and the consequence was a closed loop: the sync recorded the standalone host as the hive's only member, which made bootstrap refuse (the host is in a hive, and the hive is not empty) and made arrival permitted (the target has a node) right up to the point where `pvecm add` was pointed at a machine that is not a cluster. Neither direction could work and nothing said why.
Reads the name off the "cluster" entry, which is the only place it appears. Empty name after a SUCCESSFUL read means standalone; an unreachable endpoint is an error and must never be mistaken for one.
func (*ProxmoxInventoryService) Inventory ¶
func (s *ProxmoxInventoryService) Inventory(ctx context.Context, orgID, hiveID uuid.UUID) ([]tenant.ResourceRow, error)
Inventory returns the hive's flat resource list, mapped into the neutral shape.
func (*ProxmoxInventoryService) NodeAddresses ¶
type QueuedEmailSender ¶
type QueuedEmailSender struct {
// contains filtered or unexported fields
}
QueuedEmailSender seals the message body and puts it on the job queue.
Sealing is not optional decoration. The body carries a live token — a confirmation link, a password reset, an invitation — and River stores job args as JSONB in the control database and keeps them after the job completes. In clear, that is a working credential sitting beside the table that deliberately stores only its SHA-256; anyone who could read the jobs table could redeem every pending link. Sealed, that table holds ciphertext.
func NewQueuedEmailSender ¶
func NewQueuedEmailSender(jobs workflow.Enqueuer, masterKey []byte, fallback EmailSender) (*QueuedEmailSender, error)
NewQueuedEmailSender derives its own key from the master key, under a purpose used for nothing else — so a queue payload cannot be opened with a tenant key and vice versa.
fallback may be nil. When set it is used only if enqueueing fails, which keeps a broken queue from silently swallowing a registration confirmation.
type RefreshInput ¶
type RefreshResult ¶
type RefreshResult struct {
Tokens *IssuedTokens
OrganizationID string
Organization string
}
type RegisterEstateInput ¶
type RegisterEstateInput struct {
OrgID uuid.UUID
Name string
Provider string
Scope map[string]string
CredentialName string
CredentialKind string
// Identity is the NON-SECRET identifying component — an access key id, a
// token id, a role ARN. It is what gets fingerprinted.
Identity string
// Secret is sealed and never fingerprinted or logged.
Secret repo.Secret
}
type RegisterInput ¶
type RegisterResult ¶
type RegisterResult struct {
User *control.User
OrganizationID string
Organization string
// VerificationSent reports whether the confirmation email was handed to the
// mailer. The response says the same thing to the caller either way.
VerificationSent bool
}
RegisterResult carries no session and no tokens, by design.
Registration used to sign the caller straight in. That made the first token_session for the new organization free — and token_sessions on distinct days are exactly what the proving sweep counts toward retiring the admission ceiling. An attacker got day one of proving for an address they had never read a single message at. Requiring a confirmed address before the first session is what makes the ceiling's premise true.
type RegistrationService ¶
type RegistrationService struct {
// contains filtered or unexported fields
}
func NewRegistrationService ¶
func NewRegistrationService( db *gorm.DB, users repo.UserRepo, policies *PolicyService, accessRules AccessRuleResolver, admission *AdmissionService, verification *EmailVerificationService, ) *RegistrationService
func (*RegistrationService) Register ¶
func (s *RegistrationService) Register(ctx context.Context, in RegisterInput) (*RegisterResult, error)
type RemoveMemberInput ¶
type ReportInput ¶
type RuleInput ¶
type RuleInput struct {
Name string
Enabled bool
Family string
Table string
Chain string
Position int
SrcCidr string
DstCidr string
// SrcSetID and DstSetID point the rule at a named list instead of a literal.
//
// The whole reason sets exist: four management addresses across two ports is
// two rules and one set, where per-rule literals make it eight — and every
// address added to the whitelist adds another two. Validate refuses a rule
// that names both a set and a literal, because nft renders one and ignores
// the other, so the stored rule and the enforced rule would differ with
// nothing reporting it.
SrcSetID *uuid.UUID
DstSetID *uuid.UUID
Protocol string
SrcPort int
SrcPortEnd int
DstPort int
DstPortEnd int
InIface string
OutIface string
Action string
Comment string
}
RuleInput is the whole rule. There is no partial update: applying half an edit — a new port against the old action — produces a rule nobody intended.
type SSHKeyService ¶
type SSHKeyService struct {
// contains filtered or unexported fields
}
SSHKeyService owns the credential the control plane uses to reach hypervisors.
The private half never leaves this process. It is sealed with a key derived per-tenant, and the only method that unseals it is PrivateKey, which exists for River workers — trusted by design, running with the same environment as the API — and for nothing else. No handler returns it, and no response schema has a field it could occupy.
func NewSSHKeyService ¶
func NewSSHKeyService(tenants TenantResolver) *SSHKeyService
func (*SSHKeyService) ActiveFleetKey ¶
func (s *SSHKeyService) ActiveFleetKey(ctx context.Context, orgID uuid.UUID) (*tenant.SshKey, error)
ActiveFleetKey returns the key an operator should whitelist, and the one a worker should authenticate with.
It refuses rather than picking arbitrarily when several are usable. Two live fleet keys is not a state to paper over: the operator has whitelisted one of them on some hosts and the other elsewhere, and choosing silently means half the fleet becomes unreachable in a way that looks like a network fault.
func (*SSHKeyService) List ¶
List returns the organization's keys. Public halves and fingerprints only — the model's private fields carry json:"-" so no caller can serialise one by accident.
func (*SSHKeyService) MintFleetKey ¶
func (s *SSHKeyService) MintFleetKey(ctx context.Context, orgID uuid.UUID, name string) (*tenant.SshKey, error)
MintFleetKey creates the standing credential for reaching hypervisors.
Unscoped and non-expiring, which is safe only because its private half stays here — see tenant.SSHKeyPurposeFleet. A join key, whose private half must be written to a hypervisor, is minted by MintJoinKey and is neither.
func (*SSHKeyService) MintJoinKey ¶
func (s *SSHKeyService) MintJoinKey( ctx context.Context, orgID uuid.UUID, hostID, taskID uuid.UUID, ttl time.Duration, ) (*tenant.SshKey, error)
MintJoinKey creates a single-use credential for one cluster join.
It exists because pvecm's SSH join path (`pvecm add --use_ssh`) still has to authenticate its FIRST step — ssh-copy-id — and normally does so with the target's root password, which nothing in this system owns and which TOTP on root@pam blocks outright. A scoped, expiring key replaces that.
Genuinely single-use: after ssh-copy-id, PVE's own /root/.ssh/id_rsa is on the target and every later step of the join authenticates with it. This key bootstraps one step and is dead weight afterwards, which is why a short TTL is not a compromise.
func (*SSHKeyService) PrivateKey ¶
PrivateKey unseals a key's private half.
This is the ONLY way to obtain it, and it exists for River workers — trusted by design, running with the same environment as the API. No handler calls it and no response type has a field it could occupy, because a fleet-wide credential passing through an HTTP response would also pass through whatever logs, caches and error trackers sit between here and a browser.
type SessionService ¶
type SessionService struct {
// contains filtered or unexported fields
}
func NewSessionService ¶
func NewSessionService( users repo.UserRepoGorm, memberships repo.MembershipRepo, organizations repo.OrganizationRepo, tokens *TokenService, sessions *TokenSessionService, policies *PolicyService, ) *SessionService
func (*SessionService) ListMyOrganizations ¶
func (s *SessionService) ListMyOrganizations( ctx context.Context, userID string, currentOrgID string, ) ([]MyOrganizationResult, error)
func (*SessionService) Refresh ¶
func (s *SessionService) Refresh(ctx context.Context, in RefreshInput) (*RefreshResult, error)
func (*SessionService) SwitchOrganization ¶
func (s *SessionService) SwitchOrganization( ctx context.Context, in SwitchOrganizationInput, ) (*SwitchOrganizationResult, error)
type SignInAlternatives ¶
type SignInAlternatives interface {
// Available reports whether an alternative exists, and names it. The name is
// for the refusal message: "configure a provider" and "you have a passkey"
// lead an operator to completely different next steps.
Available(ctx context.Context, actingUserID uuid.UUID) (bool, string, error)
}
SignInAlternatives answers whether any way into the installation survives besides the password form.
An INTERFACE so the flag service does not acquire a dependency on the auth configuration or the passkey store. The direction matters: flags are the general mechanism and sign-in is one thing they happen to gate, so the knowledge flows inward as a small question rather than the flag service learning what a provider is.
type SocialAuthInput ¶
type SocialAuthResult ¶
type SocialAuthService ¶
type SocialAuthService struct {
// contains filtered or unexported fields
}
func NewSocialAuthService ¶
func NewSocialAuthService( cfg *config.Config, socialRepo repo.SocialAuthRepo, accessRules repo.AuthAccessRuleRepo, memberships repo.MembershipRepo, totps repo.UserTOTPRepo, tokens *TokenService, bootstrap *OrgBootstrapService, sessions *TokenSessionService, policies *PolicyService, states repo.OAuthStateRepo, ) *SocialAuthService
func (*SocialAuthService) Callback ¶
func (s *SocialAuthService) Callback(ctx context.Context, providerName, code, state, stateCookie, ip, ua string) (*SocialAuthResult, error)
type SocialProviderConfig ¶
SocialProviderConfig is one provider as configured.
func (SocialProviderConfig) Configured ¶
func (p SocialProviderConfig) Configured() bool
Configured requires BOTH halves. A client id with no secret is a provider somebody started wiring up and did not finish; treating it as a working way in is how the guard would wave through the exact lockout it exists to prevent.
type SourceClassifier ¶
type SourceClassifier interface {
Classify(in SourceInput) SourceKey
}
SourceClassifier turns a client address into a ceiling bucket and a trust tier. This interface is the seam; the shipped implementation deliberately covers only what can be decided offline from operator-supplied ranges.
A production classifier wants real network intelligence. Accurate options, as far as I know them — this is not an exhaustive list and the commercial landscape moves:
- MaxMind GeoLite2-ASN: free, offline MMDB, gives ASN and AS organisation. The practical basis for datacenter-vs-ISP and for the corroboration check.
- Team Cymru IP-to-ASN: free DNS/whois service. Authoritative, but it is a network call.
- RDAP (the structured successor to WHOIS): gives the organisation a block is allocated to. bgp.he.net presents the same underlying BGP and RIR data but as a web interface rather than a documented API, so RDAP or RIPEstat is the programmatic route.
- The Tor Project's bulk exit list: free and published, for SourceAnonymised.
- Cloud providers' own published ranges (AWS ip-ranges.json, Google cloud.json, Azure service tags, and equivalents): free, authoritative, offline — the best starting point for SourceDatacenter.
- Commercial feeds (IPinfo, Spur, IPQualityScore and others) additionally flag VPNs, residential proxies and hosting.
The strongest signal is not any single lookup but CORROBORATION: an address inside the allocation of the same organisation that owns the email's registrable domain means two independent attributes agree, and an attacker would have to control both. That is the property the domain-only key was reaching for and never had. It is why Classify takes the email domain.
Do NOT put a network call on this path. An admission decision that depends on a third party's availability fails whenever they do, and this gate sits in front of registration.
type SourceInput ¶
type SourceInput struct {
ClientIP string
// ForwardedFor is the raw inbound header, present only so the interlock can
// detect a proxy this server is not configured to parse.
ForwardedFor string
// TrustedProxies reports whether TRUSTED_PROXY_CIDRS is configured, i.e.
// whether ClientIP was derived from a header this server actually trusts.
TrustedProxies bool
// RegistrableDomain is the email's eTLD+1, carried so a classifier can make
// the corroboration check described above. The shipped implementation does
// not use it; a classifier with ASN data should.
RegistrableDomain string
}
SourceInput is what the classifier reasons about.
type SourceKey ¶
type SourceKey struct {
// Key is the bucket. Empty means the source could not be established, in
// which case Trusted is false and the caller must not enforce on it.
Key string
// Trusted is false when the client address cannot be distinguished at all —
// see the interlock in Classify. The ceiling degrades to observe-only
// rather than enforce a key it knows is wrong.
Trusted bool
// Coarse records bucketing at network rather than address granularity, so a
// denial can be explained honestly: "one bucket for this whole network" is
// a very different message from "one bucket for your address".
Coarse bool
Trust SourceTrust
}
SourceKey identifies the network origin of an admission request, forming the second half of the unproven-organization ceiling's key.
Why the ceiling needs a second half at all: keyed on the registrable domain ALONE it is a denial-of-service primitive rather than a control. An attacker registers three times as a@victim-corp.com, b@ and c@ — nothing on that path checks mailbox ownership — and every real employee of that company is refused permanently, because the ceiling has no time component and nothing clears it. The general rule: a quota keyed on an attribute the attacker can assert about SOMEONE ELSE is a weapon, not a bound. Adding the source makes the budget attacker-local, so they can only exhaust their own slice.
type SourceTrust ¶
type SourceTrust int
SourceTrust ranks how much an admission decision may lean on a client address. It is a tier rather than a boolean because the addresses behave genuinely differently, and the ceiling should scale with the tier rather than treating "came from somewhere" as one fact.
Ordered least to most trusted, so a caller may compare.
const ( // SourceAnonymised is a Tor exit, open proxy or commercial anonymiser. // Addresses here are shared, disposable and deliberately unattributable, so // they carry no information about who is asking. The Tor Project publishes // its exit list; commercial feeds cover the rest. SourceAnonymised SourceTrust = iota // SourceDatacenter is cloud or hosting. Automation lives here, and — the // point that matters for the ceiling — addresses inside a provider's range // are free to an attacker. Bucketing these per address hands out unlimited // buckets and the ceiling stops bounding anything, so they are bucketed at // network granularity instead. SourceDatacenter // SourceEnterpriseEgress is a corporate VPN concentrator or a SASE/ZTNA // vendor's egress range — Zscaler, Netskope, Cloudflare WARP, Prisma and // the like. By ASN these are indistinguishable from cloud hosting, which is // why a naive tier model penalises them hardest: the organisations most // likely to mandate full-tunnel egress are the regulated ones whose staff // are the LEAST likely to be abusing anything. Treating a bank's entire // workforce as untrusted cloud traffic is precisely backwards. // // The vendors publish their egress ranges, so this is the same mechanism as // the cloud list with the opposite treatment. SourceEnterpriseEgress // SourceResidential is an ISP pool: consumer broadband, mobile carriers, // dynamic DNS. Where real people are. Note the pools are dynamic, so a // per-address bucket leaks in both directions over time — a legitimate // user's address changes, and an attacker can re-dial for a fresh one. SourceResidential // SourceCorporate is an address in a block registered to an organisation // rather than to an access provider — someone on their employer's network — // where that organisation does NOT correspond to the email's domain. SourceCorporate // SourceCorroborated is the same, except the block's registered owner DOES // correspond to the email's registrable domain. Two independent attributes // agree, and an attacker would have to control both: the mailbox at the // domain, and an address inside that organisation's own allocation. This is // the strongest signal available without verifying the mailbox itself. SourceCorroborated )
type StartImpersonationInput ¶
type SwitchOrganizationInput ¶
type SwitchOrganizationResult ¶
type SwitchOrganizationResult struct {
Tokens *IssuedTokens
OrganizationID string
Organization string
}
type TOTPSetupResult ¶
type TenantResolver ¶
type TenantResolver interface {
GetTenantContext(ctx context.Context, orgID string) (*database.TenantContext, error)
}
TenantResolver is the one thing this service needs from the tenant manager: given an organization, hand back that organization's database handle and its unwrapped tenant key.
It is declared here, on the CONSUMER side, deliberately. Depending on the concrete *database.TenantManager meant the service could not be reached without a provisioned tenant — a control-plane organization row carrying an encrypted DSN and a wrapped key — so the layer that keeps estate and credential consistent had no test at all, and the tests it could have had would have re-tested the tenant manager instead. *database.TenantManager satisfies this verbatim, so no caller changes.
type ToggleSupportInput ¶
type TokenPair ¶
type TokenPair struct {
AccessToken string
RefreshToken string
TokenType string
ExpiresIn int
// RefreshExpiresIn is the refresh token's lifetime in seconds.
//
// Sent because a client that knows only ExpiresIn cannot tell a session that
// needs refreshing from one that is over. The UI was discarding a stored
// session the moment the ACCESS token expired — fifteen minutes — while the
// refresh token it was holding stayed valid for another seven days, so
// closing a tab over lunch meant signing in again for no reason.
RefreshExpiresIn int
}
type TokenService ¶
type TokenService struct {
// contains filtered or unexported fields
}
func NewTokenService ¶
func NewTokenService(secret, issuer string, accessTTL, refreshTTL time.Duration) (*TokenService, error)
func (*TokenService) Issue ¶
func (s *TokenService) Issue(userID, orgID string, now time.Time, opts ...IssueOpts) (*IssuedTokens, error)
func (*TokenService) IssueMFAPending ¶
func (*TokenService) IssueMFAPendingWithJTI ¶
func (*TokenService) Parse ¶
func (s *TokenService) Parse(tokenStr, expectedType string) (*AuthClaims, error)
type TokenSessionService ¶
type TokenSessionService struct {
// contains filtered or unexported fields
}
func NewTokenSessionService ¶
func NewTokenSessionService( tokens *TokenService, sessions repo.TokenSessionRepo, audit repo.AuthAuditRepo, users repo.UserRepo, ) *TokenSessionService
func (*TokenSessionService) DeleteExpired ¶
func (s *TokenSessionService) DeleteExpired(ctx context.Context) (int64, error)
func (*TokenSessionService) IssueTracked ¶
func (s *TokenSessionService) IssueTracked(ctx context.Context, in IssueTrackedInput) (*IssuedTokens, error)
func (*TokenSessionService) ListActive ¶
func (s *TokenSessionService) ListActive(ctx context.Context, subjectID uuid.UUID, subjectKind string) ([]control.TokenSession, error)
func (*TokenSessionService) Refresh ¶
func (s *TokenSessionService) Refresh(ctx context.Context, refreshTokenRaw, ip, ua string, overrideOpts ...IssueOpts) (*IssuedTokens, string, error)
func (*TokenSessionService) Revoke ¶
func (s *TokenSessionService) Revoke(ctx context.Context, accessJTI, subjectID, ip, ua string) error
func (*TokenSessionService) SetAccessRules ¶
func (s *TokenSessionService) SetAccessRules(r repo.AuthAccessRuleRepo)
func (*TokenSessionService) Tokens ¶
func (s *TokenSessionService) Tokens() *TokenService
type UpdateProfileInput ¶
type UpdateRoleInput ¶
type UpdateRoleInput struct {
OrgID uuid.UUID
MembershipID uuid.UUID
NewRole control.MemberRole
ActorID uuid.UUID
ActorRole control.MemberRole
}
Source Files
¶
- account_deletion_service.go
- admission_service.go
- agent_credential.go
- api_key_service.go
- email_sender.go
- email_service.go
- email_verification_service.go
- estate_service.go
- feature_flag_service.go
- firewall_convergence.go
- firewall_service.go
- fleet_service.go
- impersonation_service.go
- invitation_service.go
- login_service.go
- membership_convergence.go
- membership_service.go
- mfa_service.go
- org_bootstrap_service.go
- passkey_service.go
- password_reset_service.go
- policy_service.go
- profile_service.go
- proxmox_inventory.go
- registration_service.go
- session_service.go
- signin_alternatives.go
- social_auth_service.go
- source.go
- ssh_key_service.go
- token_service.go
- token_session_service.go