Skip to content
Danny edited this page Oct 13, 2026 · 4 revisions

IAM

iam is danny.vn/vngcloud/iam, with its own New(cfg). It reads caller identity, users, actions, policies, groups, and service accounts; see the IAM section of Services for that full read list. This page covers service account, policy, and group writes, the IAM resources this SDK writes here.

See Storage Workload Access for a dedicated service account and attached S3 key for an application. Its IAM client secret authenticates management calls; the application S3 secret is separate. Bucket policies grant the attached key's bucket access.

Service account writes

created, err := iamClient.CreateServiceAccount(ctx, &iam.CreateServiceAccountInput{
	Name:        "app",
	Description: "app service account",
})
if err != nil {
	log.Fatal(err)
}
secret := created.ClientSecret.Reveal() // save this now; it is never shown again

if _, err := iamClient.UpdateServiceAccount(ctx, &iam.UpdateServiceAccountInput{
	ServiceAccountID: created.ServiceAccount.ID,
	Description:      vngcloud.Ptr("renamed"),
}); err != nil {
	log.Fatal(err)
}

reset, err := iamClient.ResetServiceAccountSecret(ctx, &iam.ResetServiceAccountSecretInput{
	ServiceAccountID: created.ServiceAccount.ID,
})
if err != nil {
	log.Fatal(err)
}
newSecret := reset.ClientSecret.Reveal()

if _, err := iamClient.DeleteServiceAccount(ctx, &iam.DeleteServiceAccountInput{
	ServiceAccountID: created.ServiceAccount.ID,
}); err != nil {
	log.Fatal(err)
}

ClientSecret is a vngcloud.Secret, the same type compute.CreateSSHKey returns for a private key: printing, logging, or JSON-encoding it gives "[redacted]", and Reveal() is the only way to read the value back out. See Compute for the full redaction contract. When a create or reset response holds no secret, CreateServiceAccount and ResetServiceAccountSecret return iam.ErrNoSecret alongside their Output rather than succeeding silently; ClientSecret stays empty in that case. If the read CreateServiceAccount makes to confirm a new account fails, it returns iam.ErrCreateUnconfirmed alongside an Output that still carries the create response's own ID and client secret, with every other ServiceAccount field zero: the account and its secret are real either way, even though the error is not nil.

CreateServiceAccount and ResetServiceAccountSecret are POST and are sent at most once: a resend after a 401 or a followed redirect would create a second account or rotate the secret a second time, so neither is ever retried or resent, whatever the response. After any error that is not a 4xx *vngcloud.APIError, call ListServiceAccounts with Name and look for the service account before creating it again, rather than retrying blind. One found that way after a failed create has already lost its client secret. A failed reset cannot be checked this way at all: there is no read that shows whether the secret changed, so treat the previous one as no longer trustworthy either way.

UpdateServiceAccount, DeleteServiceAccount, and ResetServiceAccountSecret refuse to run, sending no request, when their target is the caller itself, holds a policy that grants an IAM write right, such as CreatePolicy or AttachPolicyToIamUser, found through the account's own action list, or when the caller itself is a service account, whatever the target.

A service-account caller (user-sa or service-sa) refuses every service-account-targeted write, not only one against its own ID: GreenNode's userinfo response is not confirmed to report a service-account caller's ID in the same form as a target's ID or ClientID, so the SDK cannot safely tell them apart and refuses every case rather than risk missing a real self-change.

Neither ErrSelfChange nor ErrPrivilegedChange names the policy, statement, or action involved, and there is no way to turn either guard off; make such a change from the IAM console instead. CreateServiceAccount has no guard: creating a service account changes no one's rights.

Policy writes

// sa is a service account from CreateServiceAccount, as shown above.
created, err := iamClient.CreatePolicy(ctx, &iam.CreatePolicyInput{
	Name: "app-read",
	Statements: []iam.Statement{
		{Effect: "allow", Actions: []string{"vserver:ListServers"}, Resources: []string{"*"}},
	},
})
if err != nil {
	log.Fatal(err)
}

if _, err := iamClient.UpdatePolicy(ctx, &iam.UpdatePolicyInput{
	PolicyID:    created.Policy.ID,
	Description: vngcloud.Ptr("read-only server access"),
}); err != nil {
	log.Fatal(err)
}

if _, err := iamClient.AttachServiceAccountPolicy(ctx, &iam.AttachServiceAccountPolicyInput{
	PolicyID:         created.Policy.ID,
	ServiceAccountID: sa.ServiceAccount.ID,
}); err != nil {
	log.Fatal(err)
}

if _, err := iamClient.DetachServiceAccountPolicy(ctx, &iam.DetachServiceAccountPolicyInput{
	PolicyID:         created.Policy.ID,
	ServiceAccountID: sa.ServiceAccount.ID,
}); err != nil {
	log.Fatal(err)
}

