users

package module
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 16, 2026 License: MIT Imports: 14 Imported by: 0

README

kite-mcp-users

Go Reference

User identity store for the algo2go ecosystem. Provides user CRUD, role-based access control (admin/trader/viewer), bcrypt-hashed password storage, TOTP-based MFA enrollment (admin-only), and invitation tokens. SQLite-backed via the algo2go/kite-mcp-alerts shared DB.

Used by Sundeepg98/kite-mcp-server for admin login, role gating, MFA enrollment, and invitation flows.

Why a separate module?

User identity + RBAC + MFA are foundational primitives for any algo2go consumer that needs admin/trader/viewer separation. Hosting as a module:

  • Centralizes the user store + RBAC contract across consumers
  • Lets MFA + bcrypt + TOTP signatures version independently
  • Pairs cleanly with algo2go/kite-mcp-alerts (shared DB) and upstream consumers needing admin gating

Stability promise

v0.x — unstable. Type signatures may evolve as RBAC + MFA patterns mature. Pin v0.1.0 deliberately. v1.0 ships only after the public API (Store, Role/Status constants, MFA enrollment, TOTP helpers, invitation tokens) is reviewed for stability.

Install

go get github.com/algo2go/kite-mcp-users@v0.1.0

Public API (selected)

Store
  • Store — user CRUD with *alerts.DB backend
  • NewStore(db *alerts.DB) *Store — constructor
  • Store.Create / Read / Update / Delete / List — RBAC-aware CRUD
Role + status constants
  • RoleAdmin, RoleTrader, RoleViewer
  • StatusActive, StatusSuspended, StatusOffboarded
MFA (admin-only)
  • Store.SetEncryptionKey(key []byte) — wires AES-256 key for TOTP secret encryption at rest
  • Store.EnrollMFA / VerifyMFA / DisableMFA — admin TOTP lifecycle
  • ProvisioningURI(secret, issuer, account) string — RFC 6238 URI
  • VerifyTOTP(secret, code) bool — TOTP code verification
Invitations
  • Store.CreateInvitation / RedeemInvitation — token-based onboarding

Dependencies

  • github.com/algo2go/kite-mcp-alerts — shared DB backend
  • github.com/stretchr/testify — assertions
  • golang.org/x/crypto/bcrypt — password hashing

All algo2go deps are published modules; no upstream replace directives needed.

Reference consumer

Sundeepg98/kite-mcp-server — consumed by:

  • app/wire.go, app/app.go — service wiring + admin route gating
  • oauth/handlers_admin_mfa.go — admin MFA enrollment flow
  • kc/manager_init.go, kc/store_registry.go — service registry
  • kc/usecases/admin_usecases.go, family_usecases.go — use cases
  • kc/ops/admin/render.go — admin dashboard rendering
  • mcp/admin_tools_test.go — admin MCP tool tests
  • plugins/rolegate/plugin.go — RBAC viewer-blocks-write plugin
  • plugins/telegramnotify/plugin.go — family-admin DM after-hook

License

MIT — see LICENSE.

Authors

Original design: Sundeepg98 (Zerodha Tech). Multi-module promotion (2026-05-10): algo2go contributors.

Documentation

Overview

Package users — MFA (TOTP) enrollment storage on the User store.

This file extends Store with TOTP-based MFA. It treats the TOTP secret as T1 data (per docs/data-classification.md): AES-256-GCM encrypted at rest using the same HKDF-derived key the rest of T1 storage uses.

Scope: ADMIN ROLE ONLY in this slice. Per the user-direction in the brief ("Out of scope: Non-admin user MFA"), enrollment for non-admins is rejected at this layer rather than in the HTTP handler — defence in depth so a misconfigured route can't sidestep the gate.

All methods are thread-safe and follow the same lock-then-snapshot- then-DB pattern the password / role helpers in store.go use.

Package users — TOTP (Time-based One-Time Password) per RFC 6238.

Pure Go implementation built on HOTP (RFC 4226) with a 30-second time step and SHA-1 HMAC. SHA-1 is the spec-mandated default for TOTP and is used by every authenticator app (Google Authenticator, Authy, 1Password, etc.). The keyspace lives outside SHA-1's collision-relevant input domain — TOTP uses HMAC-SHA1, not bare SHA-1, so the SHA-1 collision history does not degrade the HOTP/TOTP security argument (NIST SP 800-63B §5.1.4 / RFC 6238 §5.1).

Validation accepts a ±1 step skew window (the call site can override) to tolerate clock drift between the authenticator app and the server. The 30-second step gives a 90-second total accept window with skew=1.

