Documentation
¶
Overview ¶
Package iam is ai's INTERNAL IAM client: the small, clean OIDC+REST surface ai needs to talk to Hanzo IAM (hanzo.id), decoupled from the retired SDK module github.com/hanzoai/iam-v1.
It is NOT a published SDK. It exists so ai links only against standard OIDC (JWKS token verification) and the IAM server's JSON REST API (/v1/iam/...), instead of pulling in the whole iam-v1 server module (beego, xorm, ldap, aliyun/aws SDKs, …) that it dragged into ai's build for code ai never ran.
The wire types (User, Claims, Permission, Resource, …) mirror the IAM server's JSON models so requests round-trip losslessly — in particular the full User field set with its json tags is reproduced verbatim, because object/redact.go reflects over every User field by json tag as a fail-secure secret-redaction control. Slimming User would silently weaken that control and break its test, so fidelity is required, not optional.
Endpoint resolution: callers configure a Client explicitly via InitConfig / NewClient (ai's account bootstrap does), and the package-level helpers fall back to a lazily-built client from IAM_ENDPOINT / IAM_ISSUER (default https://hanzo.id) so a read never nil-panics when InitConfig was skipped.
Index ¶
- Constants
- func AddPermission(permission *Permission) (bool, error)
- func DeletePermission(permission *Permission) (bool, error)
- func DeleteResourceWithTag(resource *Resource, tag string) (bool, error)
- func GetOAuthToken(code, state string, opts ...OAuthOption) (*oauth2.Token, error)
- func InitConfig(endpoint, clientId, clientSecret, certificate, organizationName, ... string)
- func SendEmail(title, content, sender string, receivers ...string) error
- func SetHttpClient(httpClient HttpClient)
- func UpdatePermission(permission *Permission) (bool, error)
- func UpdateUserForColumns(user *User, columns []string) (bool, error)
- func UploadResource(user, tag, parent, fullFilePath string, fileBytes []byte) (string, string, error)
- type Address
- type Application
- type AuthConfig
- type Cert
- type Claims
- type Client
- func (c *Client) AddPermission(permission *Permission) (bool, error)
- func (c *Client) DeletePermission(permission *Permission) (bool, error)
- func (c *Client) DeleteResourceWithTag(resource *Resource, tag string) (bool, error)
- func (c *Client) DoGetBytes(url string) ([]byte, error)
- func (c *Client) DoGetResponse(url string) (*Response, error)
- func (c *Client) DoPost(action string, queryMap map[string]string, postBytes []byte, ...) (*Response, error)
- func (c *Client) GetApplication(name string) (*Application, error)
- func (c *Client) GetCert(name string) (*Cert, error)
- func (c *Client) GetId(name string) string
- func (c *Client) GetOAuthToken(code, state string, opts ...OAuthOption) (*oauth2.Token, error)
- func (c *Client) GetOrganization(name string) (*Organization, error)
- func (c *Client) GetPermission(name string) (*Permission, error)
- func (c *Client) GetPermissions() ([]*Permission, error)
- func (c *Client) GetProviders() ([]*Provider, error)
- func (c *Client) GetResources(owner, user, field, value, sortField, sortOrder string) ([]*Resource, error)
- func (c *Client) GetUrl(action string, queryMap map[string]string) string
- func (c *Client) GetUser(name string) (*User, error)
- func (c *Client) GetUsers() ([]*User, error)
- func (c *Client) ParseJwtToken(token string) (*Claims, error)
- func (c *Client) SendEmail(title, content, sender string, receivers ...string) error
- func (c *Client) UpdatePermission(permission *Permission) (bool, error)
- func (c *Client) UpdateUserForColumns(user *User, columns []string) (bool, error)
- func (c *Client) UploadResource(user, tag, parent, fullFilePath string, fileBytes []byte) (string, string, error)
- type FaceId
- type HttpClient
- type ManagedAccount
- type MfaAccount
- type MfaItem
- type MfaProps
- type OAuthOption
- type Organization
- type Permission
- type ProductInfo
- type Provider
- type Resource
- type Response
- type Role
- type Status
- type User
Constants ¶
const PlatformOwner = "admin"
GetApplication fetches an application by name (owned by admin). PlatformOwner is the reserved org that holds cross-tenant configuration: applications, organizations and the signing certs applications point at. It is not a tenant and nothing customer-facing lives in it.
It is a constant, and named, because these reads MUST agree. An application addressed in one partition and its certificate in another resolves an app and then fails to resolve the key that app's tokens are signed with — which reads as a missing cert rather than as a mis-addressed one.
Variables ¶
This section is empty.
Functions ¶
func AddPermission ¶
func AddPermission(permission *Permission) (bool, error)
func DeletePermission ¶
func DeletePermission(permission *Permission) (bool, error)
func DeleteResourceWithTag ¶
func GetOAuthToken ¶
func GetOAuthToken(code, state string, opts ...OAuthOption) (*oauth2.Token, error)
GetOAuthToken exchanges a code using the configured (or env-derived) client.
func InitConfig ¶
func InitConfig(endpoint, clientId, clientSecret, certificate, organizationName, applicationName string)
InitConfig sets the package-global Client used by the package-level helpers (GetUser, ParseJwtToken, …). ai's account bootstrap calls this with the deployed IAM endpoint and app credentials.
func SetHttpClient ¶
func SetHttpClient(httpClient HttpClient)
SetHttpClient overrides the shared http client (tests).
func UpdatePermission ¶
func UpdatePermission(permission *Permission) (bool, error)
Types ¶
type Address ¶
type Address struct {
Tag string `json:"tag"`
Line1 string `json:"line1"`
Line2 string `json:"line2"`
City string `json:"city"`
State string `json:"state"`
ZipCode string `json:"zipCode"`
Region string `json:"region"`
}
Address is one postal address on a User.
type Application ¶
type Application struct {
Owner string `json:"owner"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
Cert string `json:"cert"`
RedirectUris []string `json:"redirectUris"`
}
Application is the subset of the IAM Application model ai reads: DisplayName, the signing cert name, and the OAuth redirect allowlist. Read-only response (GetApplication), so a projection is correct.
func GetApplication ¶
func GetApplication(name string) (*Application, error)
GetApplication uses the configured (or env-derived) client.
type AuthConfig ¶
type AuthConfig struct {
Endpoint string
ClientId string
ClientSecret string
Certificate string
OrganizationName string
ApplicationName string
}
AuthConfig is the connection config for a Client: the IAM server endpoint and the app credentials/context requests are made under.
type Cert ¶
type Cert struct {
Owner string `json:"owner"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
Scope string `json:"scope"`
Type string `json:"type"`
CryptoAlgorithm string `json:"cryptoAlgorithm"`
Certificate string `json:"certificate"`
}
Cert is the subset of the IAM Cert model ai reads. ai consumes the PEM in Certificate (to verify token signatures when JWKS is unavailable).
type Claims ¶
type Claims struct {
User
AccessToken string `json:"accessToken"`
jwt.RegisteredClaims
TokenType string `json:"tokenType"`
RefreshTokenType string `json:"TokenType"`
SigninMethod string `json:"signinMethod"`
// Orgs is the signed membership set. The type is account's, not a local
// re-declaration: the JSON tags are a wire contract with IAM, and a copy of
// them here is a copy that can drift one field at a time. If it did, every
// membership would decode empty and every org switch would fail closed to
// home — presenting as "the switcher does nothing".
Orgs []account.OrgRef `json:"orgs,omitempty"`
}
Claims is the verified access-token claim set. It embeds the User (so a token carries the subject's profile) plus the standard registered claims and the Hanzo-specific token/org/billing claims. Shape matches the IAM server so the /get-account response promotes every field identically.
func ParseJwtToken ¶
ParseJwtToken verifies a JWT's signature against the IAM server's published JWKS (proper OIDC) and returns its claims. RS256/RS512/ES256/ES512 only.
type Client ¶
type Client struct {
AuthConfig
CustomHeaders map[string]string
}
Client talks to a Hanzo IAM server over its /v1/iam/ JSON REST API and verifies its tokens via published JWKS. It is the one place the endpoint and app credentials live.
func NewClient ¶
func NewClient(endpoint, clientId, clientSecret, certificate, organizationName, applicationName string) *Client
NewClient builds a Client for an explicit endpoint + app context.
func NewClientWithConf ¶
func NewClientWithConf(config *AuthConfig) *Client
NewClientWithConf builds a Client from an AuthConfig.
func (*Client) AddPermission ¶
func (c *Client) AddPermission(permission *Permission) (bool, error)
func (*Client) DeletePermission ¶
func (c *Client) DeletePermission(permission *Permission) (bool, error)
func (*Client) DeleteResourceWithTag ¶
DeleteResourceWithTag deletes a resource, defaulting its owner to the client's organization.
func (*Client) DoGetBytes ¶
DoGetBytes GETs url and returns the envelope's data field re-marshaled to JSON bytes, ready to unmarshal into a typed value.
func (*Client) DoGetResponse ¶
DoGetResponse GETs url and returns the decoded IAM envelope, erroring on a non-"ok" status.
func (*Client) DoPost ¶
func (c *Client) DoPost(action string, queryMap map[string]string, postBytes []byte, isForm, isFile bool) (*Response, error)
DoPost posts to /v1/iam/<action>. isForm/isFile select multipart file upload, multipart form fields, or a raw text/plain body.
func (*Client) GetApplication ¶
func (c *Client) GetApplication(name string) (*Application, error)
func (*Client) GetCert ¶
GetCert fetches a signing certificate by name from the PLATFORM partition, which is where certs live — the same partition GetApplication reads from.
It used to qualify the id with c.OrganizationName, the caller's own tenant. That is the wrong owner and it was silent: the application read resolves admin/<app>, the application's `cert` field is a bare name, and the cert row is written owner=admin — so a deployment whose IAM_ORG was anything but "admin" asked for <tenant>/<cert>, got "the entity does not exist", and could not establish the key every bearer token is validated against. The store had the cert the whole time; only the question was addressed to the wrong tenant.
Both reads now name the partition through one constant, so an application and its own certificate can no longer be looked up in two different places.
func (*Client) GetOAuthToken ¶
GetOAuthToken exchanges an authorization code for a token against the IAM server's OAuth endpoints.
func (*Client) GetOrganization ¶
func (c *Client) GetOrganization(name string) (*Organization, error)
GetOrganization fetches an organization by name.
func (*Client) GetPermission ¶
func (c *Client) GetPermission(name string) (*Permission, error)
GetPermission fetches a permission by name within the client's organization.
func (*Client) GetPermissions ¶
func (c *Client) GetPermissions() ([]*Permission, error)
GetPermissions lists all permissions in the client's organization.
func (*Client) GetProviders ¶
GetProviders lists all providers in the client's organization.
func (*Client) GetResources ¶
func (c *Client) GetResources(owner, user, field, value, sortField, sortOrder string) ([]*Resource, error)
GetResources lists stored resources matching the given filter.
func (*Client) GetUrl ¶
GetUrl builds a /v1/iam/<action> URL with the given query params. Hanzo IAM serves its JSON API under /v1/iam/ only (the iam-v1 /api/ prefix is retired).
func (*Client) ParseJwtToken ¶
ParseJwtToken verifies token against this client's IAM endpoint JWKS, falling back to a configured certificate PEM only when JWKS is unreachable.
func (*Client) SendEmail ¶
SendEmail sends an email via IAM's configured mail provider (/v1/iam/send-email). This proxies mail through IAM exactly as ai did via the old SDK; it is not identity, but keeping the REST call preserves behavior with no new coupling.
func (*Client) UpdatePermission ¶
func (c *Client) UpdatePermission(permission *Permission) (bool, error)
func (*Client) UpdateUserForColumns ¶
UpdateUserForColumns updates only the named columns of user.
type FaceId ¶
type FaceId struct {
Name string `json:"name"`
FaceIdData []float64 `json:"faceIdData"`
ImageUrl string `json:"ImageUrl"`
}
FaceId is enrolled biometric face data.
type HttpClient ¶
HttpClient is the minimal http doer a Client uses; *http.Client satisfies it.
type ManagedAccount ¶
type ManagedAccount struct {
Application string `json:"application"`
Username string `json:"username"`
Password string `json:"password"`
SigninUrl string `json:"signinUrl"`
}
ManagedAccount is a linked downstream account (carries a password).
type MfaAccount ¶
type MfaAccount struct {
AccountName string `json:"accountName"`
Issuer string `json:"issuer"`
SecretKey string `json:"secretKey"`
Origin string `json:"origin"`
}
MfaAccount is a TOTP account entry (carries a secret key).
type MfaProps ¶
type MfaProps struct {
Enabled bool `json:"enabled"`
IsPreferred bool `json:"isPreferred"`
MfaType string `json:"mfaType"`
Secret string `json:"secret,omitempty"`
CountryCode string `json:"countryCode,omitempty"`
URL string `json:"url,omitempty"`
RecoveryCodes []string `json:"recoveryCodes,omitempty"`
}
MfaProps is one enrolled multi-factor method. Secret/RecoveryCodes are credentials and are cleared by object/redact.go before a user is returned.
type OAuthOption ¶
type OAuthOption func(*oauthOptions)
OAuthOption configures an OAuth request.
func WithHTTPClient ¶
func WithHTTPClient(httpClient *http.Client) OAuthOption
WithHTTPClient sets a custom http client for the OAuth exchange.
type Organization ¶
type Organization struct {
Owner string `json:"owner"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
}
Organization is the subset of the IAM Organization model ai reads. It is a read-only response (GetOrganization), so a projection is correct: json unmarshal ignores the fields ai does not consume.
func GetOrganization ¶
func GetOrganization(name string) (*Organization, error)
GetOrganization uses the configured (or env-derived) client.
type Permission ¶
type Permission struct {
Owner string `json:"owner"`
Name string `json:"name"`
CreatedTime string `json:"createdTime"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Users []string `json:"users"`
Groups []string `json:"groups"`
Roles []string `json:"roles"`
Domains []string `json:"domains"`
Model string `json:"model"`
Adapter string `json:"adapter"`
ResourceType string `json:"resourceType"`
Resources []string `json:"resources"`
Actions []string `json:"actions"`
Effect string `json:"effect"`
IsEnabled bool `json:"isEnabled"`
Submitter string `json:"submitter"`
Approver string `json:"approver"`
ApproveTime string `json:"approveTime"`
State string `json:"state"`
}
Permission mirrors the IAM server's Permission JSON model. ai marshals a client-supplied permission into this type and posts it back on add/update/delete, so the full field set is kept for a lossless round-trip (a slim struct would drop unknown fields and corrupt the stored permission).
func GetPermission ¶
func GetPermission(name string) (*Permission, error)
func GetPermissions ¶
func GetPermissions() ([]*Permission, error)
type ProductInfo ¶
type ProductInfo struct {
Owner string `json:"owner"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
Image string `json:"image,omitempty"`
Detail string `json:"detail,omitempty"`
Price float64 `json:"price"`
Currency string `json:"currency,omitempty"`
IsRecharge bool `json:"isRecharge,omitempty"`
Quantity int `json:"quantity,omitempty"`
PricingName string `json:"pricingName,omitempty"`
PlanName string `json:"planName,omitempty"`
}
ProductInfo is one line item on a User's cart.
type Provider ¶
type Provider struct {
Owner string `json:"owner"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
Category string `json:"category"`
Type string `json:"type"`
}
Provider is the subset of the IAM Provider model ai reads. ai filters the provider list by Category (e.g. "Storage"). Read-only response (GetProviders), so a projection is correct.
func GetProviders ¶
GetProviders uses the configured (or env-derived) client.
type Resource ¶
type Resource struct {
Owner string `json:"owner"`
Name string `json:"name"`
CreatedTime string `json:"createdTime"`
User string `json:"user"`
Provider string `json:"provider"`
Application string `json:"application"`
Tag string `json:"tag"`
Parent string `json:"parent"`
FileName string `json:"fileName"`
FileType string `json:"fileType"`
FileFormat string `json:"fileFormat"`
FileSize int `json:"fileSize"`
Url string `json:"url"`
Description string `json:"description"`
}
Resource mirrors the IAM Resource JSON model (a stored file record). ai constructs one client-side to delete by tag, and reads Name/CreatedTime/ FileSize/Url off the list response, so the full field set is kept.
NOTE: these resource calls proxy file storage through IAM's /v1/iam/ upload/list/delete endpoints — the same behavior ai had via the old SDK. Re-homing file storage to S3 directly is a possible future cleanup, but is out of scope for decoupling from the retired iam-v1 module: keeping the REST calls preserves behavior exactly.
func GetResources ¶
type Response ¶
type Response struct {
Status Status `json:"status"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
Data2 interface{} `json:"data2"`
}
Response is the IAM JSON envelope: {status, msg, data, data2}.
type Role ¶
type Role struct {
Owner string `json:"owner"`
Name string `json:"name"`
CreatedTime string `json:"createdTime"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
Users []string `json:"users"`
Groups []string `json:"groups"`
Roles []string `json:"roles"`
Domains []string `json:"domains"`
IsEnabled bool `json:"isEnabled"`
}
Role is an IAM role reference.
type Status ¶ added in v1.831.9
type Status string
Status is the IAM envelope's status field.
The server has emitted it BOTH as a string ("ok") and — since iam v1.33.x — as a JSON number (200). A plain `string` field fails the ENTIRE decode on the numeric form, and the one caller that matters treats a decode failure as "IAM unreachable" and continues with auth features switched off. The pod still passes its probes, so nothing crashes and nothing reverts: the binary just serves 401 to every authenticated call. One field's wire drift silently disarms authentication.
Accepting both shapes removes that failure mode at the only place it can occur.
func (*Status) UnmarshalJSON ¶ added in v1.831.9
UnmarshalJSON accepts the string form, the numeric form, or null, and normalizes to the string the callers compare against. A 2xx number IS the success code, so it normalizes to "ok"; any other number keeps its digits so a real error still reads as one rather than being laundered into success. Unknown shapes are an error, not a guess — silently accepting them is what caused this.
type User ¶
type User struct {
Owner string `json:"owner"`
Name string `json:"name"`
CreatedTime string `json:"createdTime"`
UpdatedTime string `json:"updatedTime"`
DeletedTime string `json:"deletedTime"`
Id string `json:"id"`
ExternalId string `json:"externalId"`
Type string `json:"type"`
Password string `json:"password"`
PasswordSalt string `json:"passwordSalt"`
PasswordType string `json:"passwordType"`
DisplayName string `json:"displayName"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Avatar string `json:"avatar"`
AvatarType string `json:"avatarType"`
PermanentAvatar string `json:"permanentAvatar"`
Email string `json:"email"`
EmailVerified bool `json:"emailVerified"`
Phone string `json:"phone"`
CountryCode string `json:"countryCode"`
Region string `json:"region"`
Location string `json:"location"`
Address []string `json:"address"`
Addresses []*Address `json:"addresses"`
Affiliation string `json:"affiliation"`
Title string `json:"title"`
IdCardType string `json:"idCardType"`
IdCard string `json:"idCard"`
RealName string `json:"realName"`
IsVerified bool `json:"isVerified"`
Homepage string `json:"homepage"`
Bio string `json:"bio"`
Tag string `json:"tag"`
Language string `json:"language"`
Gender string `json:"gender"`
Birthday string `json:"birthday"`
Education string `json:"education"`
Score int `json:"score"`
Karma int `json:"karma"`
Ranking int `json:"ranking"`
Balance float64 `json:"balance"`
BalanceCredit float64 `json:"balanceCredit"`
Currency string `json:"currency"`
BalanceCurrency string `json:"balanceCurrency"`
IsDefaultAvatar bool `json:"isDefaultAvatar"`
IsOnline bool `json:"isOnline"`
IsAdmin bool `json:"isAdmin"`
IsForbidden bool `json:"isForbidden"`
IsDeleted bool `json:"isDeleted"`
SignupApplication string `json:"signupApplication"`
Hash string `json:"hash"`
PreHash string `json:"preHash"`
RegisterType string `json:"registerType"`
RegisterSource string `json:"registerSource"`
AccessKey string `json:"accessKey"`
AccessSecret string `json:"accessSecret"`
AccessToken string `json:"accessToken"`
OriginalToken string `json:"originalToken"`
OriginalRefreshToken string `json:"originalRefreshToken"`
CreatedIp string `json:"createdIp"`
LastSigninTime string `json:"lastSigninTime"`
LastSigninIp string `json:"lastSigninIp"`
GitHub string `json:"github"`
Google string `json:"google"`
QQ string `json:"qq"`
WeChat string `json:"wechat"`
Facebook string `json:"facebook"`
DingTalk string `json:"dingtalk"`
Weibo string `json:"weibo"`
Gitee string `json:"gitee"`
LinkedIn string `json:"linkedin"`
Wecom string `json:"wecom"`
Lark string `json:"lark"`
Gitlab string `json:"gitlab"`
Adfs string `json:"adfs"`
Baidu string `json:"baidu"`
Alipay string `json:"alipay"`
Infoflow string `json:"infoflow"`
Apple string `json:"apple"`
AzureAD string `json:"azuread"`
AzureADB2c string `json:"azureadb2c"`
Slack string `json:"slack"`
Steam string `json:"steam"`
Bilibili string `json:"bilibili"`
Okta string `json:"okta"`
Douyin string `json:"douyin"`
Kwai string `json:"kwai"`
Line string `json:"line"`
Amazon string `json:"amazon"`
Auth0 string `json:"auth0"`
BattleNet string `json:"battlenet"`
Bitbucket string `json:"bitbucket"`
Box string `json:"box"`
CloudFoundry string `json:"cloudfoundry"`
Dailymotion string `json:"dailymotion"`
Deezer string `json:"deezer"`
DigitalOcean string `json:"digitalocean"`
Discord string `json:"discord"`
Dropbox string `json:"dropbox"`
EveOnline string `json:"eveonline"`
Fitbit string `json:"fitbit"`
Gitea string `json:"gitea"`
Heroku string `json:"heroku"`
InfluxCloud string `json:"influxcloud"`
Instagram string `json:"instagram"`
Intercom string `json:"intercom"`
Kakao string `json:"kakao"`
Lastfm string `json:"lastfm"`
Mailru string `json:"mailru"`
Meetup string `json:"meetup"`
MicrosoftOnline string `json:"microsoftonline"`
Nextcloud string `json:"nextcloud"`
OneDrive string `json:"onedrive"`
Oura string `json:"oura"`
Patreon string `json:"patreon"`
Paypal string `json:"paypal"`
SalesForce string `json:"salesforce"`
Shopify string `json:"shopify"`
Soundcloud string `json:"soundcloud"`
Spotify string `json:"spotify"`
Strava string `json:"strava"`
Stripe string `json:"stripe"`
TikTok string `json:"tiktok"`
Tumblr string `json:"tumblr"`
Twitch string `json:"twitch"`
Twitter string `json:"twitter"`
Typetalk string `json:"typetalk"`
Uber string `json:"uber"`
VK string `json:"vk"`
Wepay string `json:"wepay"`
Xero string `json:"xero"`
Yahoo string `json:"yahoo"`
Yammer string `json:"yammer"`
Yandex string `json:"yandex"`
Zoom string `json:"zoom"`
Custom string `json:"custom"`
Custom2 string `json:"custom2"`
Custom3 string `json:"custom3"`
Custom4 string `json:"custom4"`
Custom5 string `json:"custom5"`
Custom6 string `json:"custom6"`
Custom7 string `json:"custom7"`
Custom8 string `json:"custom8"`
Custom9 string `json:"custom9"`
Custom10 string `json:"custom10"`
PreferredMfaType string `json:"preferredMfaType"`
RecoveryCodes []string `json:"recoveryCodes"`
TotpSecret string `json:"totpSecret"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"`
MfaRadiusEnabled bool `json:"mfaRadiusEnabled"`
MfaRadiusUsername string `json:"mfaRadiusUsername"`
MfaRadiusProvider string `json:"mfaRadiusProvider"`
MfaPushEnabled bool `json:"mfaPushEnabled"`
MfaPushReceiver string `json:"mfaPushReceiver"`
MfaPushProvider string `json:"mfaPushProvider"`
MultiFactorAuths []*MfaProps `json:"multiFactorAuths,omitempty"`
Invitation string `json:"invitation"`
InvitationCode string `json:"invitationCode"`
FaceIds []*FaceId `json:"faceIds"`
Cart []ProductInfo `json:"cart"`
Ldap string `json:"ldap"`
Properties map[string]string `json:"properties"`
Roles []*Role `json:"roles"`
Permissions []*Permission `json:"permissions"`
Groups []string `json:"groups"`
LastChangePasswordTime string `json:"lastChangePasswordTime"`
LastSigninWrongTime string `json:"lastSigninWrongTime"`
SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `json:"managedAccounts"`
MfaAccounts []MfaAccount `json:"mfaAccounts"`
MfaItems []*MfaItem `json:"mfaItems"`
MfaRememberDeadline string `json:"mfaRememberDeadline"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
IpWhitelist string `json:"ipWhitelist"`
// BillingAccount is the signed `billing_account` claim: WHO PAYS for this
// credential, stated by IAM at mint time. Wire is "<kind>:<subject>" (e.g.
// "org:acme"); empty when IAM could not attribute one, in which case a reader
// must fall back to Payer's shape rule rather than bill a guess.
//
// It lives HERE, on the identity, and not on the Claims envelope that carries
// it, because every place that spends money holds a *User. When this field sat
// on Claims it was structurally unreachable from 27 of the 28 account.Payer
// call sites, so they all silently billed the shape-rule fallback: an org admin
// paid from their own empty wallet while the company pool sat funded and
// unused, and no layer could report an error because each one was doing exactly
// what it was told. Prefer u.Payer(ledger) over reading this directly.
BillingAccount string `json:"billing_account,omitempty"`
}
User mirrors the Hanzo IAM server's User JSON model. The FULL field set is reproduced deliberately (not a slim projection): object/redact.go reflects over every field by its json tag as a fail-secure secret-redaction control, and it must see every credential field (password*, *Secret, *Token, totp, social-login ids, custom1..10, …) to zero it before a user is returned to a client. A slim User would silently narrow that control and break its test. The json tags are therefore load-bearing and match the server verbatim; the server's xorm/db tags are dropped (ai has no ORM).
func (*User) Payer ¶ added in v1.831.13
Payer is THE money address a credential spends from — the one place that turns an authenticated identity into the (org, subject) pair a balance read, a spend gate and a usage debit all have to agree on.
WHY THIS EXISTS. account.Payer already owns the RULE (org pool vs personal wallet). What kept going wrong was not the rule but its INPUT: every caller built its own account.Credential by hand, and a caller that forgets one field gets a confidently wrong answer instead of a compile error. Twenty-eight sites built that struct; exactly one passed Account, so the signed billing_account claim was discarded almost everywhere. The result was the same bug this codebase has now hit five times — two layers deriving one address two ways — except distributed across the whole surface: a spend gate read the personal wallet while the funded org pool went untouched, and nothing errored, because each layer was faithfully answering the question it was asked.
So the fix is not "remember to pass the claim" — it is to stop asking callers to assemble the credential at all. Hand this method a ledger and it answers with the identity's own money address, claim included, every time.
ledger names the org whose ledger pays (the X-Org-Id namespace). Empty means "this user's home org", which is the behavior that predates org switching. A nil receiver yields the zero Account — unattributable, which every caller must treat as "refuse", never as "free".
func (*User) PayerSubject ¶ added in v1.831.13
PayerSubject is Payer(...).Subject() — the string form the balance, gate and usage paths pass around as ?user=. It exists so those call sites read as one step rather than two, and so a subject can never be built from a Credential assembled somewhere other than Payer above.