You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
736 lines
26 KiB
736 lines
26 KiB
package s3api
|
|
|
|
// This file provides IAM API functionality embedded in the S3 server.
|
|
// Common IAM types and helpers are imported from the shared weed/iam package.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/seaweedfs/seaweedfs/weed/credential"
|
|
"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/policy_engine"
|
|
. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// EmbeddedIamApi provides IAM API functionality embedded in the S3 server.
|
|
// This allows running a single server that handles both S3 and IAM requests.
|
|
type EmbeddedIamApi struct {
|
|
credentialManager *credential.CredentialManager
|
|
iam *IdentityAccessManagement
|
|
policyLock sync.RWMutex
|
|
}
|
|
|
|
// NewEmbeddedIamApi creates a new embedded IAM API handler.
|
|
func NewEmbeddedIamApi(credentialManager *credential.CredentialManager, iam *IdentityAccessManagement) *EmbeddedIamApi {
|
|
return &EmbeddedIamApi{
|
|
credentialManager: credentialManager,
|
|
iam: iam,
|
|
}
|
|
}
|
|
|
|
// Type aliases for IAM response types from shared package
|
|
type (
|
|
iamCommonResponse = iamlib.CommonResponse
|
|
iamListUsersResponse = iamlib.ListUsersResponse
|
|
iamListAccessKeysResponse = iamlib.ListAccessKeysResponse
|
|
iamDeleteAccessKeyResponse = iamlib.DeleteAccessKeyResponse
|
|
iamCreatePolicyResponse = iamlib.CreatePolicyResponse
|
|
iamCreateUserResponse = iamlib.CreateUserResponse
|
|
iamDeleteUserResponse = iamlib.DeleteUserResponse
|
|
iamGetUserResponse = iamlib.GetUserResponse
|
|
iamUpdateUserResponse = iamlib.UpdateUserResponse
|
|
iamCreateAccessKeyResponse = iamlib.CreateAccessKeyResponse
|
|
iamPutUserPolicyResponse = iamlib.PutUserPolicyResponse
|
|
iamDeleteUserPolicyResponse = iamlib.DeleteUserPolicyResponse
|
|
iamGetUserPolicyResponse = iamlib.GetUserPolicyResponse
|
|
iamErrorResponse = iamlib.ErrorResponse
|
|
iamError = iamlib.Error
|
|
)
|
|
|
|
// Helper function wrappers using shared package
|
|
func iamHash(s *string) string {
|
|
return iamlib.Hash(s)
|
|
}
|
|
|
|
func iamStringWithCharset(length int, charset string) (string, error) {
|
|
return iamlib.GenerateRandomString(length, charset)
|
|
}
|
|
|
|
func iamStringSlicesEqual(a, b []string) bool {
|
|
return iamlib.StringSlicesEqual(a, b)
|
|
}
|
|
|
|
func iamMapToStatementAction(action string) string {
|
|
return iamlib.MapToStatementAction(action)
|
|
}
|
|
|
|
func iamMapToIdentitiesAction(action string) string {
|
|
return iamlib.MapToIdentitiesAction(action)
|
|
}
|
|
|
|
// Constants from shared package
|
|
const (
|
|
iamCharsetUpper = iamlib.CharsetUpper
|
|
iamCharset = iamlib.Charset
|
|
iamPolicyDocumentVersion = iamlib.PolicyDocumentVersion
|
|
iamUserDoesNotExist = iamlib.UserDoesNotExist
|
|
)
|
|
|
|
func newIamErrorResponse(errCode string, errMsg string) iamErrorResponse {
|
|
errorResp := iamErrorResponse{}
|
|
errorResp.Error.Type = "Sender"
|
|
errorResp.Error.Code = &errCode
|
|
errorResp.Error.Message = &errMsg
|
|
return errorResp
|
|
}
|
|
|
|
func (e *EmbeddedIamApi) writeIamErrorResponse(w http.ResponseWriter, r *http.Request, iamErr *iamError) {
|
|
if iamErr == nil {
|
|
glog.Errorf("No error found")
|
|
return
|
|
}
|
|
|
|
errCode := iamErr.Code
|
|
errMsg := iamErr.Error.Error()
|
|
glog.Errorf("IAM Response %+v", errMsg)
|
|
|
|
errorResp := newIamErrorResponse(errCode, errMsg)
|
|
internalErrorResponse := newIamErrorResponse(iam.ErrCodeServiceFailureException, "Internal server error")
|
|
|
|
switch errCode {
|
|
case iam.ErrCodeNoSuchEntityException:
|
|
s3err.WriteXMLResponse(w, r, http.StatusNotFound, errorResp)
|
|
case iam.ErrCodeEntityAlreadyExistsException:
|
|
s3err.WriteXMLResponse(w, r, http.StatusConflict, errorResp)
|
|
case iam.ErrCodeMalformedPolicyDocumentException, iam.ErrCodeInvalidInputException:
|
|
s3err.WriteXMLResponse(w, r, http.StatusBadRequest, errorResp)
|
|
case iam.ErrCodeServiceFailureException:
|
|
s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse)
|
|
default:
|
|
s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse)
|
|
}
|
|
}
|
|
|
|
// GetS3ApiConfiguration loads the S3 API configuration from the credential manager.
|
|
func (e *EmbeddedIamApi) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
|
|
config, err := e.credentialManager.LoadConfiguration(context.Background())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load configuration: %w", err)
|
|
}
|
|
proto.Merge(s3cfg, config)
|
|
return nil
|
|
}
|
|
|
|
// PutS3ApiConfiguration saves the S3 API configuration to the credential manager.
|
|
func (e *EmbeddedIamApi) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
|
|
return e.credentialManager.SaveConfiguration(context.Background(), s3cfg)
|
|
}
|
|
|
|
// ListUsers lists all IAM users.
|
|
func (e *EmbeddedIamApi) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) iamListUsersResponse {
|
|
var resp iamListUsersResponse
|
|
for _, ident := range s3cfg.Identities {
|
|
resp.ListUsersResult.Users = append(resp.ListUsersResult.Users, &iam.User{UserName: &ident.Name})
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// ListAccessKeys lists access keys for a user.
|
|
func (e *EmbeddedIamApi) ListAccessKeys(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) iamListAccessKeysResponse {
|
|
var resp iamListAccessKeysResponse
|
|
status := iam.StatusTypeActive
|
|
userName := values.Get("UserName")
|
|
for _, ident := range s3cfg.Identities {
|
|
if userName != "" && userName != ident.Name {
|
|
continue
|
|
}
|
|
for _, cred := range ident.Credentials {
|
|
resp.ListAccessKeysResult.AccessKeyMetadata = append(resp.ListAccessKeysResult.AccessKeyMetadata,
|
|
&iam.AccessKeyMetadata{UserName: &ident.Name, AccessKeyId: &cred.AccessKey, Status: &status},
|
|
)
|
|
}
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// CreateUser creates a new IAM user.
|
|
func (e *EmbeddedIamApi) CreateUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamCreateUserResponse, *iamError) {
|
|
var resp iamCreateUserResponse
|
|
userName := values.Get("UserName")
|
|
|
|
// Validate UserName is not empty
|
|
if userName == "" {
|
|
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
|
|
}
|
|
|
|
// Check for duplicate user
|
|
for _, ident := range s3cfg.Identities {
|
|
if ident.Name == userName {
|
|
return resp, &iamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("user %s already exists", userName)}
|
|
}
|
|
}
|
|
|
|
resp.CreateUserResult.User.UserName = &userName
|
|
s3cfg.Identities = append(s3cfg.Identities, &iam_pb.Identity{Name: userName})
|
|
return resp, nil
|
|
}
|
|
|
|
// DeleteUser deletes an IAM user.
|
|
func (e *EmbeddedIamApi) DeleteUser(s3cfg *iam_pb.S3ApiConfiguration, userName string) (iamDeleteUserResponse, *iamError) {
|
|
var resp iamDeleteUserResponse
|
|
for i, ident := range s3cfg.Identities {
|
|
if userName == ident.Name {
|
|
s3cfg.Identities = append(s3cfg.Identities[:i], s3cfg.Identities[i+1:]...)
|
|
return resp, nil
|
|
}
|
|
}
|
|
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
|
}
|
|
|
|
// GetUser gets an IAM user.
|
|
func (e *EmbeddedIamApi) GetUser(s3cfg *iam_pb.S3ApiConfiguration, userName string) (iamGetUserResponse, *iamError) {
|
|
var resp iamGetUserResponse
|
|
for _, ident := range s3cfg.Identities {
|
|
if userName == ident.Name {
|
|
resp.GetUserResult.User = iam.User{UserName: &ident.Name}
|
|
return resp, nil
|
|
}
|
|
}
|
|
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
|
}
|
|
|
|
// UpdateUser updates an IAM user.
|
|
func (e *EmbeddedIamApi) UpdateUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamUpdateUserResponse, *iamError) {
|
|
var resp iamUpdateUserResponse
|
|
userName := values.Get("UserName")
|
|
newUserName := values.Get("NewUserName")
|
|
if newUserName != "" {
|
|
for _, ident := range s3cfg.Identities {
|
|
if userName == ident.Name {
|
|
ident.Name = newUserName
|
|
return resp, nil
|
|
}
|
|
}
|
|
} else {
|
|
return resp, nil
|
|
}
|
|
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
|
}
|
|
|
|
// CreateAccessKey creates an access key for a user.
|
|
func (e *EmbeddedIamApi) CreateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamCreateAccessKeyResponse, *iamError) {
|
|
var resp iamCreateAccessKeyResponse
|
|
userName := values.Get("UserName")
|
|
status := iam.StatusTypeActive
|
|
|
|
accessKeyId, err := iamStringWithCharset(21, iamCharsetUpper)
|
|
if err != nil {
|
|
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate access key: %w", err)}
|
|
}
|
|
secretAccessKey, err := iamStringWithCharset(42, iamCharset)
|
|
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
|
|
|
|
for _, ident := range s3cfg.Identities {
|
|
if userName == ident.Name {
|
|
ident.Credentials = append(ident.Credentials,
|
|
&iam_pb.Credential{AccessKey: accessKeyId, SecretKey: secretAccessKey})
|
|
return resp, nil
|
|
}
|
|
}
|
|
// User not found - return error instead of implicitly creating the user
|
|
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
|
}
|
|
|
|
// DeleteAccessKey deletes an access key for a user.
|
|
func (e *EmbeddedIamApi) DeleteAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) iamDeleteAccessKeyResponse {
|
|
var resp iamDeleteAccessKeyResponse
|
|
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
|
|
}
|
|
|
|
// GetPolicyDocument parses a policy document string.
|
|
func (e *EmbeddedIamApi) 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
|
|
}
|
|
|
|
// CreatePolicy validates and creates a new IAM managed policy.
|
|
// NOTE: Currently this only validates the policy document and returns policy metadata.
|
|
// The policy is not persisted to a managed policy store. To apply permissions to a user,
|
|
// use PutUserPolicy which stores the policy inline on the user's identity.
|
|
// TODO: Implement managed policy storage for full AWS IAM compatibility (ListPolicies, GetPolicy, AttachUserPolicy).
|
|
func (e *EmbeddedIamApi) CreatePolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamCreatePolicyResponse, *iamError) {
|
|
var resp iamCreatePolicyResponse
|
|
policyName := values.Get("PolicyName")
|
|
policyDocumentString := values.Get("PolicyDocument")
|
|
_, err := e.GetPolicyDocument(&policyDocumentString)
|
|
if err != nil {
|
|
return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
|
|
}
|
|
policyId := iamHash(&policyDocumentString)
|
|
arn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)
|
|
resp.CreatePolicyResult.Policy.PolicyName = &policyName
|
|
resp.CreatePolicyResult.Policy.Arn = &arn
|
|
resp.CreatePolicyResult.Policy.PolicyId = &policyId
|
|
return resp, nil
|
|
}
|
|
|
|
// getActions extracts actions from a policy document.
|
|
// S3 ARN format: arn:aws:s3:::bucket or arn:aws:s3:::bucket/path/*
|
|
// res[5] contains the bucket and optional path after :::
|
|
func (e *EmbeddedIamApi) 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() {
|
|
res := strings.Split(resource, ":")
|
|
if len(res) != 6 || res[0] != "arn" || res[1] != "aws" || res[2] != "s3" {
|
|
continue
|
|
}
|
|
for _, action := range statement.Action.Strings() {
|
|
act := strings.Split(action, ":")
|
|
if len(act) != 2 || act[0] != "s3" {
|
|
continue
|
|
}
|
|
statementAction := iamMapToStatementAction(act[1])
|
|
if statementAction == "" {
|
|
return nil, fmt.Errorf("not a valid action: '%s'", act[1])
|
|
}
|
|
|
|
resourcePath := res[5]
|
|
if resourcePath == "*" {
|
|
// Wildcard - applies to all buckets
|
|
actions = append(actions, statementAction)
|
|
continue
|
|
}
|
|
|
|
// Parse bucket and optional object path
|
|
// Examples: "mybucket", "mybucket/*", "mybucket/prefix/*"
|
|
bucket, objectPath, hasSep := strings.Cut(resourcePath, "/")
|
|
if bucket == "" {
|
|
continue // Invalid: empty bucket name
|
|
}
|
|
|
|
if !hasSep || objectPath == "" || objectPath == "*" {
|
|
// Bucket-level or bucket/* - use just bucket name
|
|
actions = append(actions, fmt.Sprintf("%s:%s", statementAction, bucket))
|
|
} else {
|
|
// Path-specific: bucket/path/* -> Action:bucket/path
|
|
// Remove trailing /* if present for cleaner action format
|
|
objectPath = strings.TrimSuffix(objectPath, "/*")
|
|
objectPath = strings.TrimSuffix(objectPath, "*")
|
|
if objectPath == "" {
|
|
actions = append(actions, fmt.Sprintf("%s:%s", statementAction, bucket))
|
|
} else {
|
|
actions = append(actions, fmt.Sprintf("%s:%s/%s", statementAction, bucket, objectPath))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(actions) == 0 {
|
|
return nil, fmt.Errorf("no valid actions found in policy document")
|
|
}
|
|
return actions, nil
|
|
}
|
|
|
|
// PutUserPolicy attaches a policy to a user.
|
|
func (e *EmbeddedIamApi) PutUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamPutUserPolicyResponse, *iamError) {
|
|
var resp iamPutUserPolicyResponse
|
|
userName := values.Get("UserName")
|
|
policyDocumentString := values.Get("PolicyDocument")
|
|
policyDocument, err := e.GetPolicyDocument(&policyDocumentString)
|
|
if err != nil {
|
|
return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
|
|
}
|
|
actions, err := e.getActions(&policyDocument)
|
|
if err != nil {
|
|
return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
|
|
}
|
|
glog.V(3).Infof("PutUserPolicy: actions=%v", actions)
|
|
for _, ident := range s3cfg.Identities {
|
|
if userName != ident.Name {
|
|
continue
|
|
}
|
|
ident.Actions = actions
|
|
return resp, nil
|
|
}
|
|
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("the user with name %s cannot be found", userName)}
|
|
}
|
|
|
|
// GetUserPolicy gets the policy attached to a user.
|
|
func (e *EmbeddedIamApi) GetUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamGetUserPolicyResponse, *iamError) {
|
|
var resp iamGetUserPolicyResponse
|
|
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
|
|
if len(ident.Actions) == 0 {
|
|
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: errors.New("no actions found")}
|
|
}
|
|
|
|
policyDocument := policy_engine.PolicyDocument{Version: iamPolicyDocumentVersion}
|
|
statements := make(map[string][]string)
|
|
for _, action := range ident.Actions {
|
|
// Action format: "ActionType" (global) or "ActionType:bucket" or "ActionType:bucket/path"
|
|
actionType, bucketPath, hasPath := strings.Cut(action, ":")
|
|
var resource string
|
|
if !hasPath {
|
|
// Global action (no bucket specified)
|
|
resource = "*"
|
|
} else if strings.Contains(bucketPath, "/") {
|
|
// Path-specific: bucket/path -> arn:aws:s3:::bucket/path/*
|
|
resource = fmt.Sprintf("arn:aws:s3:::%s/*", bucketPath)
|
|
} else {
|
|
// Bucket-level: bucket -> arn:aws:s3:::bucket/*
|
|
resource = fmt.Sprintf("arn:aws:s3:::%s/*", bucketPath)
|
|
}
|
|
statements[resource] = append(statements[resource],
|
|
fmt.Sprintf("s3:%s", iamMapToIdentitiesAction(actionType)),
|
|
)
|
|
}
|
|
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 iamStringSlicesEqual(statement.Action.Strings(), actions) {
|
|
policyDocument.Statement[i].Resource = policy_engine.NewStringOrStringSlice(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.NewStringOrStringSlice(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(iamUserDoesNotExist, userName)}
|
|
}
|
|
|
|
// DeleteUserPolicy removes the inline policy from a user (clears their actions).
|
|
func (e *EmbeddedIamApi) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamDeleteUserPolicyResponse, *iamError) {
|
|
var resp iamDeleteUserPolicyResponse
|
|
userName := values.Get("UserName")
|
|
for _, ident := range s3cfg.Identities {
|
|
if ident.Name == userName {
|
|
ident.Actions = nil
|
|
return resp, nil
|
|
}
|
|
}
|
|
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
|
}
|
|
|
|
// 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 (e *EmbeddedIamApi) 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 e.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 := e.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)
|
|
}
|
|
|
|
// iamSelfServiceActions are actions that users can perform on their own resources without admin rights.
|
|
// According to AWS IAM, users can manage their own access keys without requiring full admin permissions.
|
|
var iamSelfServiceActions = map[string]bool{
|
|
"CreateAccessKey": true,
|
|
"DeleteAccessKey": true,
|
|
"ListAccessKeys": true,
|
|
"GetUser": true,
|
|
"UpdateAccessKey": true,
|
|
}
|
|
|
|
// iamRequiresAdminForOthers returns true if the action requires admin rights when operating on other users.
|
|
func iamRequiresAdminForOthers(action string) bool {
|
|
return iamSelfServiceActions[action]
|
|
}
|
|
|
|
// AuthIam provides IAM-specific authentication that allows self-service operations.
|
|
// Users can manage their own access keys without admin rights, but need admin for operations on other users.
|
|
// The action parameter is accepted for interface compatibility with cb.Limit but is not used
|
|
// since IAM permission checking is done based on the IAM Action parameter in the request.
|
|
func (e *EmbeddedIamApi) AuthIam(f http.HandlerFunc, _ Action) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// If auth is not enabled, allow all
|
|
if !e.iam.isEnabled() {
|
|
f(w, r)
|
|
return
|
|
}
|
|
|
|
// Parse form to get Action and UserName
|
|
if err := r.ParseForm(); err != nil {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
|
return
|
|
}
|
|
|
|
action := r.Form.Get("Action")
|
|
targetUserName := r.PostForm.Get("UserName")
|
|
|
|
// Authenticate the request using signature-only verification.
|
|
// This bypasses S3 authorization checks (identity.canDo) since IAM operations
|
|
// have their own permission model based on self-service vs admin operations.
|
|
identity, errCode := e.iam.AuthSignatureOnly(r)
|
|
if errCode != s3err.ErrNone {
|
|
s3err.WriteErrorResponse(w, r, errCode)
|
|
return
|
|
}
|
|
|
|
// IAM API requests must be authenticated - reject nil identity
|
|
// (can happen for authTypePostPolicy or authTypeStreamingUnsigned)
|
|
if identity == nil {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
return
|
|
}
|
|
|
|
// Store identity in context
|
|
if identity != nil && identity.Name != "" {
|
|
ctx := SetIdentityNameInContext(r.Context(), identity.Name)
|
|
ctx = SetIdentityInContext(ctx, identity)
|
|
r = r.WithContext(ctx)
|
|
}
|
|
|
|
// Check permissions based on action type
|
|
if iamRequiresAdminForOthers(action) {
|
|
// Self-service action: allow if operating on own resources or no target specified
|
|
if targetUserName == "" || targetUserName == identity.Name {
|
|
// Self-service: allowed
|
|
f(w, r)
|
|
return
|
|
}
|
|
// Operating on another user: require admin
|
|
if !identity.isAdmin() {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
return
|
|
}
|
|
} else {
|
|
// All other IAM actions require admin (CreateUser, DeleteUser, PutUserPolicy, etc.)
|
|
if !identity.isAdmin() {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
|
return
|
|
}
|
|
}
|
|
|
|
f(w, r)
|
|
}
|
|
}
|
|
|
|
// DoActions handles IAM API actions.
|
|
func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
|
|
// Lock to prevent concurrent read-modify-write race conditions
|
|
e.policyLock.Lock()
|
|
defer e.policyLock.Unlock()
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
|
return
|
|
}
|
|
values := r.PostForm
|
|
s3cfg := &iam_pb.S3ApiConfiguration{}
|
|
if err := e.GetS3ApiConfiguration(s3cfg); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
|
return
|
|
}
|
|
|
|
glog.V(4).Infof("IAM DoActions: %+v", values)
|
|
var response interface{}
|
|
var iamErr *iamError
|
|
changed := true
|
|
switch r.Form.Get("Action") {
|
|
case "ListUsers":
|
|
response = e.ListUsers(s3cfg, values)
|
|
changed = false
|
|
case "ListAccessKeys":
|
|
e.handleImplicitUsername(r, values)
|
|
response = e.ListAccessKeys(s3cfg, values)
|
|
changed = false
|
|
case "CreateUser":
|
|
response, iamErr = e.CreateUser(s3cfg, values)
|
|
if iamErr != nil {
|
|
e.writeIamErrorResponse(w, r, iamErr)
|
|
return
|
|
}
|
|
case "GetUser":
|
|
userName := values.Get("UserName")
|
|
response, iamErr = e.GetUser(s3cfg, userName)
|
|
if iamErr != nil {
|
|
e.writeIamErrorResponse(w, r, iamErr)
|
|
return
|
|
}
|
|
changed = false
|
|
case "UpdateUser":
|
|
response, iamErr = e.UpdateUser(s3cfg, values)
|
|
if iamErr != nil {
|
|
e.writeIamErrorResponse(w, r, iamErr)
|
|
return
|
|
}
|
|
case "DeleteUser":
|
|
userName := values.Get("UserName")
|
|
response, iamErr = e.DeleteUser(s3cfg, userName)
|
|
if iamErr != nil {
|
|
e.writeIamErrorResponse(w, r, iamErr)
|
|
return
|
|
}
|
|
case "CreateAccessKey":
|
|
e.handleImplicitUsername(r, values)
|
|
response, iamErr = e.CreateAccessKey(s3cfg, values)
|
|
if iamErr != nil {
|
|
glog.Errorf("CreateAccessKey: %+v", iamErr.Error)
|
|
e.writeIamErrorResponse(w, r, iamErr)
|
|
return
|
|
}
|
|
case "DeleteAccessKey":
|
|
e.handleImplicitUsername(r, values)
|
|
response = e.DeleteAccessKey(s3cfg, values)
|
|
case "CreatePolicy":
|
|
response, iamErr = e.CreatePolicy(s3cfg, values)
|
|
if iamErr != nil {
|
|
glog.Errorf("CreatePolicy: %+v", iamErr.Error)
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
|
return
|
|
}
|
|
// CreatePolicy only validates the policy document and returns metadata.
|
|
// Policies are not stored separately; they are attached inline via PutUserPolicy.
|
|
changed = false
|
|
case "PutUserPolicy":
|
|
response, iamErr = e.PutUserPolicy(s3cfg, values)
|
|
if iamErr != nil {
|
|
glog.Errorf("PutUserPolicy: %+v", iamErr.Error)
|
|
e.writeIamErrorResponse(w, r, iamErr)
|
|
return
|
|
}
|
|
case "GetUserPolicy":
|
|
response, iamErr = e.GetUserPolicy(s3cfg, values)
|
|
if iamErr != nil {
|
|
e.writeIamErrorResponse(w, r, iamErr)
|
|
return
|
|
}
|
|
changed = false
|
|
case "DeleteUserPolicy":
|
|
response, iamErr = e.DeleteUserPolicy(s3cfg, values)
|
|
if iamErr != nil {
|
|
e.writeIamErrorResponse(w, r, iamErr)
|
|
return
|
|
}
|
|
default:
|
|
errNotImplemented := s3err.GetAPIError(s3err.ErrNotImplemented)
|
|
errorResponse := iamErrorResponse{}
|
|
errorResponse.Error.Code = &errNotImplemented.Code
|
|
errorResponse.Error.Message = &errNotImplemented.Description
|
|
s3err.WriteXMLResponse(w, r, errNotImplemented.HTTPStatusCode, errorResponse)
|
|
return
|
|
}
|
|
if changed {
|
|
if err := e.PutS3ApiConfiguration(s3cfg); err != nil {
|
|
iamErr = &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
|
e.writeIamErrorResponse(w, r, iamErr)
|
|
return
|
|
}
|
|
// Reload in-memory identity maps so subsequent LookupByAccessKey calls
|
|
// can see newly created or deleted keys immediately
|
|
if err := e.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
|
|
}
|
|
}
|
|
// Set RequestId for AWS compatibility
|
|
if r, ok := response.(interface{ SetRequestId() }); ok {
|
|
r.SetRequestId()
|
|
}
|
|
s3err.WriteXMLResponse(w, r, http.StatusOK, response)
|
|
}
|