Provisioning URI follows the otpauth:// format documented at https://github.com/google/google-authenticator/wiki/Key-Uri-Format and is consumed unchanged by every popular authenticator app.

Index

Constants

View Source
const (
	RoleAdmin  = "admin"
	RoleTrader = "trader"
	RoleViewer = "viewer"
)

Role constants for user access control.

View Source
const (
	StatusActive     = "active"
	StatusSuspended  = "suspended"
	StatusOffboarded = "offboarded"
)

Status constants for user lifecycle.

View Source
const (
	// TOTPDigits is the number of digits in a TOTP code. RFC 6238 §5.3 specifies 6.
	TOTPDigits = 6
	// TOTPPeriodSeconds is the time step in seconds. RFC 6238 §5.2 default is 30.
	TOTPPeriodSeconds = 30
	// TOTPSecretBytes is the length of a freshly generated TOTP secret in bytes.
	// 20 bytes = 160 bits — RFC 4226 §4 minimum is 128 bits; 160 is the SHA-1
	// block-aligned value used by Google Authenticator.
	TOTPSecretBytes = 20
	// TOTPSkewSteps is the default ± window of accepted time steps.
	// 1 step = ±30s = 90s total accept window. Tolerates routine clock drift.
	TOTPSkewSteps = 1
)

TOTP parameters per RFC 6238 — the values every authenticator app expects.

Variables

This section is empty.

Functions

func GenerateTOTPCode

func GenerateTOTPCode(secret string, t time.Time) (string, error)

GenerateTOTPCode returns the TOTP code for the given secret at time t. The secret must be base32-encoded (the form returned by GenerateTOTPSecret and stored in the database). Empty / invalid secrets return an error rather than a zero code so call sites cannot silently miss enrollment.

func GenerateTOTPSecret

func GenerateTOTPSecret() (string, error)

GenerateTOTPSecret produces a fresh 160-bit secret suitable for TOTP enrollment. The returned string is the base32-encoded, no-padding form that authenticator apps expect.

func ProvisioningURI

func ProvisioningURI(secret, issuer, account string) string

ProvisioningURI returns an otpauth:// URI for QR-code enrollment. issuer is the project name shown in the authenticator app; account is usually the user's email. The URI format is the de-facto standard documented at https://github.com/google/google-authenticator/wiki/Key-Uri-Format — every popular authenticator app (Google Authenticator, Authy, 1Password, Microsoft Authenticator, Bitwarden) consumes it unchanged.

func VerifyTOTPCode

func VerifyTOTPCode(secret, code string, t time.Time, skewSteps int) bool

VerifyTOTPCode returns true if the supplied code matches the TOTP for the given secret at time t, allowing ± skewSteps of clock drift. The comparison is constant-time. An empty code or empty secret returns false without erroring, so call sites can use this as a single-line gate.

Types

type FamilyInvitation

type FamilyInvitation struct {
	ID           string    `json:"id"`
	AdminEmail   string    `json:"admin_email"`
	InvitedEmail string    `json:"invited_email"`
	Status       string    `json:"status"` // pending, accepted, expired, revoked
	CreatedAt    time.Time `json:"created_at"`
	ExpiresAt    time.Time `json:"expires_at"`
	AcceptedAt   time.Time `json:"accepted_at,omitempty"`
}

type InvitationStore

type InvitationStore struct {
	// contains filtered or unexported fields
}

func NewInvitationStore

func NewInvitationStore(db *alerts.DB) *InvitationStore

func (*InvitationStore) Accept

func (s *InvitationStore) Accept(id string) error

func (*InvitationStore) CleanupExpired

func (s *InvitationStore) CleanupExpired() int

CleanupExpired marks all pending invitations past their expiry as expired. Call periodically (e.g., daily).

func (*InvitationStore) Create

func (s *InvitationStore) Create(inv *FamilyInvitation) error

func (*InvitationStore) Get

func (*InvitationStore) GetByInvitedEmail

func (s *InvitationStore) GetByInvitedEmail(email string) *FamilyInvitation

func (*InvitationStore) InitTable

func (s *InvitationStore) InitTable() error

func (*InvitationStore) ListByAdmin

func (s *InvitationStore) ListByAdmin(adminEmail string) []*FamilyInvitation

func (*InvitationStore) LoadFromDB

func (s *InvitationStore) LoadFromDB() error

func (*InvitationStore) Revoke

func (s *InvitationStore) Revoke(id string) error

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is a thread-safe in-memory user store backed by SQLite.

func NewStore

func NewStore() *Store

NewStore creates a new user store.

func (*Store) ClearTOTPSecret

func (s *Store) ClearTOTPSecret(email string) error