if _, err := iamClient.DeletePolicy(ctx, &iam.DeletePolicyInput{
	PolicyID: created.Policy.ID,
}); err != nil {
	log.Fatal(err)
}

Statements takes the API's own document shape: at least one statement, Effect of "allow" or "deny", and non-empty Actions and Resources with no empty string. CreatePolicy and UpdatePolicy check this shape, returning vngcloud.ErrInvalidInput before any request, so a document shaped like AWS's (Version, Statement, capitalized Effect) fails instead of reaching the server as an empty policy. Action, resource, and condition values are not checked; the server checks those. UpdatePolicy sends a full PUT, so it fills any field left nil (Name, Description, Statements) from the policy's current state first.

CreatePolicy is POST and sent at most once, the same as CreateServiceAccount: a resend after a 401 or a followed redirect would create a second policy, so it is never retried or resent after any response. After any error that is not a 4xx *vngcloud.APIError, call ListPolicies with Name and look for the policy before creating it again. AttachServiceAccountPolicy and DetachServiceAccountPolicy are plain POST and DELETE: a repeat attach or detach is visible as the server's own conflict or not-found response, so no such care is needed.

CreatePolicy, UpdatePolicy, DeletePolicy, every attach and detach, DeleteGroup, AddUserToGroup, and RemoveUserFromGroup each refuse to run, sending no request, per the rules below. Every guard also refuses for a caller whose type cannot be classified.

Write Refused when
CreatePolicy The statements grant an IAM write action
UpdatePolicy The policy is managed; its current or proposed statements grant an IAM write action; or it is attached to a protected principal or group
DeletePolicy The policy is managed, or attached to a group, an IAM user, or a service account
DeleteGroup The group has a member or a policy attached
AttachServiceAccountPolicy, DetachServiceAccountPolicy The policy grants an IAM write action, the target service account already holds one, or the caller is a service account
AttachGroupPolicy, DetachGroupPolicy The policy grants an IAM write action, or the target group is protected
AttachUserPolicy, DetachUserPolicy The policy grants an IAM write action, or the target user is protected
AddUserToGroup, RemoveUserFromGroup The user or the group is protected

A protected principal is the caller, an IAM user or service account holding a policy that grants an IAM write right (an IAM user counts as protected through a group's policy too), and a protected group is one with such a policy attached, or with a protected principal as a member.

Sentinel Meaning
iam.ErrSelfChange The target service account is the caller, the caller is a service account, UpdatePolicy targets a policy attached to the caller (or to any service account, when the caller is one), or a group membership or attach call would change the caller's own rights
iam.ErrPrivilegedChange The target, or something it is attached to, holds a policy that grants an IAM write right, or the caller's own type could not be classified
iam.ErrManagedPolicy UpdatePolicy or DeletePolicy targets a GreenNode-managed policy
iam.ErrInUse DeletePolicy targets a policy still attached to a group, an IAM user, or a service account, or DeleteGroup targets a group with a member or a policy
iam.ErrNoSecret A create or reset response reported success but carried no client secret
iam.ErrCreateUnconfirmed A create succeeded but the read-back that confirms it failed
iam.ErrNotSettled CreatePolicy, UpdatePolicy, CreateGroup, or UpdateGroup succeeded but the read-back that confirms it failed; Output keeps the resource's ID

None of these sentinels name the policy, statement, or action involved, and there is no way to turn any guard off; make such a change from the IAM console instead.

Group writes

group, err := iamClient.CreateGroup(ctx, &iam.CreateGroupInput{Name: "app-readers"})
if err != nil {
	log.Fatal(err)
}
iamClient.AddUserToGroup(ctx, &iam.AddUserToGroupInput{GroupID: group.Group.ID, UserID: userID})
iamClient.AttachGroupPolicy(ctx, &iam.AttachGroupPolicyInput{PolicyID: policyID, GroupID: group.Group.ID})
iamClient.DetachGroupPolicy(ctx, &iam.DetachGroupPolicyInput{PolicyID: policyID, GroupID: group.Group.ID})
iamClient.RemoveUserFromGroup(ctx, &iam.RemoveUserFromGroupInput{GroupID: group.Group.ID, UserID: userID})
iamClient.DeleteGroup(ctx, &iam.DeleteGroupInput{GroupID: group.Group.ID})

CreateGroup always sends mode: "iam" with no initial members or policies; like CreateServiceAccount, CreateGroup and UpdateGroup have no guard. AttachUserPolicy and DetachUserPolicy attach a policy directly to an IAM user, in the same shape as the group calls above.

Clone this wiki locally