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 Casdoor 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 Casdoor server module (beego, xorm, ldap, aliyun/aws SDKs, …) that iam-v1 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 ¶
- func AddPermission(permission *Permission) (bool, error)
- func DeletePermission(permission *Permission) (bool, error)
- func DeleteResourceWithTag(resource *Resource, tag string) (bool, error)
- func EffectiveOrg(home string, orgs []OrgRef, requested string) string
- func GetOAuthToken(code, state string, opts ...OAuthOption) (*oauth2.Token, error)
- func InitConfig(endpoint, clientId, clientSecret, certificate, organizationName, ... string)
- func LedgerOrg(effective, home string, masquerade bool) 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 OrgRef
- type Organization
- type Permission
- type ProductInfo
- type Provider
- type Resource
- type Response
- type Role
- type Status
- type User
Constants ¶
This section is empty.
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 EffectiveOrg ¶ added in v1.831.10
EffectiveOrg reports the org a credential ACTS IN: its home org, unless the caller asked for another AND the signed `orgs` claim names that one.
orgs is the membership set IAM signed into the token — the ONLY admissible proof. A raw X-Org-Id is a claim by the client about itself; honoring one that the signature does not back is a cross-tenant read. A credential carrying no such claim (a cookie session, an hk-/sk-/hz- key, a token minted before the claim shipped) passes nil here, nil names no org, and the caller stays home.
The comparison is byte-exact — no trim, no case fold. "acme" and "ACME" are DISTINCT orgs, and folding them here would let a request address a ledger the membership row does not name. This matches cloud's isMember byte for byte, so the edge and the ledger cannot disagree about who is a member.
Refusal is SILENT and returns exactly what "no switch requested" returns: a non-member learns nothing about which orgs exist or which it failed to enter.
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 LedgerOrg ¶ added in v1.831.10
LedgerOrg reports the org a credential SPENDS FROM — the wallet the balance gate reads, the budget reservation holds, and the usage debit lands on.
TWO branches, deliberately not one expression:
- masquerade: a super admin may ACT in any tenant (platform ops, support), but it spends its OWN home ledger. An admin working on a customer's behalf must never drain the customer's wallet — the customer did not authorize the spend and cannot see who made it.
- everyone else: the org they are acting in, which EffectiveOrg already proved they belong to. This is the whole point of the switcher — the org selected is the org that pays.
masquerade is passed in rather than derived because the super-admin predicate (membership in the reserved admin org) lives in util, which imports this package; taking the answer as an argument keeps this a pure function of values.
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 []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)
GetApplication fetches an application by name (owned by admin).
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 Casdoor /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 OrgRef ¶
OrgRef is one entry of the signed `orgs` membership-set claim: an org the subject may act in, plus a coarse role there.
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 Casdoor 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.