ClearTOTPSecret removes a user's TOTP enrollment. Used by an admin recovery flow (lost-device) and by the offboarding path. Returns error if the user is not found.

func (*Store) Count

func (s *Store) Count() int

Count returns the number of registered users.

func (*Store) Create

func (s *Store) Create(u *User) error

Create inserts a new user into the store. Returns error if user already exists.

func (*Store) Delete

func (s *Store) Delete(email string)

Delete removes a user from the store.

func (*Store) EnsureAdmin

func (s *Store) EnsureAdmin(email string)

EnsureAdmin creates or updates a user to have admin role. Used to seed admin users from ADMIN_EMAILS env var at startup.

func (*Store) EnsureGoogleUser

func (s *Store) EnsureGoogleUser(email string)

EnsureGoogleUser auto-creates a trader account on first Google SSO login. Existing users are left unchanged (admins keep their admin role).

func (*Store) EnsureUser

func (s *Store) EnsureUser(email, kiteUID, displayName, onboardedBy string) *User

EnsureUser creates a user if they don't exist, returning the user. Used for auto-provisioning on first OAuth login.

func (*Store) Exists

func (s *Store) Exists(email string) bool

Exists returns true if a user with the given email exists.

func (*Store) Get

func (s *Store) Get(email string) (*User, bool)

Get retrieves a user by email. Returns a copy and ok=true if found.

func (*Store) GetByEmail

func (s *Store) GetByEmail(email string) (*User, bool)

GetByEmail is an alias for Get (for interface clarity).

func (*Store) GetRole

func (s *Store) GetRole(email string) string

GetRole returns the user's role. Returns empty string if user not found.

func (*Store) GetStatus

func (s *Store) GetStatus(email string) string

GetStatus returns the user's status. Returns empty string if user not found.

func (*Store) GetTOTPSecret

func (s *Store) GetTOTPSecret(email string) (string, bool)

GetTOTPSecret returns the decrypted TOTP secret for the given user. Returns ("", false) if the user is not enrolled or unknown, or if decryption fails (likely key rotation drift). The bool reports "successfully retrieved a usable secret"; callers MUST treat false as "not enrolled" and not as an error condition.

func (*Store) HasPassword

func (s *Store) HasPassword(email string) bool

HasPassword returns true if the given user has a non-empty password hash. Delegates to User.HasPassword() for the actual check.

func (*Store) HasTOTP

func (s *Store) HasTOTP(email string) bool

HasTOTP returns true if the user has an enrolled TOTP secret. Unknown users return false (no error path) so the call site can use this as a single-line gate.

func (*Store) InitTable

func (s *Store) InitTable() error

InitTable creates the users table if it does not exist.

func (*Store) IsAdmin

func (s *Store) IsAdmin(email string) bool

IsAdmin returns true if the given email belongs to an admin user. Delegates to User.IsAdmin() for the actual check.

func (*Store) List

func (s *Store) List() []*User

List returns all users as a slice (copies).

func (*Store) ListByAdminEmail

func (s *Store) ListByAdminEmail(adminEmail string) []*User

ListByAdminEmail returns all users linked to this admin.

func (*Store) LoadFromDB

func (s *Store) LoadFromDB() error

LoadFromDB populates the in-memory store from the database.

func (*Store) SetAdminEmail

func (s *Store) SetAdminEmail(email, adminEmail string) error

SetAdminEmail links a user to their admin for family billing tier inheritance.

func (*Store) SetDB

func (s *Store) SetDB(db *alerts.DB)

SetDB enables write-through persistence to the given SQLite database.

func (*Store) SetEncryptionKey

func (s *Store) SetEncryptionKey(key []byte)

SetEncryptionKey wires the AES-256 key used to encrypt TOTP secrets at rest. The caller (composition root in app/wire.go) is expected to supply the same HKDF-derived key the rest of the alerts.DB uses, so rotation via the existing migrateEncryptedData path stays one operation.

Calling SetTOTPSecret without first calling SetEncryptionKey is a programming error and returns an explicit error rather than silently storing plaintext — silent plaintext storage would be a T1 protection regression, hence fail-closed.

func (*Store) SetLogger

func (s *Store) SetLogger(logger *slog.Logger)

SetLogger sets the logger for DB error reporting.

func (*Store) SetPasswordHash

func (s *Store) SetPasswordHash(email, hash string) error

SetPasswordHash stores a bcrypt password hash for the given user.

func (*Store) SetTOTPSecret

func (s *Store) SetTOTPSecret(email, plaintextSecret string) error

SetTOTPSecret encrypts and persists a TOTP secret for the given user. Returns error in three cases:

  • encryption key not configured (store-level misconfig)
  • user not found (call site bug — enroll route should always pre-check)
  • user is not an admin (slice-1 invariant: MFA is admin-only here)

