diff --git a/go.mod b/go.mod index 078353b..cb33b55 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( k8s.io/apimachinery v0.30.0-alpha.3 k8s.io/client-go v0.30.0-alpha.3 k8s.io/klog/v2 v2.120.1 + k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 sigs.k8s.io/container-object-storage-interface-api v0.1.0 sigs.k8s.io/container-object-storage-interface-provisioner-sidecar v0.1.0 sigs.k8s.io/container-object-storage-interface-spec v0.1.0 @@ -50,7 +51,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.30.0-alpha.3 // indirect k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f // indirect - k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect sigs.k8s.io/controller-runtime v0.17.5 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect diff --git a/pkg/driver/mockclient.go b/pkg/driver/mockclient.go index b9744aa..0b74fcd 100644 --- a/pkg/driver/mockclient.go +++ b/pkg/driver/mockclient.go @@ -64,7 +64,7 @@ func (m mockS3Client) PutBucketPolicy(input *s3.PutBucketPolicyInput) (*s3.PutBu func (m mockS3Client) GetBucketPolicy(input *s3.GetBucketPolicyInput) (*s3.GetBucketPolicyOutput, error) { switch *input.Bucket { case "test-bucket": - policy := `{"Version":"2012-10-17","Statement":[{"Sid":"AddPerm","Effect":"Allow","Principal":"*","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::test-bucket/*"]}]}` + policy := `{"Version":"2012-10-17","Statement":[{"Sid":"AddPerm","Effect":"Allow","Principal":{"AWS": "*"},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::test-bucket/*"]}]}` return &s3.GetBucketPolicyOutput{Policy: &policy}, nil case "test-bucket-fail-internal": return nil, awserr.New("InternalError", "InternalError", nil) diff --git a/pkg/driver/provisioner.go b/pkg/driver/provisioner.go index f1d5927..ff061ce 100644 --- a/pkg/driver/provisioner.go +++ b/pkg/driver/provisioner.go @@ -19,6 +19,8 @@ import ( "context" "errors" "os" + "slices" + "strings" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/s3" @@ -30,10 +32,20 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/klog/v2" + "k8s.io/utils/ptr" bucketclientset "sigs.k8s.io/container-object-storage-interface-api/client/clientset/versioned" cosispec "sigs.k8s.io/container-object-storage-interface-spec" ) +type Parameters struct { + Parent string + Endpoint string + Region string + AccessKey string + SecretKey string + TLSCert []byte +} + // contains two clients // 1.) for RGWAdminOps : mainly for user related operations // 2.) for S3 operations : mainly for bucket related operations @@ -46,7 +58,10 @@ type provisionerServer struct { var _ cosispec.ProvisionerServer = &provisionerServer{} -var initializeClients = InitializeClients +var ( + fetchParameters = FetchParameters + initializeClients = InitializeClients +) func NewProvisionerServer(provisioner string) (cosispec.ProvisionerServer, error) { kubeConfig, err := rest.InClusterConfig() @@ -72,6 +87,66 @@ func NewProvisionerServer(provisioner string) (cosispec.ProvisionerServer, error }, nil } +func FetchParameters(ctx context.Context, clientset *kubernetes.Clientset, req map[string]string) (*Parameters, error) { + name := req["objectStoreUserSecretName"] + namespace := os.Getenv("POD_NAMESPACE") + if req["objectStoreUserSecretNamespace"] != "" { + namespace = req["objectStoreUserSecretNamespace"] + } + if name == "" || namespace == "" { + return nil, status.Error(codes.InvalidArgument, "objectStoreUserSecretName and objectStoreUserSecretNamespace is required") + } + + secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + klog.ErrorS(err, "failed to get object store user secret") + return nil, status.Error(codes.Internal, "failed to get object store user secret") + } + + data := secret.Data + parameters := &Parameters{ + Endpoint: string(data["Endpoint"]), + Region: string(data["Region"]), + AccessKey: string(data["AccessKey"]), + SecretKey: string(data["SecretKey"]), + Parent: string(data["Parent"]), + } + if parameters.Endpoint == "" || parameters.AccessKey == "" || parameters.SecretKey == "" { + return nil, status.Error(codes.InvalidArgument, "endpoint, accessKeyID and secretKey are required") + } + + sslCertName := string(data["SSLCertSecretName"]) + if sslCertName != "" { + secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, sslCertName, metav1.GetOptions{}) + if err != nil { + klog.ErrorS(err, "failed to get object store ssl cert") + return nil, status.Error(codes.Internal, "failed to get object store ssl cert") + } + + parameters.TLSCert = secret.Data["tls.crt"] + } + + return parameters, nil +} + +func InitializeClients(ctx context.Context, clientset *kubernetes.Clientset, parameters *Parameters) (*s3client.S3Agent, *rgwadmin.API, error) { + klog.V(5).InfoS("initializing clients", "endpoint", parameters.Endpoint, "access_key", parameters.AccessKey) + + // TODO : validate endpoint and support TLS certs + rgwAdminClient, err := rgwadmin.New(parameters.Endpoint, parameters.AccessKey, parameters.SecretKey, nil) + if err != nil { + klog.ErrorS(err, "failed to create rgw admin client") + return nil, nil, status.Error(codes.Internal, "failed to create rgw admin client") + } + + s3Client, err := s3client.NewS3Agent(parameters.AccessKey, parameters.SecretKey, parameters.Endpoint, nil, true) + if err != nil { + klog.ErrorS(err, "failed to create s3 client") + return nil, nil, status.Error(codes.Internal, "failed to create s3 client") + } + return s3Client, rgwAdminClient, nil +} + // ProvisionerCreateBucket is an idempotent method for creating buckets // It is expected to create the same bucket given a bucketName and protocol // If the bucket already exists, then it MUST return codes.AlreadyExists @@ -85,9 +160,16 @@ func (s *provisionerServer) DriverCreateBucket(ctx context.Context, klog.V(5).Infof("req %v", req) bucketName := req.GetName() + if bucketName == "" { + return nil, status.Error(codes.InvalidArgument, "bucket name is required") + } + klog.V(3).InfoS("Creating Bucket", "name", bucketName) - parameters := req.GetParameters() + parameters, err := fetchParameters(ctx, s.Clientset, req.GetParameters()) + if err != nil { + return nil, err + } s3Client, _, err := initializeClients(ctx, s.Clientset, parameters) if err != nil { @@ -129,7 +211,11 @@ func (s *provisionerServer) DriverDeleteBucket(ctx context.Context, return nil, status.Error(codes.Internal, "failed to get bucket") } - parameters := bucket.Spec.Parameters + parameters, err := fetchParameters(ctx, s.Clientset, bucket.Spec.Parameters) + if err != nil { + return nil, err + } + s3Client, _, err := initializeClients(ctx, s.Clientset, parameters) if err != nil { klog.ErrorS(err, "failed to initialize clients") @@ -145,42 +231,50 @@ func (s *provisionerServer) DriverDeleteBucket(ctx context.Context, return &cosispec.DriverDeleteBucketResponse{}, nil } -func (s *provisionerServer) DriverGrantBucketAccess(ctx context.Context, - req *cosispec.DriverGrantBucketAccessRequest) (*cosispec.DriverGrantBucketAccessResponse, error) { +func (s *provisionerServer) DriverGrantBucketAccess(ctx context.Context, req *cosispec.DriverGrantBucketAccessRequest) (*cosispec.DriverGrantBucketAccessResponse, error) { // TODO : validate below details, Authenticationtype, Parameters - userName := req.GetName() bucketName := req.GetBucketId() klog.V(5).Infof("req %v", req) - klog.Info("Granting user accessPolicy to bucket ", "userName", userName, "bucketName", bucketName) - parameters := req.GetParameters() + klog.InfoS("Granting user accessPolicy to bucket", "userName", req.GetName(), "bucketName", bucketName) - s3Client, rgwAdminClient, err := initializeClients(ctx, s.Clientset, parameters) + params, err := fetchParameters(ctx, s.Clientset, req.GetParameters()) if err != nil { - klog.ErrorS(err, "failed to initialize clients") - return nil, status.Error(codes.Internal, "failed to initialize clients") + return nil, err } - user, err := rgwAdminClient.CreateUser(ctx, rgwadmin.User{ - ID: userName, - DisplayName: userName, - }) - - // TODO : Do we need fail for UserErrorExists, or same account can have multiple BAR - if err != nil && !errors.Is(err, rgwadmin.ErrUserExists) { - klog.ErrorS(err, "failed to create user") - return nil, status.Error(codes.Internal, "User creation failed") + s3Client, rgwAdminClient, err := initializeClients(ctx, s.Clientset, params) + if err != nil { + return nil, status.Error(codes.Internal, "failed to initialize clients") } policy, err := s3Client.GetBucketPolicy(bucketName) if err != nil { - if aerr, ok := err.(awserr.Error); ok && aerr.Code() != "NoSuchBucketPolicy" { + switch awsErrorCode(err) { + case "NoSuchBucketPolicy": + // noop + case "NoSuchBucket": + return nil, status.Error(codes.NotFound, "bucket not found") + default: + klog.ErrorS(err, "get bucket policy", "userName", req.Name, "bucketName", bucketName) return nil, status.Error(codes.Internal, "fetching policy failed") } } + var key rgwadmin.UserKeySpec + switch { + case params.Parent == "": + key, err = createUser(ctx, rgwAdminClient, req.Name) + default: + key, err = createSubUser(ctx, rgwAdminClient, params.Parent, req.Name) + } + if err != nil { + return nil, err + } + + response := marshalBucketAccessResponse(key, params) statement := s3client.NewPolicyStatement(). - WithSID(userName). - ForPrincipals(userName). + WithSID(response.AccountId). + ForPrincipals(params.Parent). ForResources(bucketName). ForSubResources(bucketName). Allows(). @@ -199,10 +293,7 @@ func (s *provisionerServer) DriverGrantBucketAccess(ctx context.Context, // TODO : limit the bucket count for this user to 0 // Below response if not final, may change in future - return &cosispec.DriverGrantBucketAccessResponse{ - AccountId: userName, - Credentials: fetchUserCredentials(user, rgwAdminClient.Endpoint, ""), - }, nil + return response, nil } func (s *provisionerServer) DriverRevokeBucketAccess(ctx context.Context, @@ -215,93 +306,122 @@ func (s *provisionerServer) DriverRevokeBucketAccess(ctx context.Context, return nil, status.Error(codes.Internal, "failed to get bucket") } - parameters := bucket.Spec.Parameters + parameters, err := fetchParameters(ctx, s.Clientset, bucket.Spec.Parameters) + if err != nil { + return nil, err + } + _, rgwAdminClient, err := initializeClients(ctx, s.Clientset, parameters) if err != nil { - klog.ErrorS(err, "failed to initialize clients") return nil, status.Error(codes.Internal, "failed to initialize clients") } userName := req.GetAccountId() // TODO : instead of deleting user, revoke its permission and delete only if no more bucket attached to it - err = rgwAdminClient.RemoveUser(ctx, rgwadmin.User{ID: userName}) - if err != nil { - klog.ErrorS(err, "failed to delete user") - return nil, status.Error(codes.Internal, "failed to delete user") + switch { + case parameters.Parent == "": + err = rgwAdminClient.RemoveUser(ctx, rgwadmin.User{ID: userName}) + if err != nil && !errors.Is(err, rgwadmin.ErrNoSuchUser) { + klog.ErrorS(err, "failed to remove user") + return nil, status.Error(codes.Internal, "failed to remove user") + } + default: + parent := rgwadmin.User{ID: parameters.Parent} + err = rgwAdminClient.RemoveSubuser(ctx, parent, rgwadmin.SubuserSpec{ + Name: userName, + }) + if err != nil && strings.HasPrefix("NoSuchSubUser", err.Error()) { + klog.ErrorS(err, "failed to remove subuser") + return nil, status.Error(codes.Internal, "failed to remove subuser") + } } - return &cosispec.DriverRevokeBucketAccessResponse{}, nil -} -func fetchUserCredentials(user rgwadmin.User, endpoint string, region string) map[string]*cosispec.CredentialDetails { - s3Keys := make(map[string]string) - s3Keys["accessKeyID"] = user.Keys[0].AccessKey - s3Keys["accessSecretKey"] = user.Keys[0].SecretKey - s3Keys["endpoint"] = endpoint - s3Keys["region"] = region - creds := &cosispec.CredentialDetails{ - Secrets: s3Keys, - } - credDetails := make(map[string]*cosispec.CredentialDetails) - credDetails["s3"] = creds - return credDetails + return &cosispec.DriverRevokeBucketAccessResponse{}, nil } -func InitializeClients(ctx context.Context, clientset *kubernetes.Clientset, parameters map[string]string) (*s3client.S3Agent, *rgwadmin.API, error) { - klog.V(5).Infof("Initializing clients %v", parameters) +func createUser(ctx context.Context, client *rgwadmin.API, userName string) (rgwadmin.UserKeySpec, error) { + user, err := client.CreateUser(ctx, rgwadmin.User{ + ID: userName, + DisplayName: userName, + }) + if errors.Is(err, rgwadmin.ErrUserExists) { + user, err = getUser(ctx, client, userName) + } - objectStoreUserSecretName, namespace, err := fetchSecretNameAndNamespace(parameters) if err != nil { - return nil, nil, err + klog.ErrorS(err, "failed to create user", "userName", userName) + return rgwadmin.UserKeySpec{}, status.Error(codes.Internal, "User creation failed") } - objectStoreUserSecret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, objectStoreUserSecretName, metav1.GetOptions{}) - if err != nil { - klog.ErrorS(err, "failed to get object store user secret") - return nil, nil, status.Error(codes.Internal, "failed to get object store user secret") + return user.Keys[0], nil +} + +func createSubUser(ctx context.Context, client *rgwadmin.API, parent, userName string) (rgwadmin.UserKeySpec, error) { + parentUser := rgwadmin.User{ID: parent} + err := client.CreateSubuser(ctx, parentUser, rgwadmin.SubuserSpec{ + Name: userName, + Access: rgwadmin.SubuserAccessFull, + KeyType: ptr.To("s3"), + }) + if err != nil && !errors.Is(err, rgwadmin.ErrSubuserExists) { + klog.ErrorS(err, "failed to create subuser", "parent", parent) + return rgwadmin.UserKeySpec{}, status.Error(codes.Internal, "Subuser creation failed") } - accessKey, secretKey, rgwEndpoint, _, err := fetchParameters(objectStoreUserSecret.Data) + user, err := getUser(ctx, client, parent) if err != nil { - return nil, nil, err + return rgwadmin.UserKeySpec{}, err } - // TODO : validate endpoint and support TLS certs - - rgwAdminClient, err := rgwadmin.New(rgwEndpoint, accessKey, secretKey, nil) - if err != nil { - klog.ErrorS(err, "failed to create rgw admin client") - return nil, nil, status.Error(codes.Internal, "failed to create rgw admin client") + userName = parent + ":" + userName + i := slices.IndexFunc(user.Keys, func(key rgwadmin.UserKeySpec) bool { + return key.User == userName + }) + if i == -1 { + klog.ErrorS(errors.New("lookup key"), "key not found", "userName", userName) + return rgwadmin.UserKeySpec{}, status.Error(codes.NotFound, "Key not found in user object") } - s3Client, err := s3client.NewS3Agent(accessKey, secretKey, rgwEndpoint, nil, true) - if err != nil { - klog.ErrorS(err, "failed to create s3 client") - return nil, nil, status.Error(codes.Internal, "failed to create s3 client") + + return user.Keys[i], nil +} + +func getUser(ctx context.Context, client *rgwadmin.API, userName string) (rgwadmin.User, error) { + user, err := client.GetUser(ctx, rgwadmin.User{ID: userName}) + switch { + case errors.Is(err, rgwadmin.ErrNoSuchUser): + return rgwadmin.User{}, status.Error(codes.NotFound, "User not found") + case err != nil: + klog.ErrorS(err, "failed to get user", "userName", userName) + return rgwadmin.User{}, status.Error(codes.Internal, "Get user failed") + default: + return user, nil } - return s3Client, rgwAdminClient, nil } -func fetchParameters(secretData map[string][]byte) (string, string, string, string, error) { - accessKey := string(secretData["AccessKey"]) - secretKey := string(secretData["SecretKey"]) - endPoint := string(secretData["Endpoint"]) - if endPoint == "" || accessKey == "" || secretKey == "" { - return "", "", "", "", status.Error(codes.InvalidArgument, "endpoint, accessKeyID and secretKey are required") +func marshalBucketAccessResponse(key rgwadmin.UserKeySpec, params *Parameters) *cosispec.DriverGrantBucketAccessResponse { + s3Keys := map[string]string{ + "endpoint": params.Endpoint, + "region": params.Region, + "accessKeyID": key.AccessKey, + "accessSecretKey": key.AccessKey, } - tlsCert := string(secretData["SSLCertSecretName"]) - return accessKey, secretKey, endPoint, tlsCert, nil + return &cosispec.DriverGrantBucketAccessResponse{ + AccountId: key.User, + Credentials: map[string]*cosispec.CredentialDetails{ + "s3": { + Secrets: s3Keys, + }, + }, + } } -func fetchSecretNameAndNamespace(parameters map[string]string) (string, string, error) { - secretName := parameters["objectStoreUserSecretName"] - namespace := os.Getenv("POD_NAMESPACE") - if parameters["objectStoreUserSecretNamespace"] != "" { - namespace = parameters["objectStoreUserSecretNamespace"] - } - if secretName == "" || namespace == "" { - return "", "", status.Error(codes.InvalidArgument, "objectStoreUserSecretName and Namespace is required") +func awsErrorCode(err error) string { + var aerr awserr.Error + if !errors.As(err, &aerr) { + return "" } - return secretName, namespace, nil + return aerr.Code() } diff --git a/pkg/driver/provisioner_test.go b/pkg/driver/provisioner_test.go index 97dfbbc..c52dd8d 100644 --- a/pkg/driver/provisioner_test.go +++ b/pkg/driver/provisioner_test.go @@ -96,11 +96,10 @@ func Test_provisionerServer_DriverCreateBucket(t *testing.T) { req *cosispec.DriverCreateBucketRequest } - initializeClients = func(ctx context.Context, clientset *kubernetes.Clientset, parameters map[string]string) (*s3cli.S3Agent, *rgwadmin.API, error) { - _, _, err := fetchSecretNameAndNamespace(parameters) - if err != nil { - t.Fatalf("failed to fetch secret name and namespace: %v", err) - } + fetchParameters = func(_ context.Context, _ *kubernetes.Clientset, _ map[string]string) (*Parameters, error) { + return &Parameters{}, nil + } + initializeClients = func(ctx context.Context, clientset *kubernetes.Clientset, parameters *Parameters) (*s3cli.S3Agent, *rgwadmin.API, error) { s3Client := &s3cli.S3Agent{ Client: mockS3Client{}, } @@ -145,12 +144,10 @@ func Test_provisionerServer_DriverGrantBucketAccess(t *testing.T) { ctx context.Context req *cosispec.DriverGrantBucketAccessRequest } - initializeClients = func(ctx context.Context, clientset *kubernetes.Clientset, parameters map[string]string) (*s3cli.S3Agent, *rgwadmin.API, error) { - _, _, err := fetchSecretNameAndNamespace(parameters) - if err != nil { - t.Fatalf("failed to fetch secret name and namespace: %v", err) - } - + fetchParameters = func(_ context.Context, _ *kubernetes.Clientset, _ map[string]string) (*Parameters, error) { + return &Parameters{}, nil + } + initializeClients = func(_ context.Context, _ *kubernetes.Clientset, _ *Parameters) (*s3cli.S3Agent, *rgwadmin.API, error) { s3Client := &s3cli.S3Agent{ Client: mockS3Client{}, } @@ -178,6 +175,16 @@ func Test_provisionerServer_DriverGrantBucketAccess(t *testing.T) { if err != nil { t.Fatalf("failed to unmarshal user create json: %v", err) } + response := &cosispec.DriverGrantBucketAccessResponse{AccountId: "test-user", Credentials: map[string]*cosispec.CredentialDetails{ + "s3": { + Secrets: map[string]string{ + "accessKeyID": "AccessKey", + "accessSecretKey": "AccessKey", + "endpoint": "", + "region": "", + }, + }, + }} tests := []struct { name string fields fields @@ -187,7 +194,7 @@ func Test_provisionerServer_DriverGrantBucketAccess(t *testing.T) { }{ {"Empty Bucket Name", fields{"GrantBucketAccess Empty Bucket Name"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "", Name: "test-user", Parameters: createParameters()}}, nil, true}, {"Empty User Name", fields{"GrantBucketAccess Empty User Name"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "test-bucket", Name: "", Parameters: createParameters()}}, nil, true}, - {"Grant Bucket Access success", fields{"GrantBucketAccess Success"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "test-bucket", Name: "test-user", Parameters: createParameters()}}, &cosispec.DriverGrantBucketAccessResponse{AccountId: "test-user", Credentials: fetchUserCredentials(u, "rgw-my-store:8000", "")}, false}, + {"Grant Bucket Access success", fields{"GrantBucketAccess Success"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "test-bucket", Name: "test-user", Parameters: createParameters()}}, response, false}, {"Grant Bucket Access failure", fields{"GrantBucketAccess Failure"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "failed-bucket", Name: "test-user", Parameters: createParameters()}}, nil, true}, {"Bucket does not exist", fields{"GrantBucketAccess Does not exist"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "test-bucket-does-not-exist", Name: "test-user", Parameters: createParameters()}}, nil, true}, {"User does not exist", fields{"GrantBucketAccess User Does not exist"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "test-bucket", Name: "test-user-does-not-exist", Parameters: createParameters()}}, nil, true}, @@ -219,11 +226,7 @@ func Test_provisionerServer_DriverDeleteBucket(t *testing.T) { req *cosispec.DriverDeleteBucketRequest } - initializeClients = func(ctx context.Context, clientset *kubernetes.Clientset, parameters map[string]string) (*s3cli.S3Agent, *rgwadmin.API, error) { - _, _, err := fetchSecretNameAndNamespace(parameters) - if err != nil { - t.Fatalf("failed to fetch secret name and namespace: %v", err) - } + initializeClients = func(_ context.Context, _ *kubernetes.Clientset, _ *Parameters) (*s3cli.S3Agent, *rgwadmin.API, error) { s3Client := &s3cli.S3Agent{ Client: mockS3Client{}, } @@ -281,11 +284,7 @@ func Test_provisonerServer_DriverRevokeBucketAccess(t *testing.T) { req *cosispec.DriverRevokeBucketAccessRequest } - initializeClients = func(ctx context.Context, clientset *kubernetes.Clientset, parameters map[string]string) (*s3cli.S3Agent, *rgwadmin.API, error) { - _, _, err := fetchSecretNameAndNamespace(parameters) - if err != nil { - t.Fatalf("failed to fetch secret name and namespace: %v", err) - } + initializeClients = func(_ context.Context, _ *kubernetes.Clientset, _ *Parameters) (*s3cli.S3Agent, *rgwadmin.API, error) { s3Client := &s3cli.S3Agent{ Client: mockS3Client{}, } diff --git a/pkg/util/s3client/policy.go b/pkg/util/s3client/policy.go index 1978bef..03447c9 100644 --- a/pkg/util/s3client/policy.go +++ b/pkg/util/s3client/policy.go @@ -128,7 +128,7 @@ type PolicyStatement struct { Effect effect `json:"Effect"` // Principle is/are the Ceph user names affected by this PolicyStatement // Must be in the format of 'arn:aws:iam:::user/' - Principal map[string][]string `json:"Principal"` + Principal map[string]StringOrSlice `json:"Principal"` // Action is a list of s3:* actions Action []action `json:"Action"` // Resource is the ARN identifier for the S3 resource (bucket) @@ -136,6 +136,45 @@ type PolicyStatement struct { Resource []string `json:"Resource"` } +type StringOrSlice []string + +func (s StringOrSlice) MarshalJSON() ([]byte, error) { + switch { + case len(s) == 0: + return nil, nil + case len(s) == 1: + v := s[0] + return json.Marshal(v) + default: + v := []string(s) + return json.Marshal(v) + } +} + +func (s *StringOrSlice) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + + if data[0] == '"' && data[len(data)-1] == '"' { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + *s = []string{v} + return nil + } + + var v []string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + *s = v + return nil +} + // BucketPolicy represents set of policy statements for a single bucket. type BucketPolicy struct { // Id (optional) identifies the bucket policy @@ -240,7 +279,7 @@ func NewPolicyStatement() *PolicyStatement { return &PolicyStatement{ Sid: "", Effect: "", - Principal: map[string][]string{}, + Principal: map[string]StringOrSlice{}, Action: []action{}, Resource: []string{}, }