Files
seaweedfs/weed/iamapi/iamapi_management_handlers.go
T
Chris Lu 4e5839ce82 fix(iam): return a valid user ARN from CreateUser and GetUser (#9794)
* fix(iam): return a valid user ARN from CreateUser and GetUser

The terraform aws provider 6.41 reads a user back after creating it and
blocks until GetUser returns a value that passes arn.IsARN. We only set
UserName, so the ARN was empty and apply hung until the 2m timeout.
Populate Arn (and Path) via a shared iam.NewUser helper in both the
embedded and standalone IAM handlers.

* fix(iam): use the userName parameter directly in NewUser

Drop the redundant local copy; the value parameter is already function-local.

* fix(iam): return full user objects with ARNs from GetGroup

GetGroup listed members with only UserName set. Build them via the shared
NewUser helper so group members carry a valid Arn and Path like the other
user responses, in both the embedded and standalone IAM handlers.
2026-06-02 22:01:57 -07:00

1645 lines
59 KiB
Go

package iamapi
// This file provides IAM API handlers for the standalone IAM server.
// Common IAM types and helpers are imported from the shared weed/iam package.
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"sync"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/seaweedfs/seaweedfs/weed/glog"
iamlib "github.com/seaweedfs/seaweedfs/weed/iam"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
"github.com/seaweedfs/seaweedfs/weed/util/request_id"
)
// Constants from shared package
const (
charsetUpper = iamlib.CharsetUpper
charset = iamlib.Charset
policyDocumentVersion = iamlib.PolicyDocumentVersion
StatementActionAdmin = iamlib.StatementActionAdmin
StatementActionWrite = iamlib.StatementActionWrite
StatementActionWriteAcp = iamlib.StatementActionWriteAcp
StatementActionRead = iamlib.StatementActionRead
StatementActionReadAcp = iamlib.StatementActionReadAcp
StatementActionList = iamlib.StatementActionList
StatementActionTagging = iamlib.StatementActionTagging
StatementActionDelete = iamlib.StatementActionDelete
USER_DOES_NOT_EXIST = iamlib.UserDoesNotExist
accessKeyStatusActive = iamlib.AccessKeyStatusActive
accessKeyStatusInactive = iamlib.AccessKeyStatusInactive
)
var policyLock = sync.RWMutex{}
const policyArnPrefix = "arn:aws:iam:::policy/"
// policyDefaultVersionId is the single managed-policy version SeaweedFS exposes.
// Managed policies are stored as one current document with no version history,
// so the API presents exactly one version ("v1") which is always the default.
const policyDefaultVersionId = "v1"
// parsePolicyArn validates an IAM policy ARN and extracts the policy name.
func parsePolicyArn(policyArn string) (string, *IamError) {
if !strings.HasPrefix(policyArn, policyArnPrefix) {
return "", &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("invalid policy ARN: %s", policyArn)}
}
policyName := strings.TrimPrefix(policyArn, policyArnPrefix)
if policyName == "" {
return "", &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("invalid policy ARN: %s", policyArn)}
}
return policyName, nil
}
// userPolicyKey returns a namespaced key for inline user policies to prevent collision with managed policies.
// getOrCreateUserPolicies returns the policy map for a user, creating it if needed.
// Returns a pointer to the user's policy map from Policies.InlinePolicies.
func (p *Policies) getOrCreateUserPolicies(userName string) map[string]policy_engine.PolicyDocument {
if p.InlinePolicies == nil {
p.InlinePolicies = make(map[string]map[string]policy_engine.PolicyDocument)
}
if p.InlinePolicies[userName] == nil {
p.InlinePolicies[userName] = make(map[string]policy_engine.PolicyDocument)
}
return p.InlinePolicies[userName]
}
// getOrCreateGroupPolicies returns the policy map for a group, creating it if needed.
func (p *Policies) getOrCreateGroupPolicies(groupName string) map[string]policy_engine.PolicyDocument {
if p.GroupInlinePolicies == nil {
p.GroupInlinePolicies = make(map[string]map[string]policy_engine.PolicyDocument)
}
if p.GroupInlinePolicies[groupName] == nil {
p.GroupInlinePolicies[groupName] = make(map[string]policy_engine.PolicyDocument)
}
return p.GroupInlinePolicies[groupName]
}
// computeAggregatedActionsForUser computes the union of actions across all inline policies for a user.
// Directly accesses user's policies from Policies.InlinePolicies[userName] for O(1) lookup.
// If policies is non-nil, it uses that instead of fetching from storage (for I/O optimization).
// When policies is nil, it fetches from storage using GetPolicies.
//
// Performance: O(user_policies) instead of O(all_policies) with per-user index.
//
// Best-effort aggregation: If GetActions fails for a policy document, that policy is logged at Warning level
// but is NOT removed from persistent storage. This intentional choice ensures:
// - Stored policy documents survive even if they temporarily fail to parse
// - The policy data is preserved for potential future fixes or manual inspection
// - Only the runtime action set (ident.Actions) is affected when GetActions fails
// This keeps persistent state consistent while gracefully handling parsing errors.
func computeAggregatedActionsForUser(iama *IamApiServer, userName string, policies *Policies) ([]string, error) {
var aggregatedActions []string
actionSet := make(map[string]bool)
var policiesToUse Policies
if policies != nil {
// Use provided Policies (caller already fetched, avoids redundant I/O)
policiesToUse = *policies
} else {
// Fetch from storage
if err := iama.s3ApiConfig.GetPolicies(&policiesToUse); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return nil, err
}
}
// Direct O(1) access to user's policies using per-user index
userPolicies := policiesToUse.InlinePolicies[userName]
if len(userPolicies) == 0 {
return aggregatedActions, nil
}
for policyName, policyDocument := range userPolicies {
actions, err := GetActions(&policyDocument)
if err != nil {
// Best-effort: policy stored successfully but failed to parse; log and skip from aggregation
glog.Warningf("Failed to get actions from stored policy '%s' for user %s (policy retained in storage): %v", policyName, userName, err)
continue
}
for _, action := range actions {
if !actionSet[action] {
actionSet[action] = true
aggregatedActions = append(aggregatedActions, action)
}
}
}
return aggregatedActions, nil
}
// Helper function wrappers using shared package
func MapToStatementAction(action string) string {
return iamlib.MapToStatementAction(action)
}
func MapToIdentitiesAction(action string) string {
return iamlib.MapToIdentitiesAction(action)
}
type Policies struct {
// Policies: managed policies (flat map, unchanged for backward compatibility)
Policies map[string]policy_engine.PolicyDocument `json:"policies"`
// InlinePolicies: user-indexed inline policies for O(1) lookup
// Structure: [userName][policyName] -> PolicyDocument
// Enables fast access without iterating all policies
InlinePolicies map[string]map[string]policy_engine.PolicyDocument `json:"inlinePolicies"`
// GroupInlinePolicies: group-indexed inline policies for O(1) lookup
// Structure: [groupName][policyName] -> PolicyDocument
GroupInlinePolicies map[string]map[string]policy_engine.PolicyDocument `json:"groupInlinePolicies,omitempty"`
}
func Hash(s *string) string {
return iamlib.Hash(s)
}
func StringWithCharset(length int, charset string) (string, error) {
return iamlib.GenerateRandomString(length, charset)
}
func stringSlicesEqual(a, b []string) bool {
return iamlib.StringSlicesEqual(a, b)
}
func validateAccessKeyStatus(status string) error {
switch status {
case accessKeyStatusActive, accessKeyStatusInactive:
return nil
case "":
return fmt.Errorf("Status parameter is required")
default:
return fmt.Errorf("Status must be '%s' or '%s'", accessKeyStatusActive, accessKeyStatusInactive)
}
}
func (iama *IamApiServer) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListUsersResponse) {
resp = &ListUsersResponse{}
for _, ident := range s3cfg.Identities {
user := iamlib.NewUser(ident.Name)
resp.ListUsersResult.Users = append(resp.ListUsersResult.Users, &user)
}
return resp
}
func (iama *IamApiServer) ListAccessKeys(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListAccessKeysResponse) {
resp = &ListAccessKeysResponse{}
userName := values.Get("UserName")
for _, ident := range s3cfg.Identities {
if userName != "" && userName != ident.Name {
continue
}
for _, cred := range ident.Credentials {
status := cred.Status
if status == "" {
status = accessKeyStatusActive
}
identName := ident.Name
accessKey := cred.AccessKey
resp.ListAccessKeysResult.AccessKeyMetadata = append(resp.ListAccessKeysResult.AccessKeyMetadata,
&iam.AccessKeyMetadata{UserName: &identName, AccessKeyId: &accessKey, Status: &status},
)
}
}
return resp
}
func (iama *IamApiServer) CreateUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *CreateUserResponse) {
resp = &CreateUserResponse{}
userName := values.Get("UserName")
resp.CreateUserResult.User = iamlib.NewUser(userName)
s3cfg.Identities = append(s3cfg.Identities, &iam_pb.Identity{Name: userName})
return resp
}
func (iama *IamApiServer) DeleteUser(s3cfg *iam_pb.S3ApiConfiguration, userName string) (resp *DeleteUserResponse, err *IamError) {
resp = &DeleteUserResponse{}
for i, ident := range s3cfg.Identities {
if userName == ident.Name {
// Clean up any inline policies stored for this user
policies := Policies{}
if pErr := iama.s3ApiConfig.GetPolicies(&policies); pErr != nil && !errors.Is(pErr, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr}
}
if policies.InlinePolicies != nil {
if _, exists := policies.InlinePolicies[userName]; exists {
delete(policies.InlinePolicies, userName)
if pErr := iama.s3ApiConfig.PutPolicies(&policies); pErr != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr}
}
}
}
s3cfg.Identities = append(s3cfg.Identities[:i], s3cfg.Identities[i+1:]...)
// Remove user from all groups
removeUserFromAllGroups(s3cfg, userName)
return resp, nil
}
}
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
func (iama *IamApiServer) GetUser(s3cfg *iam_pb.S3ApiConfiguration, userName string) (resp *GetUserResponse, err *IamError) {
resp = &GetUserResponse{}
for _, ident := range s3cfg.Identities {
if userName == ident.Name {
resp.GetUserResult.User = iamlib.NewUser(ident.Name)
return resp, nil
}
}
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
func (iama *IamApiServer) UpdateUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *UpdateUserResponse, err *IamError) {
resp = &UpdateUserResponse{}
userName := values.Get("UserName")
newUserName := values.Get("NewUserName")
if newUserName == "" {
return resp, nil
}
// Find the source identity first
var sourceIdent *iam_pb.Identity
for _, ident := range s3cfg.Identities {
if ident.Name == userName {
sourceIdent = ident
break
}
}
if sourceIdent == nil {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
// No-op if renaming to the same name
if newUserName == userName {
return resp, nil
}
// Check for name collision before renaming
for _, ident := range s3cfg.Identities {
if ident.Name == newUserName {
return resp, &IamError{
Code: iam.ErrCodeEntityAlreadyExistsException,
Error: fmt.Errorf("user %s already exists", newUserName),
}
}
}
// Check for inline policy collision
policies := Policies{}
if pErr := iama.s3ApiConfig.GetPolicies(&policies); pErr != nil && !errors.Is(pErr, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr}
}
if policies.InlinePolicies != nil {
if _, exists := policies.InlinePolicies[newUserName]; exists {
return resp, &IamError{
Code: iam.ErrCodeEntityAlreadyExistsException,
Error: fmt.Errorf("inline policies already exist for user %s", newUserName),
}
}
}
sourceIdent.Name = newUserName
// Move any inline policies from old username to new username
if policies.InlinePolicies != nil {
if userPolicies, exists := policies.InlinePolicies[userName]; exists {
delete(policies.InlinePolicies, userName)
policies.InlinePolicies[newUserName] = userPolicies
if pErr := iama.s3ApiConfig.PutPolicies(&policies); pErr != nil {
// Rollback: restore identity name and inline policies
sourceIdent.Name = userName
delete(policies.InlinePolicies, newUserName)
policies.InlinePolicies[userName] = userPolicies
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr}
}
}
}
// Update group membership references
updateUserInGroups(s3cfg, userName, newUserName)
// Update service account parent references
for _, sa := range s3cfg.ServiceAccounts {
if sa.ParentUser == userName {
sa.ParentUser = newUserName
}
}
return resp, nil
}
func GetPolicyDocument(policy *string) (policy_engine.PolicyDocument, error) {
var policyDocument policy_engine.PolicyDocument
if err := json.Unmarshal([]byte(*policy), &policyDocument); err != nil {
return policy_engine.PolicyDocument{}, err
}
return policyDocument, nil
}
func (iama *IamApiServer) CreatePolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *CreatePolicyResponse, iamError *IamError) {
resp = &CreatePolicyResponse{}
policyName := values.Get("PolicyName")
policyDocumentString := values.Get("PolicyDocument")
policyDocument, err := GetPolicyDocument(&policyDocumentString)
if err != nil {
return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
}
policyId := Hash(&policyName)
arn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)
resp.CreatePolicyResult.Policy.PolicyName = &policyName
resp.CreatePolicyResult.Policy.Arn = &arn
resp.CreatePolicyResult.Policy.PolicyId = &policyId
policies := Policies{}
// Note: Lock is already held by DoActions, no need to acquire here
if err = iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if policies.Policies == nil {
policies.Policies = make(map[string]policy_engine.PolicyDocument)
}
policies.Policies[policyName] = policyDocument
if err = iama.s3ApiConfig.PutPolicies(&policies); err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
return resp, nil
}
type IamError struct {
Code string
Error error
}
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_PutUserPolicy.html
func (iama *IamApiServer) PutUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *PutUserPolicyResponse, iamError *IamError) {
resp = &PutUserPolicyResponse{}
userName := values.Get("UserName")
policyName := values.Get("PolicyName")
policyDocumentString := values.Get("PolicyDocument")
policyDocument, err := GetPolicyDocument(&policyDocumentString)
if err != nil {
return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
}
if _, err := GetActions(&policyDocument); err != nil {
return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
}
// Verify the user exists before persisting the policy
var targetIdent *iam_pb.Identity
for _, ident := range s3cfg.Identities {
if ident.Name == userName {
targetIdent = ident
break
}
}
if targetIdent == nil {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("the user with name %s cannot be found", userName)}
}
// Persist inline policy to storage using per-user indexed structure
policies := Policies{}
if err = iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
userPolicies := policies.getOrCreateUserPolicies(userName)
userPolicies[policyName] = policyDocument
if err = iama.s3ApiConfig.PutPolicies(&policies); err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
// Recompute aggregated actions (inline + managed)
aggregatedActions, computeErr := computeAllActionsForUser(iama, userName, &policies, targetIdent, s3cfg)
if computeErr != nil {
glog.Warningf("Failed to compute aggregated actions for user %s: %v; keeping existing actions", userName, computeErr)
} else {
targetIdent.Actions = aggregatedActions
}
return resp, nil
}
func (iama *IamApiServer) GetUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *GetUserPolicyResponse, err *IamError) {
resp = &GetUserPolicyResponse{}
userName := values.Get("UserName")
policyName := values.Get("PolicyName")
for _, ident := range s3cfg.Identities {
if userName != ident.Name {
continue
}
resp.GetUserPolicyResult.UserName = userName
resp.GetUserPolicyResult.PolicyName = policyName
// Try to retrieve stored inline policy from persistent storage using per-user index
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
// Propagate storage errors
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
// Direct O(1) access to user's policy using per-user index
if userPolicies := policies.InlinePolicies[userName]; userPolicies != nil {
if policyDocument, exists := userPolicies[policyName]; exists {
policyDocumentJSON, err := json.Marshal(policyDocument)
if err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
resp.GetUserPolicyResult.PolicyDocument = string(policyDocumentJSON)
return resp, nil
} else {
// User's inline policies exist but this specific policy does not
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: errors.New("policy not found")}
}
}
if len(ident.Actions) == 0 {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: errors.New("no actions found")}
}
policyDocument := policy_engine.PolicyDocument{Version: policyDocumentVersion}
statements := make(map[string][]string)
seenAction := make(map[string]map[string]bool)
for _, action := range ident.Actions {
// parse "Read:EXAMPLE-BUCKET" or "Read:EXAMPLE-BUCKET/prefix/*"
// Use SplitN so the path component (which may contain ':') is preserved intact.
act := strings.SplitN(action, ":", 2)
resource := "*"
if len(act) == 2 {
// Preserve the stored path verbatim so bucket-level and
// object-level resources remain distinguishable. GetActions
// stores the path exactly as parsed from the original ARN
// (e.g. "b-le*" for the bucket, "b-le*/*" for objects), and
// reconstruction should not rewrite one into the other.
resource = fmt.Sprintf("arn:aws:s3:::%s", act[1])
}
s3Action := fmt.Sprintf("s3:%s", MapToIdentitiesAction(act[0]))
// Dedupe actions per resource: the Read/Write/List internal verbs map to
// coarse wildcards (s3:Get*, s3:Put*, s3:List*), so multiple distinct
// original actions can collapse to the same reconstructed verb.
if seenAction[resource] == nil {
seenAction[resource] = make(map[string]bool)
}
if seenAction[resource][s3Action] {
continue
}
seenAction[resource][s3Action] = true
statements[resource] = append(statements[resource], s3Action)
}
for resource, actions := range statements {
isEqAction := false
for i, statement := range policyDocument.Statement {
// Use order-independent comparison to avoid duplicates from different action orderings
if stringSlicesEqual(statement.Action.Strings(), actions) {
policyDocument.Statement[i].Resource = policy_engine.NewStringOrStringSlicePtr(append(
policyDocument.Statement[i].Resource.Strings(), resource)...)
isEqAction = true
break
}
}
if isEqAction {
continue
}
policyDocumentStatement := policy_engine.PolicyStatement{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice(actions...),
Resource: policy_engine.NewStringOrStringSlicePtr(resource),
}
policyDocument.Statement = append(policyDocument.Statement, policyDocumentStatement)
}
policyDocumentJSON, err := json.Marshal(policyDocument)
if err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
resp.GetUserPolicyResult.PolicyDocument = string(policyDocumentJSON)
return resp, nil
}
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
// DeleteUserPolicy removes the inline policy from a user (clears their actions).
func (iama *IamApiServer) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DeleteUserPolicyResponse, err *IamError) {
resp = &DeleteUserPolicyResponse{}
userName := values.Get("UserName")
policyName := values.Get("PolicyName")
// First, verify the user exists in identities before modifying storage
var targetIdent *iam_pb.Identity
for _, ident := range s3cfg.Identities {
if ident.Name == userName {
targetIdent = ident
break
}
}
if targetIdent == nil {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
// User exists; now proceed with removing the stored inline policy from persistent storage
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
// Propagate storage errors immediately
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
// Direct O(1) access to user's policy map using per-user index
if userPolicies := policies.InlinePolicies[userName]; userPolicies != nil {
delete(userPolicies, policyName)
// Note: userPolicies is a map, so the delete modifies the map in policies.InlinePolicies[userName]
if err := iama.s3ApiConfig.PutPolicies(&policies); err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
}
// Recompute aggregated actions from remaining inline + managed policies
aggregatedActions, computeErr := computeAllActionsForUser(iama, userName, &policies, targetIdent, s3cfg)
if computeErr != nil {
glog.Warningf("Failed to recompute aggregated actions for user %s: %v; keeping existing actions", userName, computeErr)
} else {
targetIdent.Actions = aggregatedActions
}
return resp, nil
}
// ListUserPolicies lists the names of inline policies attached to a user.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListUserPolicies.html
func (iama *IamApiServer) ListUserPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListUserPoliciesResponse, iamError *IamError) {
resp = &ListUserPoliciesResponse{}
userName := values.Get("UserName")
if userName == "" {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
}
// Verify the user exists
found := false
for _, ident := range s3cfg.Identities {
if ident.Name == userName {
found = true
break
}
}
if !found {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
// List inline policy names from persistent storage
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if userPolicies := policies.InlinePolicies[userName]; userPolicies != nil {
for policyName := range userPolicies {
resp.ListUserPoliciesResult.PolicyNames = append(resp.ListUserPoliciesResult.PolicyNames, policyName)
}
sort.Strings(resp.ListUserPoliciesResult.PolicyNames)
}
resp.ListUserPoliciesResult.IsTruncated = false
return resp, nil
}
// GetPolicy retrieves a managed policy by ARN.
func (iama *IamApiServer) GetPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *GetPolicyResponse, iamError *IamError) {
resp = &GetPolicyResponse{}
policyArn := values.Get("PolicyArn")
policyName, iamError := parsePolicyArn(policyArn)
if iamError != nil {
return resp, iamError
}
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if _, exists := policies.Policies[policyName]; !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
policyId := Hash(&policyName)
path := "/"
defaultVersionId := policyDefaultVersionId
isAttachable := true
resp.GetPolicyResult.Policy.PolicyName = &policyName
resp.GetPolicyResult.Policy.Arn = &policyArn
resp.GetPolicyResult.Policy.PolicyId = &policyId
resp.GetPolicyResult.Policy.Path = &path
resp.GetPolicyResult.Policy.DefaultVersionId = &defaultVersionId
resp.GetPolicyResult.Policy.IsAttachable = &isAttachable
return resp, nil
}
// DeletePolicy removes a managed policy. Rejects deletion if the policy is still attached to any user
// (matching AWS IAM behavior: must detach before deleting).
func (iama *IamApiServer) DeletePolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DeletePolicyResponse, iamError *IamError) {
resp = &DeletePolicyResponse{}
policyArn := values.Get("PolicyArn")
policyName, iamError := parsePolicyArn(policyArn)
if iamError != nil {
return resp, iamError
}
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if _, exists := policies.Policies[policyName]; !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
// Reject deletion if the policy is still attached to any user
for _, ident := range s3cfg.Identities {
for _, name := range ident.PolicyNames {
if name == policyName {
return resp, &IamError{
Code: iam.ErrCodeDeleteConflictException,
Error: fmt.Errorf("policy %s is still attached to user %s", policyName, ident.Name),
}
}
}
}
// Reject deletion if the policy is attached to any group
if groupName, attached := isPolicyAttachedToAnyGroup(s3cfg, policyName); attached {
return resp, &IamError{
Code: iam.ErrCodeDeleteConflictException,
Error: fmt.Errorf("policy %s is still attached to group %s", policyName, groupName),
}
}
delete(policies.Policies, policyName)
if err := iama.s3ApiConfig.PutPolicies(&policies); err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
return resp, nil
}
// ListPolicies lists all managed policies.
func (iama *IamApiServer) ListPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListPoliciesResponse, iamError *IamError) {
resp = &ListPoliciesResponse{}
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
for policyName := range policies.Policies {
name := policyName
arn := fmt.Sprintf("arn:aws:iam:::policy/%s", name)
policyId := Hash(&name)
resp.ListPoliciesResult.Policies = append(resp.ListPoliciesResult.Policies, &iam.Policy{
PolicyName: &name,
Arn: &arn,
PolicyId: &policyId,
})
}
return resp, nil
}
// CreatePolicyVersion replaces a managed policy's document with a new version.
// SeaweedFS keeps a single current document per managed policy (no version
// history), so the new document always becomes version "v1" / the default. This
// is what the AWS Terraform provider calls to update an aws_iam_policy in place.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicyVersion.html
func (iama *IamApiServer) CreatePolicyVersion(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *CreatePolicyVersionResponse, iamError *IamError) {
resp = &CreatePolicyVersionResponse{}
policyArn := values.Get("PolicyArn")
policyName, iamError := parsePolicyArn(policyArn)
if iamError != nil {
return resp, iamError
}
policyDocumentString := values.Get("PolicyDocument")
if policyDocumentString == "" {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyDocument is required")}
}
// SeaweedFS stores a single, always-default managed policy version. On AWS,
// SetAsDefault=false stages a non-default version without activating it; we
// can't honor that, so reject it rather than silently changing permissions.
if !strings.EqualFold(values.Get("SetAsDefault"), "true") {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("SetAsDefault must be true: SeaweedFS stores a single managed policy version")}
}
policyDocument, err := GetPolicyDocument(&policyDocumentString)
if err != nil {
return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
}
if _, err := GetActions(&policyDocument); err != nil {
return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
}
policies := Policies{}
if err = iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if _, exists := policies.Policies[policyName]; !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
policies.Policies[policyName] = policyDocument
if err = iama.s3ApiConfig.PutPolicies(&policies); err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
// The denormalized Identity.Actions caches were derived from the old
// document; refresh every identity that has this managed policy attached
// (directly or via group membership) so the new document takes effect.
recomputeActionsForManagedPolicy(iama, s3cfg, &policies, policyName)
versionId := policyDefaultVersionId
isDefault := true
document := policyDocumentString
resp.CreatePolicyVersionResult.PolicyVersion = iam.PolicyVersion{
VersionId: &versionId,
IsDefaultVersion: &isDefault,
Document: &document,
}
return resp, nil
}
// ListPolicyVersions lists the versions of a managed policy. SeaweedFS stores a
// single current document per policy, so exactly one version ("v1", the
// default) is reported.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListPolicyVersions.html
func (iama *IamApiServer) ListPolicyVersions(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListPolicyVersionsResponse, iamError *IamError) {
resp = &ListPolicyVersionsResponse{}
policyName, iamError := parsePolicyArn(values.Get("PolicyArn"))
if iamError != nil {
return resp, iamError
}
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if _, exists := policies.Policies[policyName]; !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
versionId := policyDefaultVersionId
isDefault := true
resp.ListPolicyVersionsResult.Versions = []*iam.PolicyVersion{{
VersionId: &versionId,
IsDefaultVersion: &isDefault,
}}
resp.ListPolicyVersionsResult.IsTruncated = false
return resp, nil
}
// GetPolicyVersion returns the document for a managed policy version. Only the
// single stored version ("v1") exists.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetPolicyVersion.html
func (iama *IamApiServer) GetPolicyVersion(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *GetPolicyVersionResponse, iamError *IamError) {
resp = &GetPolicyVersionResponse{}
policyName, iamError := parsePolicyArn(values.Get("PolicyArn"))
if iamError != nil {
return resp, iamError
}
versionId := values.Get("VersionId")
if versionId == "" {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("VersionId is required")}
}
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
policyDocument, exists := policies.Policies[policyName]
if !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
if versionId != policyDefaultVersionId {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy version %s not found", versionId)}
}
policyDocumentJSON, err := json.Marshal(policyDocument)
if err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
isDefault := true
document := string(policyDocumentJSON)
resp.GetPolicyVersionResult.PolicyVersion = iam.PolicyVersion{
VersionId: &versionId,
IsDefaultVersion: &isDefault,
Document: &document,
}
return resp, nil
}
// DeletePolicyVersion is accepted for API completeness. With a single stored
// version that is always the default, the only valid responses are NoSuchEntity
// (unknown version) and the AWS "cannot delete the default version" conflict.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeletePolicyVersion.html
func (iama *IamApiServer) DeletePolicyVersion(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DeletePolicyVersionResponse, iamError *IamError) {
resp = &DeletePolicyVersionResponse{}
policyName, iamError := parsePolicyArn(values.Get("PolicyArn"))
if iamError != nil {
return resp, iamError
}
versionId := values.Get("VersionId")
if versionId == "" {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("VersionId is required")}
}
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if _, exists := policies.Policies[policyName]; !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
if versionId == policyDefaultVersionId {
return resp, &IamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("cannot delete the default version of policy %s", policyName)}
}
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy version %s not found", versionId)}
}
// recomputeActionsForManagedPolicy refreshes Identity.Actions for every identity
// affected by a change to the managed policy named policyName: users with the
// policy attached directly, and members of groups the policy is attached to.
func recomputeActionsForManagedPolicy(iama *IamApiServer, s3cfg *iam_pb.S3ApiConfiguration, policies *Policies, policyName string) {
affected := make(map[string]*iam_pb.Identity)
identIndex := make(map[string]*iam_pb.Identity, len(s3cfg.Identities))
for _, ident := range s3cfg.Identities {
identIndex[ident.Name] = ident
for _, pn := range ident.PolicyNames {
if pn == policyName {
affected[ident.Name] = ident
break
}
}
}
for _, g := range s3cfg.Groups {
attached := false
for _, pn := range g.PolicyNames {
if pn == policyName {
attached = true
break
}
}
if !attached {
continue
}
for _, member := range g.Members {
if ident, ok := identIndex[member]; ok {
affected[member] = ident
}
}
}
for name, ident := range affected {
aggregatedActions, err := computeAllActionsForUser(iama, name, policies, ident, s3cfg)
if err != nil {
glog.Warningf("Failed to recompute actions for user %s after managed policy %s change: %v", name, policyName, err)
continue
}
ident.Actions = aggregatedActions
}
}
// AttachUserPolicy attaches a managed policy to a user.
func (iama *IamApiServer) AttachUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *AttachUserPolicyResponse, iamError *IamError) {
resp = &AttachUserPolicyResponse{}
userName := values.Get("UserName")
policyArn := values.Get("PolicyArn")
policyName, iamError := parsePolicyArn(policyArn)
if iamError != nil {
return resp, iamError
}
// Verify managed policy exists
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
if _, exists := policies.Policies[policyName]; !exists {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
}
// Find user and attach policy
for _, ident := range s3cfg.Identities {
if ident.Name != userName {
continue
}
// Check if already attached
for _, name := range ident.PolicyNames {
if name == policyName {
return resp, nil // Already attached, idempotent
}
}
prevPolicyNames := ident.PolicyNames
ident.PolicyNames = append(ident.PolicyNames, policyName)
// Recompute aggregated actions (inline + managed + group)
aggregatedActions, err := computeAllActionsForUser(iama, userName, &policies, ident, s3cfg)
if err != nil {
// Roll back PolicyNames to keep identity consistent
ident.PolicyNames = prevPolicyNames
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to compute actions after attaching policy: %w", err)}
}
ident.Actions = aggregatedActions
return resp, nil
}
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
// DetachUserPolicy detaches a managed policy from a user.
func (iama *IamApiServer) DetachUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DetachUserPolicyResponse, iamError *IamError) {
resp = &DetachUserPolicyResponse{}
userName := values.Get("UserName")
policyArn := values.Get("PolicyArn")
policyName, iamError := parsePolicyArn(policyArn)
if iamError != nil {
return resp, iamError
}
for _, ident := range s3cfg.Identities {
if ident.Name != userName {
continue
}
// Find and remove policy name from the list
prevPolicyNames := make([]string, len(ident.PolicyNames))
copy(prevPolicyNames, ident.PolicyNames)
found := false
for i, name := range ident.PolicyNames {
if name == policyName {
ident.PolicyNames = append(ident.PolicyNames[:i], ident.PolicyNames[i+1:]...)
found = true
break
}
}
if !found {
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s is not attached to user %s", policyName, userName)}
}
// Recompute aggregated actions (inline + managed)
policies := Policies{}
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
// Roll back PolicyNames on storage error
ident.PolicyNames = prevPolicyNames
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
aggregatedActions, err := computeAllActionsForUser(iama, userName, &policies, ident, s3cfg)
if err != nil {
// Roll back PolicyNames to keep identity consistent
ident.PolicyNames = prevPolicyNames
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to compute actions after detaching policy: %w", err)}
}
ident.Actions = aggregatedActions
return resp, nil
}
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
// ListAttachedUserPolicies lists the managed policies attached to a user.
func (iama *IamApiServer) ListAttachedUserPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListAttachedUserPoliciesResponse, iamError *IamError) {
resp = &ListAttachedUserPoliciesResponse{}
userName := values.Get("UserName")
for _, ident := range s3cfg.Identities {
if ident.Name != userName {
continue
}
for _, policyName := range ident.PolicyNames {
name := policyName
arn := fmt.Sprintf("arn:aws:iam:::policy/%s", name)
resp.ListAttachedUserPoliciesResult.AttachedPolicies = append(
resp.ListAttachedUserPoliciesResult.AttachedPolicies,
&iam.AttachedPolicy{PolicyName: &name, PolicyArn: &arn},
)
}
return resp, nil
}
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
// computeAllActionsForUser computes the union of actions from user inline policies,
// user managed policies, group inline policies, and group managed policies.
// If s3cfg is provided, group memberships are resolved to include group policies.
func computeAllActionsForUser(iama *IamApiServer, userName string, policies *Policies, ident *iam_pb.Identity, s3cfgs ...*iam_pb.S3ApiConfiguration) ([]string, error) {
actionSet := make(map[string]bool)
var aggregatedActions []string
addUniqueActions := func(actions []string) {
for _, action := range actions {
if !actionSet[action] {
actionSet[action] = true
aggregatedActions = append(aggregatedActions, action)
}
}
}
// Include user inline policy actions
inlineActions, err := computeAggregatedActionsForUser(iama, userName, policies)
if err != nil {
return nil, err
}
addUniqueActions(inlineActions)
// Include user managed policy actions
for _, policyName := range ident.PolicyNames {
if policyDoc, exists := policies.Policies[policyName]; exists {
actions, err := GetActions(&policyDoc)
if err != nil {
glog.Warningf("Failed to get actions from managed policy '%s' for user %s: %v", policyName, userName, err)
continue
}
addUniqueActions(actions)
}
}
// Include group policies (both inline and managed) if s3cfg is available
if len(s3cfgs) > 0 && s3cfgs[0] != nil {
s3cfg := s3cfgs[0]
for _, g := range s3cfg.Groups {
if g.Disabled {
continue
}
isMember := false
for _, m := range g.Members {
if m == userName {
isMember = true
break
}
}
if !isMember {
continue
}
// Group managed policies
for _, policyName := range g.PolicyNames {
if policyDoc, exists := policies.Policies[policyName]; exists {
actions, err := GetActions(&policyDoc)
if err != nil {
glog.Warningf("Failed to get actions from group managed policy '%s' (group %s) for user %s: %v", policyName, g.Name, userName, err)
continue
}
addUniqueActions(actions)
}
}
// Group inline policies
if groupPolicies := policies.GroupInlinePolicies[g.Name]; groupPolicies != nil {
for policyName, policyDoc := range groupPolicies {
actions, err := GetActions(&policyDoc)
if err != nil {
glog.Warningf("Failed to get actions from group inline policy '%s' (group %s) for user %s: %v", policyName, g.Name, userName, err)
continue
}
addUniqueActions(actions)
}
}
}
}
return aggregatedActions, nil
}
func GetActions(policy *policy_engine.PolicyDocument) ([]string, error) {
var actions []string
for _, statement := range policy.Statement {
if statement.Effect != policy_engine.PolicyEffectAllow {
return nil, fmt.Errorf("not a valid effect: '%s'. Only 'Allow' is possible", statement.Effect)
}
for _, resource := range statement.Resource.Strings() {
// Parse "arn:aws:s3:::my-bucket/shared/*"
res := strings.Split(resource, ":")
if len(res) != 6 || res[0] != "arn" || res[1] != "aws" || res[2] != "s3" {
continue
}
for _, action := range statement.Action.Strings() {
// Parse "s3:Get*"
act := strings.Split(action, ":")
if len(act) != 2 || act[0] != "s3" {
continue
}
statementAction := MapToStatementAction(act[1])
if statementAction == "" {
return nil, fmt.Errorf("not a valid action: '%s'", act[1])
}
path := res[5]
if path == "*" {
actions = append(actions, statementAction)
continue
}
actions = append(actions, fmt.Sprintf("%s:%s", statementAction, path))
}
}
}
return actions, nil
}
func (iama *IamApiServer) CreateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *CreateAccessKeyResponse, iamErr *IamError) {
resp = &CreateAccessKeyResponse{}
userName := values.Get("UserName")
status := iam.StatusTypeActive
accessKeyId := values.Get("AccessKeyId")
secretAccessKey := values.Get("SecretAccessKey")
if accessKeyId != "" {
if err := iamlib.ValidateCallerSuppliedAccessKeyId(accessKeyId); err != nil {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: err}
}
}
if secretAccessKey != "" {
if err := iamlib.ValidateCallerSuppliedSecretAccessKey(secretAccessKey); err != nil {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: err}
}
}
if (accessKeyId != "") != (secretAccessKey != "") {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("AccessKeyId and SecretAccessKey must be supplied together")}
}
if owner := iamlib.FindAccessKeyOwner(s3cfg, accessKeyId); owner != nil {
glog.V(4).Infof("CreateAccessKey: supplied AccessKeyId already in use by %s %s", owner.Type, owner.Name)
return resp, &IamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("AccessKeyId is already in use")}
}
if accessKeyId == "" {
var err error
accessKeyId, err = StringWithCharset(21, charsetUpper)
if err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate access key: %w", err)}
}
}
if secretAccessKey == "" {
var err error
secretAccessKey, err = StringWithCharset(42, charset)
if err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate secret key: %w", err)}
}
}
resp.CreateAccessKeyResult.AccessKey.AccessKeyId = &accessKeyId
resp.CreateAccessKeyResult.AccessKey.SecretAccessKey = &secretAccessKey
resp.CreateAccessKeyResult.AccessKey.UserName = &userName
resp.CreateAccessKeyResult.AccessKey.Status = &status
changed := false
for _, ident := range s3cfg.Identities {
if userName == ident.Name {
ident.Credentials = append(ident.Credentials,
&iam_pb.Credential{AccessKey: accessKeyId, SecretKey: secretAccessKey, Status: accessKeyStatusActive})
changed = true
break
}
}
if !changed {
s3cfg.Identities = append(s3cfg.Identities,
&iam_pb.Identity{
Name: userName,
Credentials: []*iam_pb.Credential{
{
AccessKey: accessKeyId,
SecretKey: secretAccessKey,
Status: accessKeyStatusActive,
},
},
},
)
}
return resp, nil
}
// UpdateAccessKey updates the status of an access key (Active or Inactive).
func (iama *IamApiServer) UpdateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *UpdateAccessKeyResponse, err *IamError) {
resp = &UpdateAccessKeyResponse{}
userName := values.Get("UserName")
accessKeyId := values.Get("AccessKeyId")
status := values.Get("Status")
if userName == "" {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
}
if accessKeyId == "" {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("AccessKeyId is required")}
}
if err := validateAccessKeyStatus(status); err != nil {
return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: err}
}
for _, ident := range s3cfg.Identities {
if ident.Name != userName {
continue
}
for _, cred := range ident.Credentials {
if cred.AccessKey == accessKeyId {
cred.Status = status
return resp, nil
}
}
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("the access key with id %s for user %s cannot be found", accessKeyId, userName)}
}
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
func (iama *IamApiServer) DeleteAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DeleteAccessKeyResponse) {
resp = &DeleteAccessKeyResponse{}
userName := values.Get("UserName")
accessKeyId := values.Get("AccessKeyId")
for _, ident := range s3cfg.Identities {
if userName == ident.Name {
for i, cred := range ident.Credentials {
if cred.AccessKey == accessKeyId {
ident.Credentials = append(ident.Credentials[:i], ident.Credentials[i+1:]...)
break
}
}
break
}
}
return resp
}
// handleImplicitUsername adds username who signs the request to values if 'username' is not specified.
// According to AWS documentation: "If you do not specify a user name, IAM determines the user name
// implicitly based on the Amazon Web Services access key ID signing the request."
// This function extracts the AccessKeyId from the SigV4 credential and looks up the corresponding
// identity name in the credential store.
func (iama *IamApiServer) handleImplicitUsername(r *http.Request, values url.Values) {
if len(r.Header["Authorization"]) == 0 || values.Get("UserName") != "" {
return
}
// Log presence of auth header without exposing sensitive signature material
glog.V(4).Infof("Authorization header present, extracting access key")
// Parse AWS SigV4 Authorization header format:
// "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/iam/aws4_request, ..."
s := strings.Split(r.Header["Authorization"][0], "Credential=")
if len(s) < 2 {
return
}
s = strings.Split(s[1], ",")
if len(s) < 1 {
return
}
s = strings.Split(s[0], "/")
if len(s) < 1 {
return
}
// s[0] is the AccessKeyId
accessKeyId := s[0]
if accessKeyId == "" {
return
}
// Nil-guard: ensure iam is initialized before lookup
if iama.iam == nil {
glog.V(4).Infof("IAM not initialized, cannot look up access key")
return
}
// Look up the identity by access key to get the username
identity, _, found := iama.iam.LookupByAccessKey(accessKeyId)
if !found {
// Mask access key in logs - show only first 4 chars
maskedKey := accessKeyId
if len(accessKeyId) > 4 {
maskedKey = accessKeyId[:4] + "***"
}
glog.V(4).Infof("Access key %s not found in credential store", maskedKey)
return
}
values.Set("UserName", identity.Name)
}
func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) {
// Lock to prevent concurrent read-modify-write race conditions
policyLock.Lock()
defer policyLock.Unlock()
r, reqID := request_id.Ensure(r)
if err := r.ParseForm(); err != nil {
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
return
}
values := r.PostForm
s3cfg := &iam_pb.S3ApiConfiguration{}
if err := iama.s3ApiConfig.GetS3ApiConfiguration(s3cfg); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
return
}
glog.V(4).Infof("DoActions: %+v", iamlib.RedactSensitiveFormValues(values))
var response iamlib.RequestIDSetter
changed := true
switch r.Form.Get("Action") {
case "ListUsers":
response = iama.ListUsers(s3cfg, values)
changed = false
case "ListAccessKeys":
iama.handleImplicitUsername(r, values)
response = iama.ListAccessKeys(s3cfg, values)
changed = false
case "CreateUser":
response = iama.CreateUser(s3cfg, values)
// Use targeted create to avoid rewriting all existing user files via SaveConfiguration.
// credentialManager.CreateUser writes only the new user's file.
if iama.iam != nil {
if cm := iama.iam.GetCredentialManager(); cm != nil {
userName := values.Get("UserName")
if err := cm.CreateUser(r.Context(), &iam_pb.Identity{Name: userName}); err != nil {
writeIamErrorResponse(w, r, reqID, &IamError{Code: s3api.CredentialErrToIamErrCode(err), Error: err})
return
}
changed = false
}
}
case "GetUser":
userName := values.Get("UserName")
var err *IamError
response, err = iama.GetUser(s3cfg, userName)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "UpdateUser":
var err *IamError
response, err = iama.UpdateUser(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "DeleteUser":
userName := values.Get("UserName")
var err *IamError
response, err = iama.DeleteUser(s3cfg, userName)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "CreateAccessKey":
iama.handleImplicitUsername(r, values)
var err *IamError
response, err = iama.CreateAccessKey(s3cfg, values)
if err != nil {
glog.Errorf("CreateAccessKey: %+v", err.Error)
writeIamErrorResponse(w, r, reqID, err)
return
}
case "DeleteAccessKey":
iama.handleImplicitUsername(r, values)
response = iama.DeleteAccessKey(s3cfg, values)
case "UpdateAccessKey":
iama.handleImplicitUsername(r, values)
var err *IamError
response, err = iama.UpdateAccessKey(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "CreatePolicy":
var err *IamError
response, err = iama.CreatePolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
// CreatePolicy persists the policy document via iama.s3ApiConfig.PutPolicies().
// The `changed` flag is false because this does not modify the main s3cfg.Identities configuration.
changed = false
case "PutUserPolicy":
var err *IamError
response, err = iama.PutUserPolicy(s3cfg, values)
if err != nil {
glog.Errorf("PutUserPolicy: %+v", err.Error)
writeIamErrorResponse(w, r, reqID, err)
return
}
case "GetUserPolicy":
var err *IamError
response, err = iama.GetUserPolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "DeleteUserPolicy":
var err *IamError
response, err = iama.DeleteUserPolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "ListUserPolicies":
iama.handleImplicitUsername(r, values)
var err *IamError
response, err = iama.ListUserPolicies(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "GetPolicy":
var err *IamError
response, err = iama.GetPolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "DeletePolicy":
var err *IamError
response, err = iama.DeletePolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "ListPolicies":
var err *IamError
response, err = iama.ListPolicies(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "CreatePolicyVersion":
var err *IamError
response, err = iama.CreatePolicyVersion(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
// CreatePolicyVersion persists the new document via PutPolicies and
// recomputes affected Identity.Actions on s3cfg; keep changed=true so the
// updated identities are saved.
case "ListPolicyVersions":
var err *IamError
response, err = iama.ListPolicyVersions(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "GetPolicyVersion":
var err *IamError
response, err = iama.GetPolicyVersion(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "DeletePolicyVersion":
var err *IamError
response, err = iama.DeletePolicyVersion(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "AttachUserPolicy":
var err *IamError
response, err = iama.AttachUserPolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "DetachUserPolicy":
var err *IamError
response, err = iama.DetachUserPolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "ListAttachedUserPolicies":
var err *IamError
response, err = iama.ListAttachedUserPolicies(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
// Group actions
case "CreateGroup":
var err *IamError
response, err = iama.CreateGroup(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "DeleteGroup":
var err *IamError
response, err = iama.DeleteGroup(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "UpdateGroup":
var err *IamError
response, err = iama.UpdateGroup(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "GetGroup":
var err *IamError
response, err = iama.GetGroup(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "ListGroups":
response = iama.ListGroups(s3cfg, values)
changed = false
case "AddUserToGroup":
var err *IamError
response, err = iama.AddUserToGroup(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "RemoveUserFromGroup":
var err *IamError
response, err = iama.RemoveUserFromGroup(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "AttachGroupPolicy":
var err *IamError
response, err = iama.AttachGroupPolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "DetachGroupPolicy":
var err *IamError
response, err = iama.DetachGroupPolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
case "ListAttachedGroupPolicies":
var err *IamError
response, err = iama.ListAttachedGroupPolicies(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "PutGroupPolicy":
var err *IamError
response, err = iama.PutGroupPolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
// changed = true: PutGroupPolicy recomputes member Identity.Actions
case "GetGroupPolicy":
var err *IamError
response, err = iama.GetGroupPolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "DeleteGroupPolicy":
var err *IamError
response, err = iama.DeleteGroupPolicy(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
// changed = true: DeleteGroupPolicy recomputes member Identity.Actions
case "ListGroupPolicies":
var err *IamError
response, err = iama.ListGroupPolicies(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
case "ListGroupsForUser":
var err *IamError
response, err = iama.ListGroupsForUser(s3cfg, values)
if err != nil {
writeIamErrorResponse(w, r, reqID, err)
return
}
changed = false
default:
errNotImplemented := s3err.GetAPIError(s3err.ErrNotImplemented)
errorResponse := newErrorResponse(errNotImplemented.Code, errNotImplemented.Description, reqID)
s3err.WriteXMLResponse(w, r, errNotImplemented.HTTPStatusCode, errorResponse)
return
}
if changed {
err := iama.s3ApiConfig.PutS3ApiConfiguration(s3cfg)
if err != nil {
var iamError = IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
writeIamErrorResponse(w, r, reqID, &iamError)
return
}
// Reload in-memory identity maps so subsequent LookupByAccessKey calls
// can see newly created or deleted keys immediately
if iama.iam != nil {
if err := iama.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil {
glog.Warningf("Failed to reload IAM configuration after mutation: %v", err)
// Don't fail the request since the persistent save succeeded
}
}
}
response.SetRequestId(reqID)
s3err.WriteXMLResponse(w, r, http.StatusOK, response)
}