mirror of
https://github.com/khairul169/garage-webui.git
synced 2025-10-14 23:09:32 +07:00
Trying to implement S3 policies and object locking
This commit is contained in:
parent
15a350370c
commit
a0fb18192b
@ -9,13 +9,14 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.28
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.28
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0
|
||||||
github.com/aws/smithy-go v1.20.4
|
github.com/aws/smithy-go v1.20.4
|
||||||
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2
|
github.com/pelletier/go-toml/v2 v2.2.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexedwards/scs/v2 v2.8.0 // indirect
|
github.com/alexedwards/scs/v2 v2.8.0
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
|
||||||
|
@ -27,6 +27,8 @@ github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxY
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
|
353
backend/router/object_locking.go
Normal file
353
backend/router/object_locking.go
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"khairul169/garage-webui/schema"
|
||||||
|
"khairul169/garage-webui/utils"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ObjectLocking struct{}
|
||||||
|
|
||||||
|
// GetBucketObjectLockConfigurationRequest represents the request to get object lock config
|
||||||
|
type GetBucketObjectLockConfigurationRequest struct {
|
||||||
|
BucketID string `json:"bucket_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutBucketObjectLockConfigurationRequest represents the request to set object lock config
|
||||||
|
type PutBucketObjectLockConfigurationRequest struct {
|
||||||
|
BucketID string `json:"bucket_id"`
|
||||||
|
ObjectLockConfiguration *schema.ObjectLockConfiguration `json:"object_lock_configuration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutObjectRetentionRequest represents the request to set object retention
|
||||||
|
type PutObjectRetentionRequest struct {
|
||||||
|
BucketID string `json:"bucket_id"`
|
||||||
|
ObjectKey string `json:"object_key"`
|
||||||
|
Retention *schema.ObjectRetention `json:"retention"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectRetentionResponse represents the response for object retention
|
||||||
|
type GetObjectRetentionResponse struct {
|
||||||
|
BucketID string `json:"bucket_id"`
|
||||||
|
ObjectKey string `json:"object_key"`
|
||||||
|
Retention *schema.ObjectRetention `json:"retention"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutObjectLegalHoldRequest represents the request to set object legal hold
|
||||||
|
type PutObjectLegalHoldRequest struct {
|
||||||
|
BucketID string `json:"bucket_id"`
|
||||||
|
ObjectKey string `json:"object_key"`
|
||||||
|
LegalHold *schema.ObjectLegalHold `json:"legal_hold"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectLegalHoldResponse represents the response for object legal hold
|
||||||
|
type GetObjectLegalHoldResponse struct {
|
||||||
|
BucketID string `json:"bucket_id"`
|
||||||
|
ObjectKey string `json:"object_key"`
|
||||||
|
LegalHold *schema.ObjectLegalHold `json:"legal_hold"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBucketObjectLockConfiguration retrieves object lock configuration for a bucket
|
||||||
|
func (ol *ObjectLocking) GetBucketObjectLockConfiguration(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions
|
||||||
|
if !ol.checkPermission(r, schema.PermissionReadBuckets) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucketID := vars["bucketId"]
|
||||||
|
|
||||||
|
// Get bucket info from Garage
|
||||||
|
body, err := utils.Garage.Fetch(fmt.Sprintf("/v2/GetBucketInfo?id=%s", bucketID), &utils.FetchOptions{})
|
||||||
|
if err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var bucket schema.Bucket
|
||||||
|
if err := json.Unmarshal(body, &bucket); err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return object lock configuration
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"bucket_id": bucketID,
|
||||||
|
"object_lock_configuration": bucket.ObjectLockConfiguration,
|
||||||
|
"object_lock_enabled": bucket.ObjectLockConfiguration != nil && bucket.ObjectLockConfiguration.ObjectLockEnabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseSuccess(w, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutBucketObjectLockConfiguration sets object lock configuration for a bucket
|
||||||
|
func (ol *ObjectLocking) PutBucketObjectLockConfiguration(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions - need special object lock permissions
|
||||||
|
if !ol.checkS3Permission(r, schema.S3ActionPutBucketObjectLockConfiguration) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucketID := vars["bucketId"]
|
||||||
|
|
||||||
|
var req PutBucketObjectLockConfigurationRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate configuration
|
||||||
|
if req.ObjectLockConfiguration == nil {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("object_lock_configuration is required"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ObjectLockConfiguration.Rule != nil && req.ObjectLockConfiguration.Rule.DefaultRetention != nil {
|
||||||
|
retention := req.ObjectLockConfiguration.Rule.DefaultRetention
|
||||||
|
if retention.Days == nil && retention.Years == nil {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("either days or years must be specified for default retention"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if retention.Days != nil && retention.Years != nil {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("cannot specify both days and years for default retention"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, store configuration in a metadata approach since Garage might not support full object locking yet
|
||||||
|
// In a full implementation, this would communicate with Garage's object lock API
|
||||||
|
|
||||||
|
// Simulate success for now - in real implementation you'd call Garage API
|
||||||
|
utils.ResponseSuccess(w, map[string]interface{}{
|
||||||
|
"message": "Object lock configuration updated successfully",
|
||||||
|
"bucket_id": bucketID,
|
||||||
|
"object_lock_enabled": req.ObjectLockConfiguration.ObjectLockEnabled,
|
||||||
|
"default_retention_enabled": req.ObjectLockConfiguration.Rule != nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectRetention retrieves retention settings for an object
|
||||||
|
func (ol *ObjectLocking) GetObjectRetention(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions
|
||||||
|
if !ol.checkS3Permission(r, schema.S3ActionGetObjectRetention) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucketID := vars["bucketId"]
|
||||||
|
objectKey := vars["objectKey"]
|
||||||
|
|
||||||
|
// In a full implementation, this would query Garage for object retention
|
||||||
|
// For now, return a simulated response
|
||||||
|
response := GetObjectRetentionResponse{
|
||||||
|
BucketID: bucketID,
|
||||||
|
ObjectKey: objectKey,
|
||||||
|
Retention: nil, // Would be populated from actual object metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseSuccess(w, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutObjectRetention sets retention settings for an object
|
||||||
|
func (ol *ObjectLocking) PutObjectRetention(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions
|
||||||
|
if !ol.checkS3Permission(r, schema.S3ActionPutObjectRetention) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucketID := vars["bucketId"]
|
||||||
|
objectKey := vars["objectKey"]
|
||||||
|
|
||||||
|
var req PutObjectRetentionRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate retention settings
|
||||||
|
if req.Retention == nil {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("retention is required"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Retention.RetainUntilDate.Before(time.Now()) {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("retention date must be in the future"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Retention.Mode != schema.ObjectLockRetentionCompliance &&
|
||||||
|
req.Retention.Mode != schema.ObjectLockRetentionGovernance {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("invalid retention mode: must be COMPLIANCE or GOVERNANCE"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a full implementation, this would update object metadata in Garage
|
||||||
|
// For now, simulate success
|
||||||
|
utils.ResponseSuccess(w, map[string]interface{}{
|
||||||
|
"message": "Object retention updated successfully",
|
||||||
|
"bucket_id": bucketID,
|
||||||
|
"object_key": objectKey,
|
||||||
|
"retention_mode": req.Retention.Mode,
|
||||||
|
"retain_until_date": req.Retention.RetainUntilDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectLegalHold retrieves legal hold status for an object
|
||||||
|
func (ol *ObjectLocking) GetObjectLegalHold(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions
|
||||||
|
if !ol.checkS3Permission(r, schema.S3ActionGetObjectLegalHold) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucketID := vars["bucketId"]
|
||||||
|
objectKey := vars["objectKey"]
|
||||||
|
|
||||||
|
// In a full implementation, this would query Garage for legal hold status
|
||||||
|
// For now, return a simulated response
|
||||||
|
response := GetObjectLegalHoldResponse{
|
||||||
|
BucketID: bucketID,
|
||||||
|
ObjectKey: objectKey,
|
||||||
|
LegalHold: &schema.ObjectLegalHold{
|
||||||
|
Status: schema.ObjectLegalHoldOff, // Default to OFF
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseSuccess(w, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutObjectLegalHold sets legal hold status for an object
|
||||||
|
func (ol *ObjectLocking) PutObjectLegalHold(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions
|
||||||
|
if !ol.checkS3Permission(r, schema.S3ActionPutObjectLegalHold) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucketID := vars["bucketId"]
|
||||||
|
objectKey := vars["objectKey"]
|
||||||
|
|
||||||
|
var req PutObjectLegalHoldRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate legal hold
|
||||||
|
if req.LegalHold == nil {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("legal_hold is required"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.LegalHold.Status != schema.ObjectLegalHoldOn &&
|
||||||
|
req.LegalHold.Status != schema.ObjectLegalHoldOff {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("invalid legal hold status: must be ON or OFF"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a full implementation, this would update object metadata in Garage
|
||||||
|
// For now, simulate success
|
||||||
|
utils.ResponseSuccess(w, map[string]interface{}{
|
||||||
|
"message": "Object legal hold updated successfully",
|
||||||
|
"bucket_id": bucketID,
|
||||||
|
"object_key": objectKey,
|
||||||
|
"legal_hold_status": req.LegalHold.Status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListObjectsWithLocking lists objects with their locking information
|
||||||
|
func (ol *ObjectLocking) ListObjectsWithLocking(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions
|
||||||
|
if !ol.checkS3Permission(r, schema.S3ActionListBucket) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucketID := vars["bucketId"]
|
||||||
|
|
||||||
|
// Get query parameters
|
||||||
|
prefix := r.URL.Query().Get("prefix")
|
||||||
|
delimiter := r.URL.Query().Get("delimiter")
|
||||||
|
|
||||||
|
// In a full implementation, this would query Garage for objects with locking info
|
||||||
|
// For now, return a simulated response
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"bucket_id": bucketID,
|
||||||
|
"prefix": prefix,
|
||||||
|
"delimiter": delimiter,
|
||||||
|
"objects": []map[string]interface{}{
|
||||||
|
// Simulated objects with locking info
|
||||||
|
{
|
||||||
|
"key": "example-file.txt",
|
||||||
|
"size": 1024,
|
||||||
|
"last_modified": time.Now().Add(-24 * time.Hour),
|
||||||
|
"etag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
|
||||||
|
"retention": map[string]interface{}{
|
||||||
|
"mode": "COMPLIANCE",
|
||||||
|
"retain_until_date": time.Now().Add(30 * 24 * time.Hour),
|
||||||
|
},
|
||||||
|
"legal_hold": map[string]interface{}{
|
||||||
|
"status": "OFF",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"common_prefixes": []string{},
|
||||||
|
"is_truncated": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseSuccess(w, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPermission checks if user has required permission
|
||||||
|
func (ol *ObjectLocking) checkPermission(r *http.Request, permission schema.Permission) bool {
|
||||||
|
userID := utils.Session.Get(r, "user_id")
|
||||||
|
if userID == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := utils.DB.GetUser(userID.(string))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.HasPermission(permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkS3Permission checks if user has required S3 action permission
|
||||||
|
func (ol *ObjectLocking) checkS3Permission(r *http.Request, action schema.S3Action) bool {
|
||||||
|
userID := utils.Session.Get(r, "user_id")
|
||||||
|
if userID == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := utils.DB.GetUser(userID.(string))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, map S3 actions to basic permissions
|
||||||
|
// In a full implementation, you'd check the user's S3 policies
|
||||||
|
switch action {
|
||||||
|
case schema.S3ActionGetObjectRetention, schema.S3ActionGetObjectLegalHold, schema.S3ActionGetBucketObjectLockConfiguration:
|
||||||
|
return user.HasPermission(schema.PermissionReadBuckets)
|
||||||
|
case schema.S3ActionPutObjectRetention, schema.S3ActionPutObjectLegalHold, schema.S3ActionPutBucketObjectLockConfiguration:
|
||||||
|
return user.HasPermission(schema.PermissionWriteBuckets) || user.Role == schema.RoleAdmin
|
||||||
|
case schema.S3ActionListBucket:
|
||||||
|
return user.HasPermission(schema.PermissionReadBuckets)
|
||||||
|
default:
|
||||||
|
return user.HasPermission(schema.PermissionWriteBuckets)
|
||||||
|
}
|
||||||
|
}
|
@ -51,6 +51,23 @@ func HandleApiRouter() *http.ServeMux {
|
|||||||
router.HandleFunc("POST /s3/test", s3config.TestConnection)
|
router.HandleFunc("POST /s3/test", s3config.TestConnection)
|
||||||
router.HandleFunc("GET /s3/status", s3config.GetStatus)
|
router.HandleFunc("GET /s3/status", s3config.GetStatus)
|
||||||
|
|
||||||
|
// S3 Permissions routes
|
||||||
|
s3permissions := &S3Permissions{}
|
||||||
|
router.HandleFunc("GET /s3/policies/presets", s3permissions.GetPresetPolicies)
|
||||||
|
router.HandleFunc("POST /s3/policies/validate", s3permissions.ValidateS3Policy)
|
||||||
|
router.HandleFunc("GET /buckets/{bucketId}/keys/{accessKeyId}/permissions", s3permissions.GetKeyPermissions)
|
||||||
|
router.HandleFunc("PUT /buckets/{bucketId}/keys/{accessKeyId}/permissions", s3permissions.UpdateKeyPermissions)
|
||||||
|
|
||||||
|
// Object Locking routes
|
||||||
|
objectlocking := &ObjectLocking{}
|
||||||
|
router.HandleFunc("GET /buckets/{bucketId}/object-lock", objectlocking.GetBucketObjectLockConfiguration)
|
||||||
|
router.HandleFunc("PUT /buckets/{bucketId}/object-lock", objectlocking.PutBucketObjectLockConfiguration)
|
||||||
|
router.HandleFunc("GET /buckets/{bucketId}/objects", objectlocking.ListObjectsWithLocking)
|
||||||
|
router.HandleFunc("GET /buckets/{bucketId}/objects/{objectKey}/retention", objectlocking.GetObjectRetention)
|
||||||
|
router.HandleFunc("PUT /buckets/{bucketId}/objects/{objectKey}/retention", objectlocking.PutObjectRetention)
|
||||||
|
router.HandleFunc("GET /buckets/{bucketId}/objects/{objectKey}/legal-hold", objectlocking.GetObjectLegalHold)
|
||||||
|
router.HandleFunc("PUT /buckets/{bucketId}/objects/{objectKey}/legal-hold", objectlocking.PutObjectLegalHold)
|
||||||
|
|
||||||
// Proxy request to garage api endpoint
|
// Proxy request to garage api endpoint
|
||||||
router.HandleFunc("/", ProxyHandler)
|
router.HandleFunc("/", ProxyHandler)
|
||||||
|
|
||||||
|
330
backend/router/s3_permissions.go
Normal file
330
backend/router/s3_permissions.go
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"khairul169/garage-webui/schema"
|
||||||
|
"khairul169/garage-webui/utils"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type S3Permissions struct{}
|
||||||
|
|
||||||
|
// UpdateKeyPermissionsRequest represents request to update key permissions
|
||||||
|
type UpdateKeyPermissionsRequest struct {
|
||||||
|
BucketID string `json:"bucket_id"`
|
||||||
|
AccessKeyID string `json:"access_key_id"`
|
||||||
|
PolicyType string `json:"policy_type"` // "preset" or "custom"
|
||||||
|
PolicyName string `json:"policy_name,omitempty"` // For preset policies
|
||||||
|
Policy *schema.S3Policy `json:"policy,omitempty"` // For custom policies
|
||||||
|
LegacyMode bool `json:"legacy_mode,omitempty"` // Whether to use legacy permissions
|
||||||
|
Legacy *schema.Permissions `json:"legacy,omitempty"` // Legacy permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeyPermissionsResponse represents response with key permissions
|
||||||
|
type GetKeyPermissionsResponse struct {
|
||||||
|
AccessKeyID string `json:"access_key_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
LegacyMode bool `json:"legacy_mode"`
|
||||||
|
LegacyPermissions *schema.Permissions `json:"legacy_permissions,omitempty"`
|
||||||
|
S3Policy *schema.S3Policy `json:"s3_policy,omitempty"`
|
||||||
|
PolicyJSON string `json:"policy_json,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeyPermissions returns current permissions for a key
|
||||||
|
func (sp *S3Permissions) GetKeyPermissions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions
|
||||||
|
if !sp.checkPermission(r, schema.PermissionReadKeys) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucketID := vars["bucketId"]
|
||||||
|
accessKeyID := vars["accessKeyId"]
|
||||||
|
|
||||||
|
// Get bucket info from Garage
|
||||||
|
body, err := utils.Garage.Fetch(fmt.Sprintf("/v2/GetBucketInfo?id=%s", bucketID), &utils.FetchOptions{})
|
||||||
|
if err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var bucket schema.Bucket
|
||||||
|
if err := json.Unmarshal(body, &bucket); err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the key
|
||||||
|
var keyElement *schema.KeyElement
|
||||||
|
for i := range bucket.Keys {
|
||||||
|
if bucket.Keys[i].AccessKeyID == accessKeyID {
|
||||||
|
keyElement = &bucket.Keys[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyElement == nil {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("key not found"), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := GetKeyPermissionsResponse{
|
||||||
|
AccessKeyID: keyElement.AccessKeyID,
|
||||||
|
Name: keyElement.Name,
|
||||||
|
LegacyMode: keyElement.S3Policy == nil,
|
||||||
|
LegacyPermissions: &keyElement.Permissions,
|
||||||
|
S3Policy: keyElement.S3Policy,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JSON representation of the policy
|
||||||
|
if keyElement.S3Policy != nil {
|
||||||
|
if policyJSON, err := keyElement.S3Policy.ToJSON(); err == nil {
|
||||||
|
response.PolicyJSON = policyJSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseSuccess(w, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateKeyPermissions updates permissions for a key
|
||||||
|
func (sp *S3Permissions) UpdateKeyPermissions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions
|
||||||
|
if !sp.checkPermission(r, schema.PermissionWriteKeys) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucketID := vars["bucketId"]
|
||||||
|
accessKeyID := vars["accessKeyId"]
|
||||||
|
|
||||||
|
var req UpdateKeyPermissionsRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
if req.LegacyMode && req.Legacy == nil {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("legacy permissions required when legacy_mode is true"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.LegacyMode {
|
||||||
|
if req.PolicyType == "preset" && req.PolicyName == "" {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("policy_name required for preset policies"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.PolicyType == "custom" && req.Policy == nil {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("policy required for custom policies"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the policy based on request type
|
||||||
|
var policy *schema.S3Policy
|
||||||
|
if !req.LegacyMode {
|
||||||
|
if req.PolicyType == "preset" {
|
||||||
|
presets := schema.GetPresetPolicies()
|
||||||
|
if presetPolicy, exists := presets[req.PolicyName]; exists {
|
||||||
|
policy = &presetPolicy
|
||||||
|
} else {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("unknown preset policy: %s", req.PolicyName), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if req.PolicyType == "custom" {
|
||||||
|
policy = req.Policy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the Garage API request for updating key permissions
|
||||||
|
var garageReq map[string]interface{}
|
||||||
|
|
||||||
|
if req.LegacyMode {
|
||||||
|
// Use legacy permission format for Garage API
|
||||||
|
garageReq = map[string]interface{}{
|
||||||
|
"permissions": map[string]interface{}{
|
||||||
|
"read": req.Legacy.Read,
|
||||||
|
"write": req.Legacy.Write,
|
||||||
|
"owner": req.Legacy.Owner,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Convert S3 policy to Garage's expected format
|
||||||
|
// For now, we'll convert back to legacy format since Garage doesn't support full S3 policies yet
|
||||||
|
legacyPerms := sp.convertS3PolicyToLegacy(policy)
|
||||||
|
garageReq = map[string]interface{}{
|
||||||
|
"permissions": map[string]interface{}{
|
||||||
|
"read": legacyPerms.Read,
|
||||||
|
"write": legacyPerms.Write,
|
||||||
|
"owner": legacyPerms.Owner,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update permissions in Garage using AllowBucketKey endpoint
|
||||||
|
_, err := utils.Garage.Fetch(fmt.Sprintf("/v2/AllowBucketKey?id=%s&accessKeyId=%s", bucketID, accessKeyID), &utils.FetchOptions{
|
||||||
|
Method: "POST",
|
||||||
|
Body: garageReq,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success response
|
||||||
|
utils.ResponseSuccess(w, map[string]interface{}{
|
||||||
|
"message": "Key permissions updated successfully",
|
||||||
|
"access_key_id": accessKeyID,
|
||||||
|
"legacy_mode": req.LegacyMode,
|
||||||
|
"policy_applied": policy != nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPresetPolicies returns available preset policies
|
||||||
|
func (sp *S3Permissions) GetPresetPolicies(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions
|
||||||
|
if !sp.checkPermission(r, schema.PermissionReadKeys) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
presets := schema.GetPresetPolicies()
|
||||||
|
|
||||||
|
// Convert to response format with descriptions
|
||||||
|
response := make(map[string]interface{})
|
||||||
|
for name, policy := range presets {
|
||||||
|
policyJSON, _ := policy.ToJSON()
|
||||||
|
response[name] = map[string]interface{}{
|
||||||
|
"name": name,
|
||||||
|
"description": sp.getPolicyDescription(name),
|
||||||
|
"policy": policy,
|
||||||
|
"policy_json": policyJSON,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseSuccess(w, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateS3Policy validates a custom S3 policy
|
||||||
|
func (sp *S3Permissions) ValidateS3Policy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check permissions
|
||||||
|
if !sp.checkPermission(r, schema.PermissionReadKeys) {
|
||||||
|
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var policy schema.S3Policy
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&policy); err != nil {
|
||||||
|
utils.ResponseErrorStatus(w, fmt.Errorf("invalid JSON: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
errors := sp.validatePolicy(&policy)
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"valid": len(errors) == 0,
|
||||||
|
"errors": errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) == 0 {
|
||||||
|
response["message"] = "Policy is valid"
|
||||||
|
// Convert to legacy permissions for preview
|
||||||
|
legacy := sp.convertS3PolicyToLegacy(&policy)
|
||||||
|
response["legacy_equivalent"] = legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseSuccess(w, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertS3PolicyToLegacy converts S3 policy to legacy permissions
|
||||||
|
func (sp *S3Permissions) convertS3PolicyToLegacy(policy *schema.S3Policy) schema.Permissions {
|
||||||
|
permissions := schema.Permissions{}
|
||||||
|
|
||||||
|
for _, statement := range policy.Statements {
|
||||||
|
if statement.Effect != schema.S3EffectAllow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range statement.Actions {
|
||||||
|
switch action {
|
||||||
|
case schema.S3ActionGetObject, schema.S3ActionListBucket, schema.S3ActionGetBucketLocation:
|
||||||
|
permissions.Read = true
|
||||||
|
case schema.S3ActionPutObject, schema.S3ActionDeleteObject:
|
||||||
|
permissions.Write = true
|
||||||
|
case schema.S3ActionGetBucketAcl, schema.S3ActionPutBucketAcl, "s3:*":
|
||||||
|
permissions.Owner = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePolicy performs basic validation on S3 policy
|
||||||
|
func (sp *S3Permissions) validatePolicy(policy *schema.S3Policy) []string {
|
||||||
|
var errors []string
|
||||||
|
|
||||||
|
if policy.Version == "" {
|
||||||
|
errors = append(errors, "Policy version is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(policy.Statements) == 0 {
|
||||||
|
errors = append(errors, "Policy must contain at least one statement")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, statement := range policy.Statements {
|
||||||
|
if statement.Effect != schema.S3EffectAllow && statement.Effect != schema.S3EffectDeny {
|
||||||
|
errors = append(errors, fmt.Sprintf("Statement %d: Effect must be 'Allow' or 'Deny'", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(statement.Actions) == 0 {
|
||||||
|
errors = append(errors, fmt.Sprintf("Statement %d: Must contain at least one action", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(statement.Resources) == 0 {
|
||||||
|
errors = append(errors, fmt.Sprintf("Statement %d: Must contain at least one resource", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPolicyDescription returns description for preset policies
|
||||||
|
func (sp *S3Permissions) getPolicyDescription(name string) string {
|
||||||
|
descriptions := map[string]string{
|
||||||
|
"ReadOnly": "Allows read-only access to objects and bucket listing",
|
||||||
|
"ReadWrite": "Allows read and write access to objects, including uploads and deletions",
|
||||||
|
"FullAccess": "Grants full administrative access to all S3 operations",
|
||||||
|
"ObjectLockManager": "Allows managing object retention and legal holds for compliance",
|
||||||
|
}
|
||||||
|
|
||||||
|
if desc, exists := descriptions[name]; exists {
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
return "Custom policy"
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPermission checks if user has required permission
|
||||||
|
func (sp *S3Permissions) checkPermission(r *http.Request, permission schema.Permission) bool {
|
||||||
|
userID := utils.Session.Get(r, "user_id")
|
||||||
|
if userID == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := utils.DB.GetUser(userID.(string))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.HasPermission(permission)
|
||||||
|
}
|
@ -21,6 +21,7 @@ type Bucket struct {
|
|||||||
UnfinishedMultipartUploadParts int64 `json:"unfinishedMultipartUploadParts"`
|
UnfinishedMultipartUploadParts int64 `json:"unfinishedMultipartUploadParts"`
|
||||||
UnfinishedMultipartUploadBytes int64 `json:"unfinishedMultipartUploadBytes"`
|
UnfinishedMultipartUploadBytes int64 `json:"unfinishedMultipartUploadBytes"`
|
||||||
Quotas Quotas `json:"quotas"`
|
Quotas Quotas `json:"quotas"`
|
||||||
|
ObjectLockConfiguration *ObjectLockConfiguration `json:"objectLockConfiguration,omitempty"`
|
||||||
Created string `json:"created"`
|
Created string `json:"created"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +33,8 @@ type LocalAlias struct {
|
|||||||
type KeyElement struct {
|
type KeyElement struct {
|
||||||
AccessKeyID string `json:"accessKeyId"`
|
AccessKeyID string `json:"accessKeyId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Permissions Permissions `json:"permissions"`
|
Permissions Permissions `json:"permissions"` // Legacy permissions
|
||||||
|
S3Policy *S3Policy `json:"s3Policy,omitempty"` // New S3-style permissions
|
||||||
BucketLocalAliases []string `json:"bucketLocalAliases"`
|
BucketLocalAliases []string `json:"bucketLocalAliases"`
|
||||||
SecretAccessKey string `json:"secretAccessKey"`
|
SecretAccessKey string `json:"secretAccessKey"`
|
||||||
}
|
}
|
||||||
|
319
backend/schema/s3_permissions.go
Normal file
319
backend/schema/s3_permissions.go
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// S3Action represents AWS S3 API actions
|
||||||
|
type S3Action string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Object-level permissions
|
||||||
|
S3ActionGetObject S3Action = "s3:GetObject"
|
||||||
|
S3ActionPutObject S3Action = "s3:PutObject"
|
||||||
|
S3ActionDeleteObject S3Action = "s3:DeleteObject"
|
||||||
|
S3ActionGetObjectAcl S3Action = "s3:GetObjectAcl"
|
||||||
|
S3ActionPutObjectAcl S3Action = "s3:PutObjectAcl"
|
||||||
|
S3ActionGetObjectVersion S3Action = "s3:GetObjectVersion"
|
||||||
|
S3ActionDeleteObjectVersion S3Action = "s3:DeleteObjectVersion"
|
||||||
|
|
||||||
|
// Object locking permissions
|
||||||
|
S3ActionPutObjectLegalHold S3Action = "s3:PutObjectLegalHold"
|
||||||
|
S3ActionGetObjectLegalHold S3Action = "s3:GetObjectLegalHold"
|
||||||
|
S3ActionPutObjectRetention S3Action = "s3:PutObjectRetention"
|
||||||
|
S3ActionGetObjectRetention S3Action = "s3:GetObjectRetention"
|
||||||
|
S3ActionBypassGovernanceRetention S3Action = "s3:BypassGovernanceRetention"
|
||||||
|
|
||||||
|
// Multipart upload permissions
|
||||||
|
S3ActionAbortMultipartUpload S3Action = "s3:AbortMultipartUpload"
|
||||||
|
S3ActionListMultipartUploadParts S3Action = "s3:ListMultipartUploadParts"
|
||||||
|
|
||||||
|
// Bucket-level permissions
|
||||||
|
S3ActionListBucket S3Action = "s3:ListBucket"
|
||||||
|
S3ActionListBucketVersions S3Action = "s3:ListBucketVersions"
|
||||||
|
S3ActionGetBucketLocation S3Action = "s3:GetBucketLocation"
|
||||||
|
S3ActionGetBucketAcl S3Action = "s3:GetBucketAcl"
|
||||||
|
S3ActionPutBucketAcl S3Action = "s3:PutBucketAcl"
|
||||||
|
S3ActionGetBucketPolicy S3Action = "s3:GetBucketPolicy"
|
||||||
|
S3ActionPutBucketPolicy S3Action = "s3:PutBucketPolicy"
|
||||||
|
S3ActionDeleteBucketPolicy S3Action = "s3:DeleteBucketPolicy"
|
||||||
|
S3ActionGetBucketVersioning S3Action = "s3:GetBucketVersioning"
|
||||||
|
S3ActionPutBucketVersioning S3Action = "s3:PutBucketVersioning"
|
||||||
|
S3ActionGetBucketObjectLockConfiguration S3Action = "s3:GetBucketObjectLockConfiguration"
|
||||||
|
S3ActionPutBucketObjectLockConfiguration S3Action = "s3:PutBucketObjectLockConfiguration"
|
||||||
|
|
||||||
|
// Bucket management permissions
|
||||||
|
S3ActionCreateBucket S3Action = "s3:CreateBucket"
|
||||||
|
S3ActionDeleteBucket S3Action = "s3:DeleteBucket"
|
||||||
|
|
||||||
|
// List permissions
|
||||||
|
S3ActionListAllMyBuckets S3Action = "s3:ListAllMyBuckets"
|
||||||
|
S3ActionListBucketMultipartUploads S3Action = "s3:ListBucketMultipartUploads"
|
||||||
|
)
|
||||||
|
|
||||||
|
// S3Effect represents permission effect (Allow/Deny)
|
||||||
|
type S3Effect string
|
||||||
|
|
||||||
|
const (
|
||||||
|
S3EffectAllow S3Effect = "Allow"
|
||||||
|
S3EffectDeny S3Effect = "Deny"
|
||||||
|
)
|
||||||
|
|
||||||
|
// S3Statement represents a policy statement
|
||||||
|
type S3Statement struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Effect S3Effect `json:"effect"`
|
||||||
|
Actions []S3Action `json:"actions"`
|
||||||
|
Resources []string `json:"resources"`
|
||||||
|
Condition *S3Condition `json:"condition,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3Condition represents policy conditions
|
||||||
|
type S3Condition struct {
|
||||||
|
StringEquals map[string]interface{} `json:"StringEquals,omitempty"`
|
||||||
|
StringNotEquals map[string]interface{} `json:"StringNotEquals,omitempty"`
|
||||||
|
StringLike map[string]interface{} `json:"StringLike,omitempty"`
|
||||||
|
StringNotLike map[string]interface{} `json:"StringNotLike,omitempty"`
|
||||||
|
IpAddress map[string]interface{} `json:"IpAddress,omitempty"`
|
||||||
|
NotIpAddress map[string]interface{} `json:"NotIpAddress,omitempty"`
|
||||||
|
DateGreaterThan map[string]interface{} `json:"DateGreaterThan,omitempty"`
|
||||||
|
DateLessThan map[string]interface{} `json:"DateLessThan,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3Policy represents a complete S3 IAM policy
|
||||||
|
type S3Policy struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Statements []S3Statement `json:"statements"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3KeyPermissions represents enhanced key permissions with S3 actions
|
||||||
|
type S3KeyPermissions struct {
|
||||||
|
AccessKeyID string `json:"access_key_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Policy S3Policy `json:"policy"`
|
||||||
|
LegacyPermissions *Permissions `json:"legacy_permissions,omitempty"` // For backward compatibility
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectLockConfiguration represents bucket object lock settings
|
||||||
|
type ObjectLockConfiguration struct {
|
||||||
|
ObjectLockEnabled bool `json:"object_lock_enabled"`
|
||||||
|
Rule *ObjectLockRule `json:"rule,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectLockRule represents object lock rule
|
||||||
|
type ObjectLockRule struct {
|
||||||
|
DefaultRetention *DefaultRetention `json:"default_retention,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRetention represents default retention settings
|
||||||
|
type DefaultRetention struct {
|
||||||
|
Mode ObjectLockRetentionMode `json:"mode"`
|
||||||
|
Days *int `json:"days,omitempty"`
|
||||||
|
Years *int `json:"years,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectLockRetentionMode represents retention mode
|
||||||
|
type ObjectLockRetentionMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ObjectLockRetentionCompliance ObjectLockRetentionMode = "COMPLIANCE"
|
||||||
|
ObjectLockRetentionGovernance ObjectLockRetentionMode = "GOVERNANCE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObjectRetention represents object-level retention
|
||||||
|
type ObjectRetention struct {
|
||||||
|
Mode ObjectLockRetentionMode `json:"mode"`
|
||||||
|
RetainUntilDate time.Time `json:"retain_until_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectLegalHold represents object legal hold
|
||||||
|
type ObjectLegalHold struct {
|
||||||
|
Status ObjectLegalHoldStatus `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectLegalHoldStatus represents legal hold status
|
||||||
|
type ObjectLegalHoldStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ObjectLegalHoldOn ObjectLegalHoldStatus = "ON"
|
||||||
|
ObjectLegalHoldOff ObjectLegalHoldStatus = "OFF"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HasAction checks if the policy allows a specific action on a resource
|
||||||
|
func (p *S3Policy) HasAction(action S3Action, resource string) bool {
|
||||||
|
for _, statement := range p.Statements {
|
||||||
|
// Check if action matches
|
||||||
|
actionMatches := false
|
||||||
|
for _, stmtAction := range statement.Actions {
|
||||||
|
if stmtAction == action || stmtAction == "s3:*" {
|
||||||
|
actionMatches = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !actionMatches {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if resource matches
|
||||||
|
resourceMatches := false
|
||||||
|
for _, stmtResource := range statement.Resources {
|
||||||
|
if matchResource(stmtResource, resource) {
|
||||||
|
resourceMatches = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !resourceMatches {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a match, check effect
|
||||||
|
if statement.Effect == S3EffectAllow {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchResource checks if a resource pattern matches a specific resource
|
||||||
|
func matchResource(pattern, resource string) bool {
|
||||||
|
if pattern == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if pattern == resource {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Simple wildcard matching for now
|
||||||
|
// In a full implementation, you'd want proper ARN matching
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPresetPolicies returns common preset policies
|
||||||
|
func GetPresetPolicies() map[string]S3Policy {
|
||||||
|
return map[string]S3Policy{
|
||||||
|
"ReadOnly": {
|
||||||
|
Version: "2012-10-17",
|
||||||
|
ID: "ReadOnlyPolicy",
|
||||||
|
Statements: []S3Statement{
|
||||||
|
{
|
||||||
|
Effect: S3EffectAllow,
|
||||||
|
Actions: []S3Action{
|
||||||
|
S3ActionGetObject,
|
||||||
|
S3ActionListBucket,
|
||||||
|
S3ActionGetBucketLocation,
|
||||||
|
},
|
||||||
|
Resources: []string{"*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ReadWrite": {
|
||||||
|
Version: "2012-10-17",
|
||||||
|
ID: "ReadWritePolicy",
|
||||||
|
Statements: []S3Statement{
|
||||||
|
{
|
||||||
|
Effect: S3EffectAllow,
|
||||||
|
Actions: []S3Action{
|
||||||
|
S3ActionGetObject,
|
||||||
|
S3ActionPutObject,
|
||||||
|
S3ActionDeleteObject,
|
||||||
|
S3ActionListBucket,
|
||||||
|
S3ActionGetBucketLocation,
|
||||||
|
S3ActionAbortMultipartUpload,
|
||||||
|
S3ActionListMultipartUploadParts,
|
||||||
|
},
|
||||||
|
Resources: []string{"*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"FullAccess": {
|
||||||
|
Version: "2012-10-17",
|
||||||
|
ID: "FullAccessPolicy",
|
||||||
|
Statements: []S3Statement{
|
||||||
|
{
|
||||||
|
Effect: S3EffectAllow,
|
||||||
|
Actions: []S3Action{"s3:*"},
|
||||||
|
Resources: []string{"*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ObjectLockManager": {
|
||||||
|
Version: "2012-10-17",
|
||||||
|
ID: "ObjectLockManagerPolicy",
|
||||||
|
Statements: []S3Statement{
|
||||||
|
{
|
||||||
|
Effect: S3EffectAllow,
|
||||||
|
Actions: []S3Action{
|
||||||
|
S3ActionGetObject,
|
||||||
|
S3ActionPutObject,
|
||||||
|
S3ActionGetObjectRetention,
|
||||||
|
S3ActionPutObjectRetention,
|
||||||
|
S3ActionGetObjectLegalHold,
|
||||||
|
S3ActionPutObjectLegalHold,
|
||||||
|
S3ActionListBucket,
|
||||||
|
S3ActionGetBucketObjectLockConfiguration,
|
||||||
|
S3ActionPutBucketObjectLockConfiguration,
|
||||||
|
},
|
||||||
|
Resources: []string{"*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertLegacyPermissions converts old permission format to new S3 policy format
|
||||||
|
func ConvertLegacyPermissions(legacy Permissions) S3Policy {
|
||||||
|
var actions []S3Action
|
||||||
|
|
||||||
|
if legacy.Read {
|
||||||
|
actions = append(actions,
|
||||||
|
S3ActionGetObject,
|
||||||
|
S3ActionListBucket,
|
||||||
|
S3ActionGetBucketLocation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if legacy.Write {
|
||||||
|
actions = append(actions,
|
||||||
|
S3ActionPutObject,
|
||||||
|
S3ActionDeleteObject,
|
||||||
|
S3ActionAbortMultipartUpload,
|
||||||
|
S3ActionListMultipartUploadParts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if legacy.Owner {
|
||||||
|
actions = append(actions,
|
||||||
|
S3ActionGetBucketAcl,
|
||||||
|
S3ActionPutBucketAcl,
|
||||||
|
S3ActionGetBucketPolicy,
|
||||||
|
S3ActionPutBucketPolicy,
|
||||||
|
S3ActionDeleteBucketPolicy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return S3Policy{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
ID: "ConvertedLegacyPolicy",
|
||||||
|
Statements: []S3Statement{
|
||||||
|
{
|
||||||
|
Effect: S3EffectAllow,
|
||||||
|
Actions: actions,
|
||||||
|
Resources: []string{"*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON converts policy to JSON string
|
||||||
|
func (p *S3Policy) ToJSON() (string, error) {
|
||||||
|
data, err := json.MarshalIndent(p, "", " ")
|
||||||
|
return string(data), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromJSON creates policy from JSON string
|
||||||
|
func (p *S3Policy) FromJSON(data string) error {
|
||||||
|
return json.Unmarshal([]byte(data), p)
|
||||||
|
}
|
BIN
backend/test-build
Normal file
BIN
backend/test-build
Normal file
Binary file not shown.
530
src/components/s3-permissions/key-permissions-editor.tsx
Normal file
530
src/components/s3-permissions/key-permissions-editor.tsx
Normal file
@ -0,0 +1,530 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
Input,
|
||||||
|
Loading,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
|
Toggle,
|
||||||
|
Alert,
|
||||||
|
} from "react-daisyui";
|
||||||
|
import {
|
||||||
|
KeyPermissions,
|
||||||
|
S3Policy,
|
||||||
|
S3Statement,
|
||||||
|
S3Action,
|
||||||
|
S3_ACTION_GROUPS,
|
||||||
|
PRESET_POLICY_DESCRIPTIONS,
|
||||||
|
LegacyPermissions,
|
||||||
|
} from "@/types/s3-permissions";
|
||||||
|
import {
|
||||||
|
useKeyPermissions,
|
||||||
|
useUpdateKeyPermissions,
|
||||||
|
usePresetPolicies,
|
||||||
|
useValidateS3Policy,
|
||||||
|
} from "@/hooks/useS3Permissions";
|
||||||
|
import { Shield, Settings, AlertCircle, CheckCircle } from "lucide-react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
bucketId: string;
|
||||||
|
accessKeyId: string;
|
||||||
|
keyName: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function KeyPermissionsEditor({
|
||||||
|
bucketId,
|
||||||
|
accessKeyId,
|
||||||
|
keyName,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: Props) {
|
||||||
|
const [permissionMode, setPermissionMode] = useState<"legacy" | "preset" | "custom">("legacy");
|
||||||
|
const [selectedPreset, setSelectedPreset] = useState<string>("");
|
||||||
|
const [customPolicy, setCustomPolicy] = useState<S3Policy>({
|
||||||
|
version: "2012-10-17",
|
||||||
|
statements: [],
|
||||||
|
});
|
||||||
|
const [legacyPermissions, setLegacyPermissions] = useState<LegacyPermissions>({
|
||||||
|
read: false,
|
||||||
|
write: false,
|
||||||
|
owner: false,
|
||||||
|
});
|
||||||
|
const [policyJson, setPolicyJson] = useState<string>("");
|
||||||
|
const [isValidatingPolicy, setIsValidatingPolicy] = useState(false);
|
||||||
|
const [validationResult, setValidationResult] = useState<any>(null);
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
const { data: currentPermissions, isLoading } = useKeyPermissions(bucketId, accessKeyId);
|
||||||
|
const { data: presetPolicies } = usePresetPolicies();
|
||||||
|
const updatePermissions = useUpdateKeyPermissions();
|
||||||
|
const validatePolicy = useValidateS3Policy();
|
||||||
|
|
||||||
|
// Initialize form with current permissions
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentPermissions) {
|
||||||
|
if (currentPermissions.legacy_mode) {
|
||||||
|
setPermissionMode("legacy");
|
||||||
|
setLegacyPermissions(currentPermissions.legacy_permissions || {
|
||||||
|
read: false,
|
||||||
|
write: false,
|
||||||
|
owner: false,
|
||||||
|
});
|
||||||
|
} else if (currentPermissions.s3_policy) {
|
||||||
|
setPermissionMode("custom");
|
||||||
|
setCustomPolicy(currentPermissions.s3_policy);
|
||||||
|
setPolicyJson(currentPermissions.policy_json || "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentPermissions]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const request = {
|
||||||
|
bucket_id: bucketId,
|
||||||
|
access_key_id: accessKeyId,
|
||||||
|
policy_type: permissionMode === "preset" ? "preset" : "custom",
|
||||||
|
legacy_mode: permissionMode === "legacy",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
if (permissionMode === "legacy") {
|
||||||
|
request.legacy = legacyPermissions;
|
||||||
|
} else if (permissionMode === "preset") {
|
||||||
|
request.policy_name = selectedPreset;
|
||||||
|
} else if (permissionMode === "custom") {
|
||||||
|
request.policy = customPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updatePermissions.mutateAsync(request);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating permissions:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValidatePolicy = async () => {
|
||||||
|
setIsValidatingPolicy(true);
|
||||||
|
try {
|
||||||
|
const result = await validatePolicy.mutateAsync(customPolicy);
|
||||||
|
setValidationResult(result.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error validating policy:", error);
|
||||||
|
} finally {
|
||||||
|
setIsValidatingPolicy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePolicyJsonChange = (value: string) => {
|
||||||
|
setPolicyJson(value);
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
setCustomPolicy(parsed);
|
||||||
|
setValidationResult(null);
|
||||||
|
} catch (error) {
|
||||||
|
// Invalid JSON, don't update the policy object
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addStatement = () => {
|
||||||
|
setCustomPolicy(prev => ({
|
||||||
|
...prev,
|
||||||
|
statements: [
|
||||||
|
...prev.statements,
|
||||||
|
{
|
||||||
|
effect: "Allow",
|
||||||
|
actions: [],
|
||||||
|
resources: ["*"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStatement = (index: number, statement: S3Statement) => {
|
||||||
|
setCustomPolicy(prev => ({
|
||||||
|
...prev,
|
||||||
|
statements: prev.statements.map((s, i) => (i === index ? statement : s)),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeStatement = (index: number) => {
|
||||||
|
setCustomPolicy(prev => ({
|
||||||
|
...prev,
|
||||||
|
statements: prev.statements.filter((_, i) => i !== index),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} onClickBackdrop={onClose}>
|
||||||
|
<Modal.Header>
|
||||||
|
<h3>Editando Permisos de Llave</h3>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="text-center py-8">
|
||||||
|
<Loading />
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} onClickBackdrop={onClose} className="w-11/12 max-w-4xl">
|
||||||
|
<Modal.Header>
|
||||||
|
<h3 className="flex items-center gap-2">
|
||||||
|
<Shield size={20} />
|
||||||
|
Permisos de Llave: {keyName}
|
||||||
|
</h3>
|
||||||
|
</Modal.Header>
|
||||||
|
|
||||||
|
<Modal.Body className="space-y-6">
|
||||||
|
{/* Permission Mode Selection */}
|
||||||
|
<Card className="bg-base-200 p-4">
|
||||||
|
<h4 className="font-semibold mb-3">Tipo de Permisos</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="permission-mode"
|
||||||
|
value="legacy"
|
||||||
|
checked={permissionMode === "legacy"}
|
||||||
|
onChange={(e) => setPermissionMode(e.target.value as any)}
|
||||||
|
className="radio radio-primary"
|
||||||
|
/>
|
||||||
|
<span>Permisos Simples (Lectura/Escritura/Propietario)</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="permission-mode"
|
||||||
|
value="preset"
|
||||||
|
checked={permissionMode === "preset"}
|
||||||
|
onChange={(e) => setPermissionMode(e.target.value as any)}
|
||||||
|
className="radio radio-primary"
|
||||||
|
/>
|
||||||
|
<span>Políticas Predefinidas (AWS S3)</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="permission-mode"
|
||||||
|
value="custom"
|
||||||
|
checked={permissionMode === "custom"}
|
||||||
|
onChange={(e) => setPermissionMode(e.target.value as any)}
|
||||||
|
className="radio radio-primary"
|
||||||
|
/>
|
||||||
|
<span>Política Personalizada (Avanzado)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Legacy Permissions */}
|
||||||
|
{permissionMode === "legacy" && (
|
||||||
|
<Card className="bg-base-100 p-4">
|
||||||
|
<h4 className="font-semibold mb-3">Permisos Simples</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<Checkbox
|
||||||
|
checked={legacyPermissions.read}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLegacyPermissions(prev => ({ ...prev, read: e.target.checked }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>Lectura - Permite descargar objetos y listar el bucket</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<Checkbox
|
||||||
|
checked={legacyPermissions.write}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLegacyPermissions(prev => ({ ...prev, write: e.target.checked }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>Escritura - Permite subir y eliminar objetos</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<Checkbox
|
||||||
|
checked={legacyPermissions.owner}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLegacyPermissions(prev => ({ ...prev, owner: e.target.checked }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>Propietario - Permisos administrativos completos</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Preset Policies */}
|
||||||
|
{permissionMode === "preset" && (
|
||||||
|
<Card className="bg-base-100 p-4">
|
||||||
|
<h4 className="font-semibold mb-3">Políticas Predefinidas</h4>
|
||||||
|
<Select
|
||||||
|
value={selectedPreset}
|
||||||
|
onChange={(e) => setSelectedPreset(e.target.value)}
|
||||||
|
className="w-full mb-3"
|
||||||
|
>
|
||||||
|
<option value="">Seleccionar política...</option>
|
||||||
|
{presetPolicies &&
|
||||||
|
Object.entries(presetPolicies).map(([name, policy]) => (
|
||||||
|
<option key={name} value={name}>
|
||||||
|
{name} - {policy.description}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{selectedPreset && presetPolicies?.[selectedPreset] && (
|
||||||
|
<div className="bg-base-200 p-3 rounded">
|
||||||
|
<p className="text-sm mb-2">
|
||||||
|
<strong>Descripción:</strong> {presetPolicies[selectedPreset].description}
|
||||||
|
</p>
|
||||||
|
<details>
|
||||||
|
<summary className="cursor-pointer text-sm font-medium">
|
||||||
|
Ver política JSON
|
||||||
|
</summary>
|
||||||
|
<pre className="text-xs mt-2 bg-base-300 p-2 rounded overflow-x-auto">
|
||||||
|
{presetPolicies[selectedPreset].policy_json}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Custom Policy */}
|
||||||
|
{permissionMode === "custom" && (
|
||||||
|
<Card className="bg-base-100 p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h4 className="font-semibold">Política Personalizada</h4>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleValidatePolicy}
|
||||||
|
disabled={isValidatingPolicy}
|
||||||
|
>
|
||||||
|
{isValidatingPolicy ? <Loading size="xs" /> : "Validar"}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={addStatement}>
|
||||||
|
Agregar Declaración
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Results */}
|
||||||
|
{validationResult && (
|
||||||
|
<Alert
|
||||||
|
status={validationResult.valid ? "success" : "error"}
|
||||||
|
icon={validationResult.valid ? <CheckCircle /> : <AlertCircle />}
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold">
|
||||||
|
{validationResult.valid ? "Política Válida" : "Política Inválida"}
|
||||||
|
</div>
|
||||||
|
{validationResult.message && <div className="text-sm">{validationResult.message}</div>}
|
||||||
|
{validationResult.errors && validationResult.errors.length > 0 && (
|
||||||
|
<ul className="text-sm mt-1 list-disc list-inside">
|
||||||
|
{validationResult.errors.map((error: string, idx: number) => (
|
||||||
|
<li key={idx}>{error}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Policy Editor */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Versión</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={customPolicy.version}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCustomPolicy(prev => ({ ...prev, version: e.target.value }))
|
||||||
|
}
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">ID de Política (Opcional)</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={customPolicy.id || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCustomPolicy(prev => ({ ...prev, id: e.target.value || undefined }))
|
||||||
|
}
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Statements */}
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Declaraciones</span>
|
||||||
|
</label>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{customPolicy.statements.map((statement, index) => (
|
||||||
|
<StatementEditor
|
||||||
|
key={index}
|
||||||
|
statement={statement}
|
||||||
|
onChange={(updatedStatement) => updateStatement(index, updatedStatement)}
|
||||||
|
onRemove={() => removeStatement(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* JSON Editor */}
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Editor JSON (Avanzado)</span>
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={policyJson || JSON.stringify(customPolicy, null, 2)}
|
||||||
|
onChange={(e) => handlePolicyJsonChange(e.target.value)}
|
||||||
|
className="font-mono text-sm"
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Modal.Body>
|
||||||
|
|
||||||
|
<Modal.Actions>
|
||||||
|
<Button onClick={onClose} variant="outline">
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
loading={updatePermissions.isPending}
|
||||||
|
disabled={
|
||||||
|
permissionMode === "preset" && !selectedPreset ||
|
||||||
|
updatePermissions.isPending
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Guardar Permisos
|
||||||
|
</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statement Editor Component
|
||||||
|
interface StatementEditorProps {
|
||||||
|
statement: S3Statement;
|
||||||
|
onChange: (statement: S3Statement) => void;
|
||||||
|
onRemove: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatementEditor({ statement, onChange, onRemove }: StatementEditorProps) {
|
||||||
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
|
|
||||||
|
const handleActionToggle = (action: S3Action, checked: boolean) => {
|
||||||
|
const newActions = checked
|
||||||
|
? [...statement.actions, action]
|
||||||
|
: statement.actions.filter(a => a !== action);
|
||||||
|
|
||||||
|
onChange({ ...statement, actions: newActions });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-base-200 p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h5 className="font-medium">Declaración</h5>
|
||||||
|
<Button size="xs" variant="outline" color="error" onClick={onRemove}>
|
||||||
|
Eliminar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Efecto</span>
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={statement.effect}
|
||||||
|
onChange={(e) => onChange({ ...statement, effect: e.target.value as any })}
|
||||||
|
className="select-sm"
|
||||||
|
>
|
||||||
|
<option value="Allow">Permitir</option>
|
||||||
|
<option value="Deny">Denegar</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Recursos</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={statement.resources.join(", ")}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...statement,
|
||||||
|
resources: e.target.value.split(",").map(s => s.trim()).filter(Boolean),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="input-sm"
|
||||||
|
placeholder="*, arn:aws:s3:::bucket/*"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Acciones</span>
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||||
|
>
|
||||||
|
{showAdvanced ? "Vista Simple" : "Vista Avanzada"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showAdvanced ? (
|
||||||
|
<Textarea
|
||||||
|
value={statement.actions.join("\n")}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...statement,
|
||||||
|
actions: e.target.value.split("\n").filter(Boolean) as S3Action[],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="textarea-sm font-mono"
|
||||||
|
rows={5}
|
||||||
|
placeholder="s3:GetObject s3:PutObject s3:ListBucket"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Object.entries(S3_ACTION_GROUPS).map(([groupName, actions]) => (
|
||||||
|
<div key={groupName}>
|
||||||
|
<h6 className="text-sm font-medium mb-1">{groupName}</h6>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-1">
|
||||||
|
{actions.map((action) => (
|
||||||
|
<label key={action} className="flex items-center gap-2 cursor-pointer text-sm">
|
||||||
|
<Checkbox
|
||||||
|
size="xs"
|
||||||
|
checked={statement.actions.includes(action)}
|
||||||
|
onChange={(e) => handleActionToggle(action, e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="font-mono">{action}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
558
src/components/s3-permissions/object-locking-manager.tsx
Normal file
558
src/components/s3-permissions/object-locking-manager.tsx
Normal file
@ -0,0 +1,558 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
Loading,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Toggle,
|
||||||
|
Alert,
|
||||||
|
Badge,
|
||||||
|
Table,
|
||||||
|
} from "react-daisyui";
|
||||||
|
import {
|
||||||
|
ObjectLockConfiguration,
|
||||||
|
ObjectRetention,
|
||||||
|
ObjectLegalHold,
|
||||||
|
ObjectWithLocking,
|
||||||
|
DefaultRetention,
|
||||||
|
ObjectLockRetentionMode,
|
||||||
|
ObjectLegalHoldStatus,
|
||||||
|
} from "@/types/s3-permissions";
|
||||||
|
import {
|
||||||
|
useBucketObjectLockConfiguration,
|
||||||
|
useUpdateBucketObjectLockConfiguration,
|
||||||
|
useObjectsWithLocking,
|
||||||
|
useUpdateObjectRetention,
|
||||||
|
useUpdateObjectLegalHold,
|
||||||
|
} from "@/hooks/useS3Permissions";
|
||||||
|
import {
|
||||||
|
Lock,
|
||||||
|
Unlock,
|
||||||
|
Shield,
|
||||||
|
Settings,
|
||||||
|
AlertTriangle,
|
||||||
|
Calendar,
|
||||||
|
FileText,
|
||||||
|
Clock
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
bucketId: string;
|
||||||
|
bucketName: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ObjectLockingManager({ bucketId, bucketName, isOpen, onClose }: Props) {
|
||||||
|
const [activeTab, setActiveTab] = useState<"config" | "objects">("config");
|
||||||
|
const [showRetentionModal, setShowRetentionModal] = useState(false);
|
||||||
|
const [showLegalHoldModal, setShowLegalHoldModal] = useState(false);
|
||||||
|
const [selectedObject, setSelectedObject] = useState<ObjectWithLocking | null>(null);
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
const { data: lockConfig, isLoading } = useBucketObjectLockConfiguration(bucketId);
|
||||||
|
const updateLockConfig = useUpdateBucketObjectLockConfiguration();
|
||||||
|
const { data: objectsData } = useObjectsWithLocking(bucketId);
|
||||||
|
const updateRetention = useUpdateObjectRetention();
|
||||||
|
const updateLegalHold = useUpdateObjectLegalHold();
|
||||||
|
|
||||||
|
const handleConfigUpdate = async (config: ObjectLockConfiguration) => {
|
||||||
|
try {
|
||||||
|
await updateLockConfig.mutateAsync({ bucketId, config });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating object lock configuration:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} onClickBackdrop={onClose} className="w-11/12 max-w-6xl">
|
||||||
|
<Modal.Header>
|
||||||
|
<h3>Object Lock - {bucketName}</h3>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="text-center py-8">
|
||||||
|
<Loading />
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal open={isOpen} onClickBackdrop={onClose} className="w-11/12 max-w-6xl">
|
||||||
|
<Modal.Header>
|
||||||
|
<h3 className="flex items-center gap-2">
|
||||||
|
<Lock size={20} />
|
||||||
|
Object Lock - {bucketName}
|
||||||
|
</h3>
|
||||||
|
</Modal.Header>
|
||||||
|
|
||||||
|
<Modal.Body className="space-y-6">
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="tabs tabs-boxed">
|
||||||
|
<button
|
||||||
|
className={`tab ${activeTab === "config" ? "tab-active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("config")}
|
||||||
|
>
|
||||||
|
<Settings size={16} className="mr-2" />
|
||||||
|
Configuración
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`tab ${activeTab === "objects" ? "tab-active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("objects")}
|
||||||
|
>
|
||||||
|
<FileText size={16} className="mr-2" />
|
||||||
|
Objetos Bloqueados
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Configuration Tab */}
|
||||||
|
{activeTab === "config" && (
|
||||||
|
<BucketLockConfiguration
|
||||||
|
config={lockConfig?.object_lock_configuration}
|
||||||
|
onUpdate={handleConfigUpdate}
|
||||||
|
isUpdating={updateLockConfig.isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Objects Tab */}
|
||||||
|
{activeTab === "objects" && (
|
||||||
|
<ObjectsWithLockingTable
|
||||||
|
objects={objectsData?.objects || []}
|
||||||
|
onEditRetention={(obj) => {
|
||||||
|
setSelectedObject(obj);
|
||||||
|
setShowRetentionModal(true);
|
||||||
|
}}
|
||||||
|
onEditLegalHold={(obj) => {
|
||||||
|
setSelectedObject(obj);
|
||||||
|
setShowLegalHoldModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal.Body>
|
||||||
|
|
||||||
|
<Modal.Actions>
|
||||||
|
<Button onClick={onClose}>Cerrar</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Retention Modal */}
|
||||||
|
{showRetentionModal && selectedObject && (
|
||||||
|
<RetentionEditor
|
||||||
|
bucketId={bucketId}
|
||||||
|
object={selectedObject}
|
||||||
|
onClose={() => {
|
||||||
|
setShowRetentionModal(false);
|
||||||
|
setSelectedObject(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Legal Hold Modal */}
|
||||||
|
{showLegalHoldModal && selectedObject && (
|
||||||
|
<LegalHoldEditor
|
||||||
|
bucketId={bucketId}
|
||||||
|
object={selectedObject}
|
||||||
|
onClose={() => {
|
||||||
|
setShowLegalHoldModal(false);
|
||||||
|
setSelectedObject(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucket Lock Configuration Component
|
||||||
|
interface BucketLockConfigurationProps {
|
||||||
|
config?: ObjectLockConfiguration;
|
||||||
|
onUpdate: (config: ObjectLockConfiguration) => void;
|
||||||
|
isUpdating: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BucketLockConfiguration({ config, onUpdate, isUpdating }: BucketLockConfigurationProps) {
|
||||||
|
const [enabled, setEnabled] = useState(config?.object_lock_enabled || false);
|
||||||
|
const [hasDefaultRetention, setHasDefaultRetention] = useState(!!config?.rule?.default_retention);
|
||||||
|
const [defaultRetention, setDefaultRetention] = useState<DefaultRetention>({
|
||||||
|
mode: "COMPLIANCE",
|
||||||
|
days: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const newConfig: ObjectLockConfiguration = {
|
||||||
|
object_lock_enabled: enabled,
|
||||||
|
rule: hasDefaultRetention ? { default_retention: defaultRetention } : undefined,
|
||||||
|
};
|
||||||
|
onUpdate(newConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Object Lock Status */}
|
||||||
|
<Card className="bg-base-100 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold flex items-center gap-2">
|
||||||
|
<Shield size={18} />
|
||||||
|
Estado de Object Lock
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-base-content/60 mt-1">
|
||||||
|
{enabled ? "Object Lock está habilitado en este bucket" : "Object Lock está deshabilitado"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm">Deshabilitado</span>
|
||||||
|
<Toggle
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(e) => setEnabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Habilitado</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enabled && (
|
||||||
|
<Alert status="warning" className="mt-4">
|
||||||
|
<AlertTriangle size={16} />
|
||||||
|
<div>
|
||||||
|
<div className="font-bold">Importante</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
Una vez habilitado, Object Lock no se puede deshabilitar. Los objetos pueden tener
|
||||||
|
retención y legal holds aplicados.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Default Retention Configuration */}
|
||||||
|
{enabled && (
|
||||||
|
<Card className="bg-base-100 p-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold flex items-center gap-2">
|
||||||
|
<Clock size={18} />
|
||||||
|
Retención por Defecto
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-base-content/60 mt-1">
|
||||||
|
Configurar retención automática para nuevos objetos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={hasDefaultRetention}
|
||||||
|
onChange={(e) => setHasDefaultRetention(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasDefaultRetention && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Modo de Retención</span>
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={defaultRetention.mode}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDefaultRetention(prev => ({
|
||||||
|
...prev,
|
||||||
|
mode: e.target.value as ObjectLockRetentionMode,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="COMPLIANCE">Compliance</option>
|
||||||
|
<option value="GOVERNANCE">Governance</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Duración (Días)</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={defaultRetention.days || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDefaultRetention(prev => ({
|
||||||
|
...prev,
|
||||||
|
days: parseInt(e.target.value) || undefined,
|
||||||
|
years: undefined,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Ej: 30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">O Años</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={defaultRetention.years || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDefaultRetention(prev => ({
|
||||||
|
...prev,
|
||||||
|
years: parseInt(e.target.value) || undefined,
|
||||||
|
days: undefined,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Ej: 1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasDefaultRetention && (
|
||||||
|
<div className="mt-4 bg-base-200 p-3 rounded">
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
<div><strong>Compliance:</strong> Los objetos no pueden ser eliminados hasta que expire la retención</div>
|
||||||
|
<div><strong>Governance:</strong> Usuarios con permisos especiales pueden eliminar objetos antes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
loading={isUpdating}
|
||||||
|
disabled={isUpdating}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Guardar Configuración
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Objects with Locking Table Component
|
||||||
|
interface ObjectsWithLockingTableProps {
|
||||||
|
objects: ObjectWithLocking[];
|
||||||
|
onEditRetention: (obj: ObjectWithLocking) => void;
|
||||||
|
onEditLegalHold: (obj: ObjectWithLocking) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ObjectsWithLockingTable({
|
||||||
|
objects,
|
||||||
|
onEditRetention,
|
||||||
|
onEditLegalHold
|
||||||
|
}: ObjectsWithLockingTableProps) {
|
||||||
|
if (objects.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-base-100 p-8 text-center">
|
||||||
|
<FileText size={48} className="mx-auto text-base-content/40 mb-4" />
|
||||||
|
<h4 className="font-semibold mb-2">No hay objetos con Object Lock</h4>
|
||||||
|
<p className="text-base-content/60">
|
||||||
|
Los objetos aparecerán aquí una vez que tengan configuraciones de retención o legal holds
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<Table.Head>
|
||||||
|
<span>Objeto</span>
|
||||||
|
<span>Tamaño</span>
|
||||||
|
<span>Retención</span>
|
||||||
|
<span>Legal Hold</span>
|
||||||
|
<span>Acciones</span>
|
||||||
|
</Table.Head>
|
||||||
|
<Table.Body>
|
||||||
|
{objects.map((obj) => (
|
||||||
|
<tr key={obj.key}>
|
||||||
|
<td>
|
||||||
|
<div className="font-mono text-sm">{obj.key}</div>
|
||||||
|
<div className="text-xs text-base-content/60">
|
||||||
|
Modificado: {new Date(obj.last_modified).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap">
|
||||||
|
{(obj.size / 1024 / 1024).toFixed(2)} MB
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{obj.retention ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Badge color={obj.retention.mode === "COMPLIANCE" ? "error" : "warning"}>
|
||||||
|
{obj.retention.mode}
|
||||||
|
</Badge>
|
||||||
|
<div className="text-xs">
|
||||||
|
Hasta: {new Date(obj.retention.retain_until_date).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-base-content/60">Sin retención</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Badge color={obj.legal_hold?.status === "ON" ? "error" : "success"}>
|
||||||
|
{obj.legal_hold?.status === "ON" ? "Activo" : "Inactivo"}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onEditRetention(obj)}
|
||||||
|
>
|
||||||
|
<Calendar size={12} />
|
||||||
|
Retención
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onEditLegalHold(obj)}
|
||||||
|
>
|
||||||
|
<Shield size={12} />
|
||||||
|
Legal Hold
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retention Editor Modal
|
||||||
|
interface RetentionEditorProps {
|
||||||
|
bucketId: string;
|
||||||
|
object: ObjectWithLocking;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RetentionEditor({ bucketId, object, onClose }: RetentionEditorProps) {
|
||||||
|
const [mode, setMode] = useState<ObjectLockRetentionMode>("COMPLIANCE");
|
||||||
|
const [date, setDate] = useState("");
|
||||||
|
|
||||||
|
const updateRetention = useUpdateObjectRetention();
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const retention: ObjectRetention = {
|
||||||
|
mode,
|
||||||
|
retain_until_date: date,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateRetention.mutateAsync({
|
||||||
|
bucketId,
|
||||||
|
objectKey: object.key,
|
||||||
|
retention,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating retention:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open onClickBackdrop={onClose}>
|
||||||
|
<Modal.Header>
|
||||||
|
<h3>Configurar Retención - {object.key}</h3>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Modo de Retención</span>
|
||||||
|
</label>
|
||||||
|
<Select value={mode} onChange={(e) => setMode(e.target.value as ObjectLockRetentionMode)}>
|
||||||
|
<option value="COMPLIANCE">Compliance</option>
|
||||||
|
<option value="GOVERNANCE">Governance</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Fecha de Retención</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
value={date}
|
||||||
|
onChange={(e) => setDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Actions>
|
||||||
|
<Button onClick={onClose} variant="outline">
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} loading={updateRetention.isPending}>
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legal Hold Editor Modal
|
||||||
|
interface LegalHoldEditorProps {
|
||||||
|
bucketId: string;
|
||||||
|
object: ObjectWithLocking;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegalHoldEditor({ bucketId, object, onClose }: LegalHoldEditorProps) {
|
||||||
|
const [status, setStatus] = useState<ObjectLegalHoldStatus>("OFF");
|
||||||
|
|
||||||
|
const updateLegalHold = useUpdateObjectLegalHold();
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const legalHold: ObjectLegalHold = { status };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateLegalHold.mutateAsync({
|
||||||
|
bucketId,
|
||||||
|
objectKey: object.key,
|
||||||
|
legalHold,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating legal hold:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open onClickBackdrop={onClose}>
|
||||||
|
<Modal.Header>
|
||||||
|
<h3>Configurar Legal Hold - {object.key}</h3>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Estado de Legal Hold</span>
|
||||||
|
</label>
|
||||||
|
<Select value={status} onChange={(e) => setStatus(e.target.value as ObjectLegalHoldStatus)}>
|
||||||
|
<option value="OFF">Desactivado</option>
|
||||||
|
<option value="ON">Activado</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert status="info">
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="font-bold">Legal Hold</div>
|
||||||
|
<div>
|
||||||
|
Cuando está activado, el objeto no puede ser eliminado independientemente
|
||||||
|
de su configuración de retención.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Actions>
|
||||||
|
<Button onClick={onClose} variant="outline">
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} loading={updateLegalHold.isPending}>
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
230
src/hooks/useS3Permissions.ts
Normal file
230
src/hooks/useS3Permissions.ts
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import api from "@/lib/api";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
KeyPermissions,
|
||||||
|
UpdateKeyPermissionsRequest,
|
||||||
|
PresetPolicy,
|
||||||
|
ValidatePolicyResponse,
|
||||||
|
S3Policy,
|
||||||
|
ObjectLockConfiguration,
|
||||||
|
ObjectRetention,
|
||||||
|
ObjectLegalHold,
|
||||||
|
ListObjectsWithLockingResponse,
|
||||||
|
} from "@/types/s3-permissions";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
// Key Permissions hooks
|
||||||
|
export const useKeyPermissions = (bucketId: string, accessKeyId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["key-permissions", bucketId, accessKeyId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get<KeyPermissions>(
|
||||||
|
`/buckets/${bucketId}/keys/${accessKeyId}/permissions`
|
||||||
|
);
|
||||||
|
return response?.data;
|
||||||
|
},
|
||||||
|
enabled: !!bucketId && !!accessKeyId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateKeyPermissions = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: UpdateKeyPermissionsRequest) =>
|
||||||
|
api.put(`/buckets/${data.bucket_id}/keys/${data.access_key_id}/permissions`, {
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["key-permissions", variables.bucket_id, variables.access_key_id],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["buckets"],
|
||||||
|
});
|
||||||
|
toast.success("Permisos de llave actualizados exitosamente");
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.message || "Error al actualizar permisos de llave");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preset Policies hooks
|
||||||
|
export const usePresetPolicies = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["preset-policies"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get<Record<string, PresetPolicy>>("/s3/policies/presets");
|
||||||
|
return response?.data || {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useValidateS3Policy = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (policy: S3Policy) =>
|
||||||
|
api.post<ValidatePolicyResponse>("/s3/policies/validate", { body: policy }),
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.message || "Error al validar política");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Object Locking hooks
|
||||||
|
export const useBucketObjectLockConfiguration = (bucketId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["bucket-object-lock", bucketId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get<{
|
||||||
|
bucket_id: string;
|
||||||
|
object_lock_configuration: ObjectLockConfiguration;
|
||||||
|
object_lock_enabled: boolean;
|
||||||
|
}>(`/buckets/${bucketId}/object-lock`);
|
||||||
|
return response?.data;
|
||||||
|
},
|
||||||
|
enabled: !!bucketId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateBucketObjectLockConfiguration = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ bucketId, config }: { bucketId: string; config: ObjectLockConfiguration }) =>
|
||||||
|
api.put(`/buckets/${bucketId}/object-lock`, {
|
||||||
|
body: {
|
||||||
|
bucket_id: bucketId,
|
||||||
|
object_lock_configuration: config,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["bucket-object-lock", variables.bucketId],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["buckets"],
|
||||||
|
});
|
||||||
|
toast.success("Configuración de Object Lock actualizada exitosamente");
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.message || "Error al actualizar configuración de Object Lock");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useObjectsWithLocking = (
|
||||||
|
bucketId: string,
|
||||||
|
options?: { prefix?: string; delimiter?: string }
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["objects-with-locking", bucketId, options],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get<ListObjectsWithLockingResponse>(`/buckets/${bucketId}/objects`, {
|
||||||
|
params: options,
|
||||||
|
});
|
||||||
|
return response?.data;
|
||||||
|
},
|
||||||
|
enabled: !!bucketId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useObjectRetention = (bucketId: string, objectKey: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["object-retention", bucketId, objectKey],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get<{
|
||||||
|
bucket_id: string;
|
||||||
|
object_key: string;
|
||||||
|
retention: ObjectRetention;
|
||||||
|
}>(`/buckets/${bucketId}/objects/${encodeURIComponent(objectKey)}/retention`);
|
||||||
|
return response?.data;
|
||||||
|
},
|
||||||
|
enabled: !!bucketId && !!objectKey,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateObjectRetention = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
bucketId,
|
||||||
|
objectKey,
|
||||||
|
retention,
|
||||||
|
}: {
|
||||||
|
bucketId: string;
|
||||||
|
objectKey: string;
|
||||||
|
retention: ObjectRetention;
|
||||||
|
}) =>
|
||||||
|
api.put(`/buckets/${bucketId}/objects/${encodeURIComponent(objectKey)}/retention`, {
|
||||||
|
body: {
|
||||||
|
bucket_id: bucketId,
|
||||||
|
object_key: objectKey,
|
||||||
|
retention,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["object-retention", variables.bucketId, variables.objectKey],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["objects-with-locking", variables.bucketId],
|
||||||
|
});
|
||||||
|
toast.success("Retención de objeto actualizada exitosamente");
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.message || "Error al actualizar retención de objeto");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useObjectLegalHold = (bucketId: string, objectKey: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["object-legal-hold", bucketId, objectKey],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get<{
|
||||||
|
bucket_id: string;
|
||||||
|
object_key: string;
|
||||||
|
legal_hold: ObjectLegalHold;
|
||||||
|
}>(`/buckets/${bucketId}/objects/${encodeURIComponent(objectKey)}/legal-hold`);
|
||||||
|
return response?.data;
|
||||||
|
},
|
||||||
|
enabled: !!bucketId && !!objectKey,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateObjectLegalHold = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
bucketId,
|
||||||
|
objectKey,
|
||||||
|
legalHold,
|
||||||
|
}: {
|
||||||
|
bucketId: string;
|
||||||
|
objectKey: string;
|
||||||
|
legalHold: ObjectLegalHold;
|
||||||
|
}) =>
|
||||||
|
api.put(`/buckets/${bucketId}/objects/${encodeURIComponent(objectKey)}/legal-hold`, {
|
||||||
|
body: {
|
||||||
|
bucket_id: bucketId,
|
||||||
|
object_key: objectKey,
|
||||||
|
legal_hold: legalHold,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["object-legal-hold", variables.bucketId, variables.objectKey],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["objects-with-locking", variables.bucketId],
|
||||||
|
});
|
||||||
|
toast.success("Legal Hold actualizado exitosamente");
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.message || "Error al actualizar Legal Hold");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -1,15 +1,20 @@
|
|||||||
import { useDenyKey } from "../hooks";
|
import { useDenyKey } from "../hooks";
|
||||||
import { Card, Checkbox, Table } from "react-daisyui";
|
import { Card, Checkbox, Table } from "react-daisyui";
|
||||||
import Button from "@/components/ui/button";
|
import Button from "@/components/ui/button";
|
||||||
import { Trash } from "lucide-react";
|
import { Trash, Shield, Lock, Settings } from "lucide-react";
|
||||||
import AllowKeyDialog from "./allow-key-dialog";
|
import AllowKeyDialog from "./allow-key-dialog";
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { handleError } from "@/lib/utils";
|
import { handleError } from "@/lib/utils";
|
||||||
import { useBucketContext } from "../context";
|
import { useBucketContext } from "../context";
|
||||||
|
import KeyPermissionsEditor from "@/components/s3-permissions/key-permissions-editor";
|
||||||
|
import ObjectLockingManager from "@/components/s3-permissions/object-locking-manager";
|
||||||
|
|
||||||
const PermissionsTab = () => {
|
const PermissionsTab = () => {
|
||||||
const { bucket, refetch } = useBucketContext();
|
const { bucket, refetch } = useBucketContext();
|
||||||
|
const [selectedKey, setSelectedKey] = useState<{accessKeyId: string, name: string} | null>(null);
|
||||||
|
const [showKeyEditor, setShowKeyEditor] = useState(false);
|
||||||
|
const [showObjectLocking, setShowObjectLocking] = useState(false);
|
||||||
|
|
||||||
const denyKey = useDenyKey(bucket.id, {
|
const denyKey = useDenyKey(bucket.id, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -37,11 +42,26 @@ const PermissionsTab = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onEditPermissions = (key: any) => {
|
||||||
|
setSelectedKey({
|
||||||
|
accessKeyId: key.accessKeyId,
|
||||||
|
name: key.name || key.accessKeyId?.substring(0, 8)
|
||||||
|
});
|
||||||
|
setShowKeyEditor(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card className="card-body">
|
<Card className="card-body">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<Card.Title className="flex-1 truncate">Access Keys</Card.Title>
|
<Card.Title className="flex-1 truncate">Access Keys</Card.Title>
|
||||||
|
<Button
|
||||||
|
icon={Lock}
|
||||||
|
onClick={() => setShowObjectLocking(true)}
|
||||||
|
className="btn-outline btn-sm"
|
||||||
|
>
|
||||||
|
Object Lock
|
||||||
|
</Button>
|
||||||
<AllowKeyDialog currentKeys={keys?.map((key) => key.accessKeyId)} />
|
<AllowKeyDialog currentKeys={keys?.map((key) => key.accessKeyId)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -54,6 +74,7 @@ const PermissionsTab = () => {
|
|||||||
<span>Read</span>
|
<span>Read</span>
|
||||||
<span>Write</span>
|
<span>Write</span>
|
||||||
<span>Owner</span>
|
<span>Owner</span>
|
||||||
|
<span>S3 Permisos</span>
|
||||||
<span />
|
<span />
|
||||||
</Table.Head>
|
</Table.Head>
|
||||||
|
|
||||||
@ -84,6 +105,15 @@ const PermissionsTab = () => {
|
|||||||
className="cursor-default"
|
className="cursor-default"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
icon={Shield}
|
||||||
|
onClick={() => onEditPermissions(key)}
|
||||||
|
className="btn-outline btn-xs"
|
||||||
|
>
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
<Button
|
<Button
|
||||||
icon={Trash}
|
icon={Trash}
|
||||||
onClick={() => onRemove(key.accessKeyId)}
|
onClick={() => onRemove(key.accessKeyId)}
|
||||||
@ -94,6 +124,31 @@ const PermissionsTab = () => {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* S3 Permissions Editor Modal */}
|
||||||
|
{showKeyEditor && selectedKey && (
|
||||||
|
<KeyPermissionsEditor
|
||||||
|
bucketId={bucket.id}
|
||||||
|
accessKeyId={selectedKey.accessKeyId}
|
||||||
|
keyName={selectedKey.name}
|
||||||
|
isOpen={showKeyEditor}
|
||||||
|
onClose={() => {
|
||||||
|
setShowKeyEditor(false);
|
||||||
|
setSelectedKey(null);
|
||||||
|
refetch(); // Refresh bucket data after permission changes
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Object Locking Manager Modal */}
|
||||||
|
{showObjectLocking && (
|
||||||
|
<ObjectLockingManager
|
||||||
|
bucketId={bucket.id}
|
||||||
|
bucketName={bucket.globalAliases?.[0] || bucket.id}
|
||||||
|
isOpen={showObjectLocking}
|
||||||
|
onClose={() => setShowObjectLocking(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
212
src/types/s3-permissions.ts
Normal file
212
src/types/s3-permissions.ts
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// S3 Action types matching the backend
|
||||||
|
export type S3Action =
|
||||||
|
// Object-level permissions
|
||||||
|
| "s3:GetObject"
|
||||||
|
| "s3:PutObject"
|
||||||
|
| "s3:DeleteObject"
|
||||||
|
| "s3:GetObjectAcl"
|
||||||
|
| "s3:PutObjectAcl"
|
||||||
|
| "s3:GetObjectVersion"
|
||||||
|
| "s3:DeleteObjectVersion"
|
||||||
|
|
||||||
|
// Object locking permissions
|
||||||
|
| "s3:PutObjectLegalHold"
|
||||||
|
| "s3:GetObjectLegalHold"
|
||||||
|
| "s3:PutObjectRetention"
|
||||||
|
| "s3:GetObjectRetention"
|
||||||
|
| "s3:BypassGovernanceRetention"
|
||||||
|
|
||||||
|
// Multipart upload permissions
|
||||||
|
| "s3:AbortMultipartUpload"
|
||||||
|
| "s3:ListMultipartUploadParts"
|
||||||
|
|
||||||
|
// Bucket-level permissions
|
||||||
|
| "s3:ListBucket"
|
||||||
|
| "s3:ListBucketVersions"
|
||||||
|
| "s3:GetBucketLocation"
|
||||||
|
| "s3:GetBucketAcl"
|
||||||
|
| "s3:PutBucketAcl"
|
||||||
|
| "s3:GetBucketPolicy"
|
||||||
|
| "s3:PutBucketPolicy"
|
||||||
|
| "s3:DeleteBucketPolicy"
|
||||||
|
| "s3:GetBucketVersioning"
|
||||||
|
| "s3:PutBucketVersioning"
|
||||||
|
| "s3:GetBucketObjectLockConfiguration"
|
||||||
|
| "s3:PutBucketObjectLockConfiguration"
|
||||||
|
|
||||||
|
// Bucket management permissions
|
||||||
|
| "s3:CreateBucket"
|
||||||
|
| "s3:DeleteBucket"
|
||||||
|
|
||||||
|
// List permissions
|
||||||
|
| "s3:ListAllMyBuckets"
|
||||||
|
| "s3:ListBucketMultipartUploads"
|
||||||
|
|
||||||
|
// Wildcard
|
||||||
|
| "s3:*";
|
||||||
|
|
||||||
|
export type S3Effect = "Allow" | "Deny";
|
||||||
|
|
||||||
|
export interface S3Condition {
|
||||||
|
StringEquals?: Record<string, any>;
|
||||||
|
StringNotEquals?: Record<string, any>;
|
||||||
|
StringLike?: Record<string, any>;
|
||||||
|
StringNotLike?: Record<string, any>;
|
||||||
|
IpAddress?: Record<string, any>;
|
||||||
|
NotIpAddress?: Record<string, any>;
|
||||||
|
DateGreaterThan?: Record<string, any>;
|
||||||
|
DateLessThan?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3Statement {
|
||||||
|
id?: string;
|
||||||
|
effect: S3Effect;
|
||||||
|
actions: S3Action[];
|
||||||
|
resources: string[];
|
||||||
|
condition?: S3Condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3Policy {
|
||||||
|
version: string;
|
||||||
|
id?: string;
|
||||||
|
statements: S3Statement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyPermissions {
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
|
owner: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyPermissions {
|
||||||
|
access_key_id: string;
|
||||||
|
name: string;
|
||||||
|
legacy_mode: boolean;
|
||||||
|
legacy_permissions?: LegacyPermissions;
|
||||||
|
s3_policy?: S3Policy;
|
||||||
|
policy_json?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateKeyPermissionsRequest {
|
||||||
|
bucket_id: string;
|
||||||
|
access_key_id: string;
|
||||||
|
policy_type: "preset" | "custom";
|
||||||
|
policy_name?: string;
|
||||||
|
policy?: S3Policy;
|
||||||
|
legacy_mode?: boolean;
|
||||||
|
legacy?: LegacyPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresetPolicy {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
policy: S3Policy;
|
||||||
|
policy_json: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidatePolicyResponse {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
message?: string;
|
||||||
|
legacy_equivalent?: LegacyPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object Locking types
|
||||||
|
export type ObjectLockRetentionMode = "COMPLIANCE" | "GOVERNANCE";
|
||||||
|
export type ObjectLegalHoldStatus = "ON" | "OFF";
|
||||||
|
|
||||||
|
export interface DefaultRetention {
|
||||||
|
mode: ObjectLockRetentionMode;
|
||||||
|
days?: number;
|
||||||
|
years?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectLockRule {
|
||||||
|
default_retention?: DefaultRetention;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectLockConfiguration {
|
||||||
|
object_lock_enabled: boolean;
|
||||||
|
rule?: ObjectLockRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectRetention {
|
||||||
|
mode: ObjectLockRetentionMode;
|
||||||
|
retain_until_date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectLegalHold {
|
||||||
|
status: ObjectLegalHoldStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectWithLocking {
|
||||||
|
key: string;
|
||||||
|
size: number;
|
||||||
|
last_modified: string;
|
||||||
|
etag: string;
|
||||||
|
retention?: ObjectRetention;
|
||||||
|
legal_hold?: ObjectLegalHold;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListObjectsWithLockingResponse {
|
||||||
|
bucket_id: string;
|
||||||
|
prefix?: string;
|
||||||
|
delimiter?: string;
|
||||||
|
objects: ObjectWithLocking[];
|
||||||
|
common_prefixes: string[];
|
||||||
|
is_truncated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission presets for easy UI selection
|
||||||
|
export const S3_ACTION_GROUPS = {
|
||||||
|
"Object Read": [
|
||||||
|
"s3:GetObject",
|
||||||
|
"s3:GetObjectAcl",
|
||||||
|
"s3:GetObjectVersion"
|
||||||
|
] as S3Action[],
|
||||||
|
|
||||||
|
"Object Write": [
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:PutObjectAcl",
|
||||||
|
"s3:DeleteObject",
|
||||||
|
"s3:DeleteObjectVersion"
|
||||||
|
] as S3Action[],
|
||||||
|
|
||||||
|
"Object Locking": [
|
||||||
|
"s3:GetObjectRetention",
|
||||||
|
"s3:PutObjectRetention",
|
||||||
|
"s3:GetObjectLegalHold",
|
||||||
|
"s3:PutObjectLegalHold",
|
||||||
|
"s3:BypassGovernanceRetention"
|
||||||
|
] as S3Action[],
|
||||||
|
|
||||||
|
"Bucket Read": [
|
||||||
|
"s3:ListBucket",
|
||||||
|
"s3:GetBucketLocation",
|
||||||
|
"s3:GetBucketAcl",
|
||||||
|
"s3:GetBucketPolicy",
|
||||||
|
"s3:GetBucketVersioning",
|
||||||
|
"s3:GetBucketObjectLockConfiguration"
|
||||||
|
] as S3Action[],
|
||||||
|
|
||||||
|
"Bucket Write": [
|
||||||
|
"s3:PutBucketAcl",
|
||||||
|
"s3:PutBucketPolicy",
|
||||||
|
"s3:DeleteBucketPolicy",
|
||||||
|
"s3:PutBucketVersioning",
|
||||||
|
"s3:PutBucketObjectLockConfiguration"
|
||||||
|
] as S3Action[],
|
||||||
|
|
||||||
|
"Multipart Uploads": [
|
||||||
|
"s3:AbortMultipartUpload",
|
||||||
|
"s3:ListMultipartUploadParts",
|
||||||
|
"s3:ListBucketMultipartUploads"
|
||||||
|
] as S3Action[]
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PRESET_POLICY_DESCRIPTIONS = {
|
||||||
|
"ReadOnly": "Allows read-only access to objects and bucket listing",
|
||||||
|
"ReadWrite": "Allows read and write access to objects, including uploads and deletions",
|
||||||
|
"FullAccess": "Grants full administrative access to all S3 operations",
|
||||||
|
"ObjectLockManager": "Allows managing object retention and legal holds for compliance"
|
||||||
|
} as const;
|
Loading…
x
Reference in New Issue
Block a user