mirror of
https://github.com/minio/minio.git
synced 2024-12-24 06:05:55 -05:00
add missing admin actions, enhance AccountUsageInfo (#9607)
This commit is contained in:
parent
247795dd36
commit
814ddc0923
@ -572,6 +572,111 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
// AccountUsageInfoHandler returns usage
|
||||
func (a adminAPIHandlers) AccountUsageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AccountUsageInfo")
|
||||
|
||||
defer logger.AuditLog(w, r, "AccountUsageInfo", mustGetClaimsFromToken(r))
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Set prefix value for "s3:prefix" policy conditionals.
|
||||
r.Header.Set("prefix", "")
|
||||
|
||||
// Set delimiter value for "s3:delimiter" policy conditionals.
|
||||
r.Header.Set("delimiter", SlashSeparator)
|
||||
|
||||
isAllowedAccess := func(bucketName string) (rd, wr bool) {
|
||||
// Use the following trick to filter in place
|
||||
// https://github.com/golang/go/wiki/SliceTricks#filter-in-place
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Action: iampolicy.ListBucketAction,
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
||||
IsOwner: owner,
|
||||
ObjectName: "",
|
||||
Claims: claims,
|
||||
}) {
|
||||
rd = true
|
||||
}
|
||||
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Action: iampolicy.PutObjectAction,
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
||||
IsOwner: owner,
|
||||
ObjectName: "",
|
||||
Claims: claims,
|
||||
}) {
|
||||
wr = true
|
||||
}
|
||||
|
||||
return rd, wr
|
||||
}
|
||||
|
||||
buckets, err := objectAPI.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Load the latest calculated data usage
|
||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
accountName := cred.AccessKey
|
||||
if cred.ParentUser != "" {
|
||||
accountName = cred.ParentUser
|
||||
}
|
||||
|
||||
acctInfo := madmin.AccountUsageInfo{
|
||||
AccountName: accountName,
|
||||
}
|
||||
|
||||
for _, bucket := range buckets {
|
||||
rd, wr := isAllowedAccess(bucket.Name)
|
||||
if rd || wr {
|
||||
var size uint64
|
||||
// Fetch the data usage of the current bucket
|
||||
if !dataUsageInfo.LastUpdate.IsZero() && len(dataUsageInfo.BucketsSizes) > 0 {
|
||||
size = dataUsageInfo.BucketsSizes[bucket.Name]
|
||||
}
|
||||
acctInfo.Buckets = append(acctInfo.Buckets, madmin.BucketUsageInfo{
|
||||
Name: bucket.Name,
|
||||
Created: bucket.Created,
|
||||
Size: size,
|
||||
Access: madmin.AccountAccess{
|
||||
Read: rd,
|
||||
Write: wr,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
usageInfoJSON, err := json.Marshal(acctInfo)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, usageInfoJSON)
|
||||
}
|
||||
|
||||
// InfoCannedPolicyV2 - GET /minio/admin/v2/info-canned-policy?name={policyName}
|
||||
func (a adminAPIHandlers) InfoCannedPolicyV2(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "InfoCannedPolicyV2")
|
||||
|
@ -326,70 +326,6 @@ func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Re
|
||||
writeSuccessResponseJSON(w, dataUsageInfoJSON)
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) AccountingUsageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AccountingUsageInfo")
|
||||
|
||||
defer logger.AuditLog(w, r, "AccountingUsageInfo", mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AccountingUsageInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var accountingUsageInfo = make(map[string]madmin.BucketAccountingUsage)
|
||||
|
||||
buckets, err := objectAPI.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
// writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := globalIAMSys.ListUsers()
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Load the latest calculated data usage
|
||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
// Calculate for each bucket, which users are allowed to access to it
|
||||
for _, bucket := range buckets {
|
||||
bucketUsageInfo := madmin.BucketAccountingUsage{}
|
||||
|
||||
// Fetch the data usage of the current bucket
|
||||
if !dataUsageInfo.LastUpdate.IsZero() && dataUsageInfo.BucketsSizes != nil {
|
||||
bucketUsageInfo.Size = dataUsageInfo.BucketsSizes[bucket.Name]
|
||||
}
|
||||
|
||||
for user := range users {
|
||||
rd, wr, custom := globalIAMSys.GetAccountAccess(user, bucket.Name)
|
||||
if rd || wr || custom {
|
||||
bucketUsageInfo.AccessList = append(bucketUsageInfo.AccessList, madmin.AccountAccess{
|
||||
AccountName: user,
|
||||
Read: rd,
|
||||
Write: wr,
|
||||
Custom: custom,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
accountingUsageInfo[bucket.Name] = bucketUsageInfo
|
||||
}
|
||||
|
||||
usageInfoJSON, err := json.Marshal(accountingUsageInfo)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, usageInfoJSON)
|
||||
}
|
||||
|
||||
func newLockEntry(l lockRequesterInfo, resource, server string) *madmin.LockEntry {
|
||||
entry := &madmin.LockEntry{
|
||||
Timestamp: l.Timestamp,
|
||||
|
@ -62,8 +62,6 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps, enab
|
||||
// DataUsageInfo operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/datausageinfo").HandlerFunc(httpTraceAll(adminAPI.DataUsageInfoHandler))
|
||||
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountingusageinfo").HandlerFunc(httpTraceAll(adminAPI.AccountingUsageInfoHandler))
|
||||
|
||||
if globalIsDistXL || globalIsXL {
|
||||
/// Heal operations
|
||||
|
||||
@ -115,6 +113,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps, enab
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
|
||||
|
||||
// Add user IAM
|
||||
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountusageinfo").HandlerFunc(httpTraceAll(adminAPI.AccountUsageInfoHandler))
|
||||
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
|
||||
|
105
cmd/iam.go
105
cmd/iam.go
@ -28,7 +28,6 @@ import (
|
||||
"github.com/minio/minio/cmd/config"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
@ -1345,110 +1344,6 @@ func (sys *IAMSys) policyDBSet(name, policyName string, userType IAMUserType, is
|
||||
return nil
|
||||
}
|
||||
|
||||
var iamAccountReadAccessActions = iampolicy.NewActionSet(
|
||||
iampolicy.ListMultipartUploadPartsAction,
|
||||
iampolicy.ListBucketMultipartUploadsAction,
|
||||
iampolicy.ListBucketAction,
|
||||
iampolicy.HeadBucketAction,
|
||||
iampolicy.GetObjectAction,
|
||||
iampolicy.GetBucketLocationAction,
|
||||
|
||||
// iampolicy.ListAllMyBucketsAction,
|
||||
)
|
||||
|
||||
var iamAccountWriteAccessActions = iampolicy.NewActionSet(
|
||||
iampolicy.AbortMultipartUploadAction,
|
||||
iampolicy.CreateBucketAction,
|
||||
iampolicy.PutObjectAction,
|
||||
iampolicy.DeleteObjectAction,
|
||||
iampolicy.DeleteBucketAction,
|
||||
)
|
||||
|
||||
var iamAccountOtherAccessActions = iampolicy.NewActionSet(
|
||||
iampolicy.BypassGovernanceRetentionAction,
|
||||
iampolicy.PutObjectRetentionAction,
|
||||
iampolicy.GetObjectRetentionAction,
|
||||
iampolicy.GetObjectLegalHoldAction,
|
||||
iampolicy.PutObjectLegalHoldAction,
|
||||
iampolicy.GetBucketObjectLockConfigurationAction,
|
||||
iampolicy.PutBucketObjectLockConfigurationAction,
|
||||
|
||||
iampolicy.ListenBucketNotificationAction,
|
||||
|
||||
iampolicy.PutBucketLifecycleAction,
|
||||
iampolicy.GetBucketLifecycleAction,
|
||||
|
||||
iampolicy.PutBucketNotificationAction,
|
||||
iampolicy.GetBucketNotificationAction,
|
||||
|
||||
iampolicy.PutBucketPolicyAction,
|
||||
iampolicy.DeleteBucketPolicyAction,
|
||||
iampolicy.GetBucketPolicyAction,
|
||||
|
||||
iampolicy.PutBucketEncryptionAction,
|
||||
iampolicy.GetBucketEncryptionAction,
|
||||
)
|
||||
|
||||
// GetAccountAccess iterates over all policies documents associated to a user
|
||||
// and returns if the user has read and/or write access to any resource.
|
||||
func (sys *IAMSys) GetAccountAccess(accountName, bucket string) (rd, wr, o bool) {
|
||||
policies, err := sys.PolicyDBGet(accountName, false)
|
||||
if err != nil {
|
||||
return false, false, false
|
||||
}
|
||||
|
||||
if len(policies) == 0 {
|
||||
// No policy found.
|
||||
return false, false, false
|
||||
}
|
||||
|
||||
// Policies were found, evaluate all of them.
|
||||
sys.store.rlock()
|
||||
defer sys.store.runlock()
|
||||
|
||||
var availablePolicies []iampolicy.Policy
|
||||
for _, pname := range policies {
|
||||
p, found := sys.iamPolicyDocsMap[pname]
|
||||
if found {
|
||||
availablePolicies = append(availablePolicies, p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(availablePolicies) == 0 {
|
||||
return false, false, false
|
||||
}
|
||||
|
||||
combinedPolicy := availablePolicies[0]
|
||||
for i := 1; i < len(availablePolicies); i++ {
|
||||
combinedPolicy.Statements = append(combinedPolicy.Statements,
|
||||
availablePolicies[i].Statements...)
|
||||
}
|
||||
|
||||
allActions := iampolicy.NewActionSet(iampolicy.AllActions)
|
||||
for _, st := range combinedPolicy.Statements {
|
||||
// Ignore if this is not an allow policy statement
|
||||
if st.Effect != policy.Allow {
|
||||
continue
|
||||
}
|
||||
// Fast calculation if there is s3:* permissions to any resource
|
||||
if !st.Actions.Intersection(allActions).IsEmpty() {
|
||||
rd, wr, o = true, true, true
|
||||
break
|
||||
}
|
||||
if !st.Actions.Intersection(iamAccountReadAccessActions).IsEmpty() {
|
||||
rd = true
|
||||
}
|
||||
if !st.Actions.Intersection(iamAccountWriteAccessActions).IsEmpty() {
|
||||
wr = true
|
||||
}
|
||||
if !st.Actions.Intersection(iamAccountOtherAccessActions).IsEmpty() {
|
||||
o = true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PolicyDBGet - gets policy set on a user or group. Since a user may
|
||||
// be a member of multiple groups, this function returns an array of
|
||||
// applicable policies (each group is mapped to at most one policy).
|
||||
|
@ -140,7 +140,6 @@ const (
|
||||
|
||||
// List of all supported actions.
|
||||
var supportedActions = map[Action]struct{}{
|
||||
AllActions: {},
|
||||
AbortMultipartUploadAction: {},
|
||||
CreateBucketAction: {},
|
||||
DeleteBucketAction: {},
|
||||
@ -157,25 +156,26 @@ var supportedActions = map[Action]struct{}{
|
||||
ListBucketMultipartUploadsAction: {},
|
||||
ListenBucketNotificationAction: {},
|
||||
ListMultipartUploadPartsAction: {},
|
||||
PutBucketLifecycleAction: {},
|
||||
GetBucketLifecycleAction: {},
|
||||
PutBucketNotificationAction: {},
|
||||
PutBucketPolicyAction: {},
|
||||
PutObjectAction: {},
|
||||
GetBucketLifecycleAction: {},
|
||||
PutBucketLifecycleAction: {},
|
||||
BypassGovernanceRetentionAction: {},
|
||||
PutObjectRetentionAction: {},
|
||||
GetObjectRetentionAction: {},
|
||||
GetObjectLegalHoldAction: {},
|
||||
PutObjectLegalHoldAction: {},
|
||||
PutBucketObjectLockConfigurationAction: {},
|
||||
GetBucketObjectLockConfigurationAction: {},
|
||||
PutBucketTaggingAction: {},
|
||||
PutBucketObjectLockConfigurationAction: {},
|
||||
GetBucketTaggingAction: {},
|
||||
BypassGovernanceRetentionAction: {},
|
||||
PutBucketTaggingAction: {},
|
||||
GetObjectTaggingAction: {},
|
||||
PutObjectTaggingAction: {},
|
||||
DeleteObjectTaggingAction: {},
|
||||
PutBucketEncryptionAction: {},
|
||||
GetBucketEncryptionAction: {},
|
||||
AllActions: {},
|
||||
}
|
||||
|
||||
// List of all supported object actions.
|
||||
|
@ -31,8 +31,6 @@ const (
|
||||
|
||||
// StorageInfoAdminAction - allow listing server info
|
||||
StorageInfoAdminAction = "admin:StorageInfo"
|
||||
// AccountingUsageInfoAdminAction - allow listing accounting usage info
|
||||
AccountingUsageInfoAdminAction = "admin:AccountingUsageInfo"
|
||||
// DataUsageInfoAdminAction - allow listing data usage info
|
||||
DataUsageInfoAdminAction = "admin:DataUsageInfo"
|
||||
// TopLocksAdminAction - allow listing top locks
|
||||
@ -114,17 +112,16 @@ const (
|
||||
|
||||
// List of all supported admin actions.
|
||||
var supportedAdminActions = map[AdminAction]struct{}{
|
||||
AllAdminActions: {},
|
||||
HealAdminAction: {},
|
||||
ServerInfoAdminAction: {},
|
||||
StorageInfoAdminAction: {},
|
||||
DataUsageInfoAdminAction: {},
|
||||
TopLocksAdminAction: {},
|
||||
ProfilingAdminAction: {},
|
||||
TraceAdminAction: {},
|
||||
OBDInfoAdminAction: {},
|
||||
ConsoleLogAdminAction: {},
|
||||
KMSKeyStatusAdminAction: {},
|
||||
ServerInfoAdminAction: {},
|
||||
OBDInfoAdminAction: {},
|
||||
ServerUpdateAdminAction: {},
|
||||
ServiceRestartAdminAction: {},
|
||||
ServiceStopAdminAction: {},
|
||||
@ -137,6 +134,7 @@ var supportedAdminActions = map[AdminAction]struct{}{
|
||||
GetUserAdminAction: {},
|
||||
AddUserToGroupAdminAction: {},
|
||||
RemoveUserFromGroupAdminAction: {},
|
||||
GetGroupAdminAction: {},
|
||||
ListGroupsAdminAction: {},
|
||||
EnableGroupAdminAction: {},
|
||||
DisableGroupAdminAction: {},
|
||||
@ -144,9 +142,10 @@ var supportedAdminActions = map[AdminAction]struct{}{
|
||||
DeletePolicyAdminAction: {},
|
||||
GetPolicyAdminAction: {},
|
||||
AttachPolicyAdminAction: {},
|
||||
ListUserPoliciesAdminAction: {},
|
||||
SetBucketQuotaAdminAction: {},
|
||||
GetBucketQuotaAdminAction: {},
|
||||
ListUserPoliciesAdminAction: {},
|
||||
AllAdminActions: {},
|
||||
}
|
||||
|
||||
func parseAdminAction(s string) (AdminAction, error) {
|
||||
|
@ -192,50 +192,6 @@ func (adm *AdminClient) DataUsageInfo(ctx context.Context) (DataUsageInfo, error
|
||||
return dataUsageInfo, nil
|
||||
}
|
||||
|
||||
// AccountAccess contains information about
|
||||
type AccountAccess struct {
|
||||
AccountName string `json:"accountName"`
|
||||
Read bool `json:"read"`
|
||||
Write bool `json:"write"`
|
||||
Custom bool `json:"custom"`
|
||||
}
|
||||
|
||||
// BucketAccountingUsage represents the accounting usage of a particular bucket
|
||||
type BucketAccountingUsage struct {
|
||||
Size uint64 `json:"size"`
|
||||
AccessList []AccountAccess `json:"accessList"`
|
||||
}
|
||||
|
||||
// AccountingUsageInfo returns the accounting usage info, currently it returns
|
||||
// the type of access of different accounts to the different buckets.
|
||||
func (adm *AdminClient) AccountingUsageInfo(ctx context.Context) (map[string]BucketAccountingUsage, error) {
|
||||
resp, err := adm.executeMethod(ctx, http.MethodGet, requestData{relPath: adminAPIPrefix + "/accountingusageinfo"})
|
||||
defer closeResponse(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check response http status code
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, httpRespToErrorResponse(resp)
|
||||
}
|
||||
|
||||
// Unmarshal the server's json response
|
||||
var accountingUsageInfo map[string]BucketAccountingUsage
|
||||
|
||||
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(respBytes, &accountingUsageInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return accountingUsageInfo, nil
|
||||
}
|
||||
|
||||
// InfoMessage container to hold server admin related information.
|
||||
type InfoMessage struct {
|
||||
Mode string `json:"mode,omitempty"`
|
||||
|
@ -23,11 +23,63 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
)
|
||||
|
||||
// AccountAccess contains information about
|
||||
type AccountAccess struct {
|
||||
Read bool `json:"read"`
|
||||
Write bool `json:"write"`
|
||||
}
|
||||
|
||||
// BucketUsageInfo represents bucket usage of a bucket, and its relevant
|
||||
// access type for an account
|
||||
type BucketUsageInfo struct {
|
||||
Name string `json:"name"`
|
||||
Size uint64 `json:"size"`
|
||||
Created time.Time `json:"created"`
|
||||
Access AccountAccess `json:"access"`
|
||||
}
|
||||
|
||||
// AccountUsageInfo represents the account usage info of an
|
||||
// account across buckets.
|
||||
type AccountUsageInfo struct {
|
||||
AccountName string
|
||||
Buckets []BucketUsageInfo
|
||||
}
|
||||
|
||||
// AccountUsageInfo returns the usage info for the authenticating account.
|
||||
func (adm *AdminClient) AccountUsageInfo(ctx context.Context) (AccountUsageInfo, error) {
|
||||
resp, err := adm.executeMethod(ctx, http.MethodGet, requestData{relPath: adminAPIPrefix + "/accountusageinfo"})
|
||||
defer closeResponse(resp)
|
||||
if err != nil {
|
||||
return AccountUsageInfo{}, err
|
||||
}
|
||||
|
||||
// Check response http status code
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return AccountUsageInfo{}, httpRespToErrorResponse(resp)
|
||||
}
|
||||
|
||||
// Unmarshal the server's json response
|
||||
var accountInfo AccountUsageInfo
|
||||
|
||||
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return AccountUsageInfo{}, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(respBytes, &accountInfo)
|
||||
if err != nil {
|
||||
return AccountUsageInfo{}, err
|
||||
}
|
||||
|
||||
return accountInfo, nil
|
||||
}
|
||||
|
||||
// AccountStatus - account status.
|
||||
type AccountStatus string
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user