func (*Store) TOTPEnrolledAt

func (s *Store) TOTPEnrolledAt(email string) (time.Time, bool)

TOTPEnrolledAt returns the enrollment timestamp for the user. Returns (zero, false) if the user is not enrolled or unknown.

func (*Store) UpdateKiteUID

func (s *Store) UpdateKiteUID(email, kiteUID string)

UpdateKiteUID sets the Kite user ID for a user.

func (*Store) UpdateLastLogin

func (s *Store) UpdateLastLogin(email string)

UpdateLastLogin records the current time as the user's last login.

func (*Store) UpdateRole

func (s *Store) UpdateRole(email, role string) error

UpdateRole changes the user's role. Returns error if user not found or invalid role.

func (*Store) UpdateStatus

func (s *Store) UpdateStatus(email, status string) error

UpdateStatus changes the user's status. Returns error if user not found or invalid status.

func (*Store) VerifyPassword

func (s *Store) VerifyPassword(email, password string) (bool, error)

VerifyPassword checks the given plaintext password against the stored bcrypt hash. Returns (true, nil) on match, (false, nil) on mismatch, (false, error) on lookup failure. For timing safety, always runs bcrypt comparison even for unknown users.

func (*Store) VerifyTOTP

func (s *Store) VerifyTOTP(email, code string) (bool, error)

VerifyTOTP returns (true, nil) if the supplied 6-digit code matches the user's stored TOTP secret within the default skew window. Returns (false, nil) for any non-match (wrong code, not enrolled, unknown user). Returns a non-nil error only on infrastructure failure (encryption key missing). Callers should route a not-enrolled user to enrollment, not treat the false as authentication failure.

type User

type User struct {
	ID           string    `json:"id"`
	Email        string    `json:"email"`
	KiteUID      string    `json:"kite_uid,omitempty"`
	DisplayName  string    `json:"display_name,omitempty"`
	Role         string    `json:"role"`
	Status       string    `json:"status"`
	PasswordHash string    `json:"-"` // bcrypt hash, never serialized
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
	LastLogin    time.Time `json:"last_login,omitempty"`
	OnboardedBy  string    `json:"onboarded_by"`
	AdminEmail   string    `json:"admin_email,omitempty"`
	// TOTPSecretEnc is the AES-256-GCM-encrypted TOTP secret (RFC 6238)
	// for admin MFA. Empty when not enrolled. Never serialised to JSON
	// — the MFA enrollment endpoint returns the plaintext exactly once
	// at enrollment time, never afterwards. See kc/users/mfa.go.
	TOTPSecretEnc  string    `json:"-"`
	TOTPEnrolledAt time.Time `json:"totp_enrolled_at,omitempty"`
}

User represents a registered user of the system.

func (*User) BelongsToAdmin

func (u *User) BelongsToAdmin(adminEmail string) bool

BelongsToAdmin returns true if the user is linked to the given admin email as a family member.

func (*User) CanBeOnboardedByGoogleSSO

func (u *User) CanBeOnboardedByGoogleSSO() bool

CanBeOnboardedByGoogleSSO returns true if the user was auto-provisioned via Google SSO (vs. invited or seeded from env).

func (*User) CanTrade

func (u *User) CanTrade() bool

CanTrade returns true if this user is allowed to place trades (not a viewer, and active).

func (*User) HasPassword

func (u *User) HasPassword() bool

HasPassword returns true if this user has a non-empty password hash.

func (*User) IsActive

func (u *User) IsActive() bool

IsActive returns true if this user's status is active.

func (*User) IsAdmin

func (u *User) IsAdmin() bool

IsAdmin returns true if this user has the admin role and is active.

func (*User) IsBillingOwner

func (u *User) IsBillingOwner() bool

IsBillingOwner returns true if the user is their own billing owner (either an admin or a self-onboarded user with no parent admin).

func (*User) IsFamilyMember

func (u *User) IsFamilyMember() bool

IsFamilyMember returns true if the user belongs to a family billing group (has an admin email set) and is therefore not the billing owner.

func (*User) IsOffboarded

func (u *User) IsOffboarded() bool

IsOffboarded returns true if the user's account has been offboarded.

func (*User) IsSuspended

func (u *User) IsSuspended() bool

IsSuspended returns true if the user's account is suspended.

func (*User) IsTrader

func (u *User) IsTrader() bool

IsTrader returns true if the user has the trader role (not admin, not viewer).

func (*User) IsViewer

func (u *User) IsViewer() bool

IsViewer returns true if the user is restricted to read-only access.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL