mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-12 09:17:44 +02:00
* s3err: add InvalidArgument and AuthorizationHeaderMalformed codes Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: answer unrecognized bucket PUT sub-resources with 501 A PUT on a bucket carrying an unrecognized query (logging, metrics, intelligent-tiering, ...) fell through to the bare CreateBucket route and returned BucketAlreadyOwnedByYou or re-created the bucket. AWS answers these with NotImplemented. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: reject malformed copy-source and multipart PUT parameters A malformed X-Amz-Copy-Source or a non-numeric partNumber fell through to the plain PutObject route and stored the body as a regular object. Answer them with InvalidArgument-class errors instead of writing data. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: verify x-amz-content-sha256 against the streamed body A PUT carrying a hex or base64 payload hash now streams through a verifier that reports a mismatch once the stream is exhausted, instead of storing an object that does not match its declared hash. The error is deferred so intermediate reads that drop (n>0, err) results cannot silently swallow it. Sentinel values (unsigned/streaming payloads) remain exempt and malformed values fail fast. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: map truncated SigV4 headers to the error for the missing field AWS answers an Authorization header missing Credential= with InvalidArgument and one missing or malformed Signature= with AuthorizationHeaderMalformed, instead of a generic MissingFields. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: answer IAM/STS failures in the query-protocol envelope Embedded IAM and STS routes now report authentication, form-parse and authorization failures with the IAM ErrorResponse body instead of the S3 Error envelope, so IAM SDK clients can parse them. Requests signed for s3 keep the S3 envelope, keyed off the credential scope. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: return 403 AccessDenied when the request has no Date Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: answer throttling rejections as SlowDown ErrTooManyRequest and ErrRequestBytesExceed reported made-up codes; AWS serves these throttling rejections as SlowDown. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: reject versionId requests on buckets that never had versioning GET, HEAD and DELETE carrying a non-empty versionId on an unversioned bucket now fail with InvalidArgument instead of being answered as a plain object request. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: answer DeleteObjects over 1000 keys with MalformedXML Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: InvalidArgument for non-numeric or out-of-range part numbers partNumber=abc, 0 and >10000 all resolve to InvalidArgument, matching AWS, instead of InvalidPart. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * iam: correct LimitExceeded, InvalidAction and ServiceFailure mappings LimitExceeded is a conflict (409), an unknown Action is InvalidAction (404) rather than NotImplemented, and internal failures report the IAM receiver fault type. Applies to both the embedded IAM endpoint and the standalone iamapi server. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * iam: refuse DeleteUser while access keys remain Deleting a user with live credentials orphaned its access keys; AWS answers DeleteConflict until they are removed first. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test/s3: add S3/IAM error-response compatibility harness * s3: return InvalidArgument for malformed x-amz-content-sha256 A header value that decodes to neither 32-byte hex nor base64 is a malformed argument, not a hash mismatch. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: stop spawned mini when readiness times out A slow-starting server otherwise survives the failure path and keeps the S3 port occupied for the next run. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: register atexit cleanup before setup A failed setup previously skipped cleanup, leaking the bucket and IAM user on persistent servers. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: reject unrouted subresources on the DELETE bucket catch-all PutBucketHandler gained the same guard when the route-level check moved into the handlers; DeleteBucketHandler was missed, so an authorized DELETE /bucket?logging could delete the bucket instead of answering NotImplemented. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: tolerate unset fixture variables in cleanup and cover DELETE ?logging Cleanup now runs its IAM/multipart steps only when setup reached them, so an early setup failure still removes the bucket. Added a DELETE bucket-subresource case asserting NotImplemented and bucket survival. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: fail a case when its side-effect check reports a regression A non-empty check note now fails the case, so a deleted bucket or an object created by a malformed request cannot slip through behind a passing status check. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1657 lines
59 KiB
Go
1657 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 {
|
|
// AWS IAM behavior: prevent deletion if user has dependent resources.
|
|
if len(ident.ServiceAccountIds) > 0 {
|
|
return resp, &IamError{
|
|
Code: iam.ErrCodeDeleteConflictException,
|
|
Error: fmt.Errorf("cannot delete user %s: user has %d service account(s). Delete service accounts first", userName, len(ident.ServiceAccountIds)),
|
|
}
|
|
}
|
|
if len(ident.Credentials) > 0 {
|
|
return resp, &IamError{
|
|
Code: iam.ErrCodeDeleteConflictException,
|
|
Error: fmt.Errorf("cannot delete user %s: user has %d access key(s). Delete access keys first", userName, len(ident.Credentials)),
|
|
}
|
|
}
|
|
// 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:
|
|
errorResponse := newErrorResponse("InvalidAction", fmt.Sprintf("The action %s is not valid for this endpoint", values.Get("Action")), reqID)
|
|
s3err.WriteXMLResponse(w, r, http.StatusNotFound, 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)
|
|
}
|