aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3api_embedded_iam.go
diff options
context:
space:
mode:
authorchrislu <chris.lu@gmail.com>2025-12-14 16:31:06 -0800
committerchrislu <chris.lu@gmail.com>2025-12-14 17:43:57 -0800
commite03b2ee8bbeb6e23bf46bcc26cb72ada615a24c2 (patch)
tree2688ad5cb600bbeef7f760184cfa5044711930a6 /weed/s3api/s3api_embedded_iam.go
parent8bdc4390a04604af79f91c7dce94e3b2b58442f7 (diff)
downloadseaweedfs-origin/feature/iam-user-status-management-7745.tar.xz
seaweedfs-origin/feature/iam-user-status-management-7745.zip
feat(iam): add SetUserStatus and UpdateAccessKey actions (#7745)origin/feature/iam-user-status-management-7745
Add ability to enable/disable users and access keys without deleting them. ## Changes ### Protocol Buffer Updates - Add `disabled` field (bool) to Identity message for user status - false (default) = enabled, true = disabled - No backward compatibility hack needed since zero value is correct - Add `status` field (string: Active/Inactive) to Credential message ### New IAM Actions - SetUserStatus: Enable or disable a user (requires admin) - UpdateAccessKey: Change access key status (self-service or admin) ### Behavior - Disabled users: All API requests return AccessDenied - Inactive access keys: Signature validation fails - Status check happens early in auth flow for performance - Backward compatible: existing configs default to enabled (disabled=false) ### Use Cases 1. Temporary suspension: Disable user access during investigation 2. Key rotation: Deactivate old key before deletion 3. Offboarding: Disable rather than delete for audit purposes 4. Emergency response: Quickly disable compromised credentials Fixes #7745
Diffstat (limited to 'weed/s3api/s3api_embedded_iam.go')
-rw-r--r--weed/s3api/s3api_embedded_iam.go105
1 files changed, 97 insertions, 8 deletions
diff --git a/weed/s3api/s3api_embedded_iam.go b/weed/s3api/s3api_embedded_iam.go
index 5dce8a3e0..11c03386f 100644
--- a/weed/s3api/s3api_embedded_iam.go
+++ b/weed/s3api/s3api_embedded_iam.go
@@ -56,6 +56,8 @@ type (
iamPutUserPolicyResponse = iamlib.PutUserPolicyResponse
iamDeleteUserPolicyResponse = iamlib.DeleteUserPolicyResponse
iamGetUserPolicyResponse = iamlib.GetUserPolicyResponse
+ iamSetUserStatusResponse = iamlib.SetUserStatusResponse
+ iamUpdateAccessKeyResponse = iamlib.UpdateAccessKeyResponse
iamErrorResponse = iamlib.ErrorResponse
iamError = iamlib.Error
)
@@ -83,10 +85,12 @@ func iamMapToIdentitiesAction(action string) string {
// Constants from shared package
const (
- iamCharsetUpper = iamlib.CharsetUpper
- iamCharset = iamlib.Charset
- iamPolicyDocumentVersion = iamlib.PolicyDocumentVersion
- iamUserDoesNotExist = iamlib.UserDoesNotExist
+ iamCharsetUpper = iamlib.CharsetUpper
+ iamCharset = iamlib.Charset
+ iamPolicyDocumentVersion = iamlib.PolicyDocumentVersion
+ iamUserDoesNotExist = iamlib.UserDoesNotExist
+ iamAccessKeyStatusActive = iamlib.AccessKeyStatusActive
+ iamAccessKeyStatusInactive = iamlib.AccessKeyStatusInactive
)
func newIamErrorResponse(errCode string, errMsg string) iamErrorResponse {
@@ -151,15 +155,23 @@ func (e *EmbeddedIamApi) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.
// 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 {
+ // Return actual status from credential, default to Active if not set
+ status := cred.Status
+ if status == "" {
+ status = iamAccessKeyStatusActive
+ }
+ // Capture copies to avoid loop variable pointer aliasing
+ identName := ident.Name
+ accessKey := cred.AccessKey
+ statusCopy := status
resp.ListAccessKeysResult.AccessKeyMetadata = append(resp.ListAccessKeysResult.AccessKeyMetadata,
- &iam.AccessKeyMetadata{UserName: &ident.Name, AccessKeyId: &cred.AccessKey, Status: &status},
+ &iam.AccessKeyMetadata{UserName: &identName, AccessKeyId: &accessKey, Status: &statusCopy},
)
}
}
@@ -184,7 +196,7 @@ func (e *EmbeddedIamApi) CreateUser(s3cfg *iam_pb.S3ApiConfiguration, values url
}
resp.CreateUserResult.User.UserName = &userName
- s3cfg.Identities = append(s3cfg.Identities, &iam_pb.Identity{Name: userName})
+ s3cfg.Identities = append(s3cfg.Identities, &iam_pb.Identity{Name: userName}) // Disabled defaults to false (enabled)
return resp, nil
}
@@ -253,7 +265,7 @@ func (e *EmbeddedIamApi) CreateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, value
for _, ident := range s3cfg.Identities {
if userName == ident.Name {
ident.Credentials = append(ident.Credentials,
- &iam_pb.Credential{AccessKey: accessKeyId, SecretKey: secretAccessKey})
+ &iam_pb.Credential{AccessKey: accessKeyId, SecretKey: secretAccessKey, Status: iamAccessKeyStatusActive})
return resp, nil
}
}
@@ -477,6 +489,70 @@ func (e *EmbeddedIamApi) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, valu
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}
+// SetUserStatus enables or disables a user without deleting them.
+// This is a SeaweedFS extension for temporary user suspension, offboarding, etc.
+// When a user is disabled, all API requests using their credentials will return AccessDenied.
+func (e *EmbeddedIamApi) SetUserStatus(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamSetUserStatusResponse, *iamError) {
+ var resp iamSetUserStatusResponse
+ userName := values.Get("UserName")
+ status := values.Get("Status")
+
+ // Validate UserName
+ if userName == "" {
+ return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
+ }
+
+ // Validate Status - must be "Active" or "Inactive"
+ if status != iamAccessKeyStatusActive && status != iamAccessKeyStatusInactive {
+ return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("Status must be '%s' or '%s'", iamAccessKeyStatusActive, iamAccessKeyStatusInactive)}
+ }
+
+ for _, ident := range s3cfg.Identities {
+ if ident.Name == userName {
+ // Set disabled based on status: Active = not disabled, Inactive = disabled
+ ident.Disabled = (status == iamAccessKeyStatusInactive)
+ return resp, nil
+ }
+ }
+ return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
+}
+
+// UpdateAccessKey updates the status of an access key (Active or Inactive).
+// This allows key rotation workflows where old keys are deactivated before deletion.
+func (e *EmbeddedIamApi) UpdateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamUpdateAccessKeyResponse, *iamError) {
+ var resp iamUpdateAccessKeyResponse
+ userName := values.Get("UserName")
+ accessKeyId := values.Get("AccessKeyId")
+ status := values.Get("Status")
+
+ // Validate required parameters
+ 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 status != iamAccessKeyStatusActive && status != iamAccessKeyStatusInactive {
+ return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("Status must be '%s' or '%s'", iamAccessKeyStatusActive, iamAccessKeyStatusInactive)}
+ }
+
+ 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
+ }
+ }
+ // User found but access key not found
+ return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("the access key with id %s cannot be found", accessKeyId)}
+ }
+
+ 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."
@@ -707,6 +783,19 @@ func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
e.writeIamErrorResponse(w, r, iamErr)
return
}
+ case "SetUserStatus":
+ response, iamErr = e.SetUserStatus(s3cfg, values)
+ if iamErr != nil {
+ e.writeIamErrorResponse(w, r, iamErr)
+ return
+ }
+ case "UpdateAccessKey":
+ e.handleImplicitUsername(r, values)
+ response, iamErr = e.UpdateAccessKey(s3cfg, values)
+ if iamErr != nil {
+ e.writeIamErrorResponse(w, r, iamErr)
+ return
+ }
default:
errNotImplemented := s3err.GetAPIError(s3err.ErrNotImplemented)
errorResponse := iamErrorResponse{}