diff --git a/backend/router/browse.go b/backend/router/browse.go index 5df5acb..6b4ceb1 100644 --- a/backend/router/browse.go +++ b/backend/router/browse.go @@ -33,7 +33,7 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { limit = 100 } - client, err := getS3Client(bucket) + client, err := getS3Client(bucket, nil) if err != nil { utils.ResponseError(w, err) return @@ -88,7 +88,7 @@ func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { thumbnail := queryParams.Get("thumb") == "1" download := queryParams.Get("dl") == "1" - client, err := getS3Client(bucket) + client, err := getS3Client(bucket, nil) if err != nil { utils.ResponseError(w, err) return @@ -184,7 +184,7 @@ func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) { defer file.Close() } - client, err := getS3Client(bucket) + client, err := getS3Client(bucket, nil) if err != nil { utils.ResponseError(w, err) return @@ -220,7 +220,7 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) { recursive := r.URL.Query().Get("recursive") == "true" isDirectory := strings.HasSuffix(key, "/") - client, err := getS3Client(bucket) + client, err := getS3Client(bucket, nil) if err != nil { utils.ResponseError(w, err) return @@ -284,10 +284,15 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) { utils.ResponseSuccess(w, res) } -func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) { +func getBucketCredentials(bucket string, permission *string) (aws.CredentialsProvider, error) { cacheKey := fmt.Sprintf("key:%s", bucket) - cacheData := utils.Cache.Get(cacheKey) + // Some CORS operations with the SDK are only achievable using a "owner" access key + if permission != nil { + cacheKey = fmt.Sprintf("key:%s:%s", *permission, bucket) + } + + cacheData := utils.Cache.Get(cacheKey) if cacheData != nil { return cacheData.(aws.CredentialsProvider), nil } @@ -305,7 +310,10 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) { var key schema.KeyElement for _, k := range bucketData.Keys { - if !k.Permissions.Read || !k.Permissions.Write { + if permission == nil && (!k.Permissions.Read || !k.Permissions.Write){ + continue + } + if permission != nil && !k.Permissions.HasPermission(*permission) { continue } @@ -319,14 +327,18 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) { break } + if key.AccessKeyID == "" { + return nil, fmt.Errorf("a valid access key was not found for this bucket") + } + credential := credentials.NewStaticCredentialsProvider(key.AccessKeyID, key.SecretAccessKey, "") utils.Cache.Set(cacheKey, credential, time.Hour) return credential, nil } -func getS3Client(bucket string) (*s3.Client, error) { - creds, err := getBucketCredentials(bucket) +func getS3Client(bucket string, permission *string) (*s3.Client, error) { + creds, err := getBucketCredentials(bucket, permission) if err != nil { return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucket, err) } diff --git a/backend/router/buckets.go b/backend/router/buckets.go index b1c105a..b4b54e7 100644 --- a/backend/router/buckets.go +++ b/backend/router/buckets.go @@ -61,20 +61,33 @@ func (b *Buckets) GetAll(w http.ResponseWriter, r *http.Request) { func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){ bucket := r.PathValue("bucket") - client, err := getS3Client(bucket) + perm := "owner" + client, err := getS3Client(bucket, &perm) if err != nil { utils.ResponseError(w, err) return } - out, err := client.GetBucketCors(context.Background(), &s3.GetBucketCorsInput{ + cors, err := client.GetBucketCors(context.Background(), &s3.GetBucketCorsInput{ Bucket: aws.String(bucket), }) - res := []schema.BucketCors{} + if err != nil { + utils.ResponseError(w, err) + return + } + + var defaultCors = []schema.BucketCors{{ + AllowedOrigins: []string{}, + AllowedMethods: []string{}, + AllowedHeaders: []string{}, + ExposeHeaders: []string{}, + MaxAgeSeconds: nil, + }} - for _, rule := range out.CORSRules { + res := make([]schema.BucketCors, 0, len(cors.CORSRules)) + for _, rule := range cors.CORSRules { res = append(res, schema.BucketCors{ AllowedOrigins: rule.AllowedOrigins, AllowedMethods: rule.AllowedMethods, @@ -84,9 +97,8 @@ func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){ }) } - if err != nil { - utils.ResponseError(w, err) - return + if len(res) == 0 { + res = defaultCors } utils.ResponseSuccess(w, res) @@ -94,7 +106,6 @@ func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){ func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){ bucket := r.PathValue("bucket") - var body struct { Rules []schema.BucketCors `json:"rules"` @@ -105,7 +116,8 @@ func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){ return } - client, err := getS3Client(bucket) + perm := "owner" + client, err := getS3Client(bucket, &perm) if err != nil { utils.ResponseError(w, err) @@ -115,19 +127,19 @@ func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){ cors := make([]types.CORSRule, 0, len(body.Rules)) for _, rule := range body.Rules { - if len(rule.AllowedMethods) == 0 || len(rule.AllowedOrigins) == 0 { - utils.ResponseError(w, fmt.Errorf("each CORS rule must have at least one allowed method and origin")) - return - } + if len(rule.AllowedMethods) == 0 || len(rule.AllowedOrigins) == 0 { + utils.ResponseError(w, fmt.Errorf("each CORS rule must have at least one allowed method and origin")) + return + } - cors = append(cors, types.CORSRule{ - AllowedHeaders: utils.NilIfEmpty(rule.AllowedHeaders), - AllowedMethods: rule.AllowedMethods, // required - AllowedOrigins: rule.AllowedOrigins, // required - ExposeHeaders: utils.NilIfEmpty(rule.ExposeHeaders), - MaxAgeSeconds: rule.MaxAgeSeconds, - }) -} + cors = append(cors, types.CORSRule{ + AllowedHeaders: utils.NilIfEmpty(rule.AllowedHeaders), + AllowedMethods: rule.AllowedMethods, // required + AllowedOrigins: rule.AllowedOrigins, // required + ExposeHeaders: utils.NilIfEmpty(rule.ExposeHeaders), + MaxAgeSeconds: rule.MaxAgeSeconds, + }) + } res, err := client.PutBucketCors(context.Background(), &s3.PutBucketCorsInput{ Bucket: aws.String(bucket), diff --git a/backend/schema/bucket.go b/backend/schema/bucket.go index e721d66..d26a868 100644 --- a/backend/schema/bucket.go +++ b/backend/schema/bucket.go @@ -60,3 +60,16 @@ type WebsiteConfig struct { IndexDocument string `json:"indexDocument"` ErrorDocument string `json:"errorDocument"` } + +func (p *Permissions) HasPermission(perm string) bool { + switch perm { + case "read": + return p.Read + case "write": + return p.Write + case "owner": + return p.Owner + default: + return false + } +} \ No newline at end of file diff --git a/src/pages/buckets/manage/hooks.ts b/src/pages/buckets/manage/hooks.ts index dd589f4..7449120 100644 --- a/src/pages/buckets/manage/hooks.ts +++ b/src/pages/buckets/manage/hooks.ts @@ -117,6 +117,7 @@ export const useBucketCors = (bucketName?: string) => { queryKey: ["bucket_cors", bucketName], queryFn: () => api.get(`/buckets/${bucketName}/cors`), enabled: !!bucketName, + retry: false, }); }; diff --git a/src/pages/buckets/manage/page.tsx b/src/pages/buckets/manage/page.tsx index 21b8db2..5e489c5 100644 --- a/src/pages/buckets/manage/page.tsx +++ b/src/pages/buckets/manage/page.tsx @@ -39,7 +39,9 @@ const tabs: Tab[] = [ const ManageBucketPage = () => { const { id } = useParams(); const { data, error, isLoading, refetch } = useBucket(id); - const { data: cors } = useBucketCors(data?.globalAliases[0]); + const { data: cors, isLoading: corsLoading } = useBucketCors( + data?.globalAliases[0] + ); const name = data?.globalAliases[0]; @@ -63,7 +65,7 @@ const ManageBucketPage = () => { )} - {data && cors && ( + {data && !corsLoading && (
{ - const { bucket } = useBucketContext(); + const { bucket, bucketName } = useBucketContext(); const { dialogRef, isOpen, onOpen, onClose } = useDisclosure(); const { data: keys } = useKeys(); const form = useForm({ @@ -37,6 +37,7 @@ const AllowKeyDialog = ({ currentKeys }: Props) => { onClose(); toast.success("Key allowed!"); queryClient.invalidateQueries({ queryKey: ["bucket", bucket.id] }); + queryClient.invalidateQueries({ queryKey: ["bucket_cors", bucketName] }); }, onError: handleError, }); diff --git a/src/pages/buckets/manage/permissions/cors-configuration.tsx b/src/pages/buckets/manage/permissions/cors-configuration.tsx index c45ac02..4e22f83 100644 --- a/src/pages/buckets/manage/permissions/cors-configuration.tsx +++ b/src/pages/buckets/manage/permissions/cors-configuration.tsx @@ -1,12 +1,12 @@ -import { Card, Modal } from "react-daisyui"; +import { Alert, Card, Modal } from "react-daisyui"; import Button from "@/components/ui/button"; -import { CheckCircle, Plus } from "lucide-react"; +import { CheckCircle, CircleXIcon, Plus } from "lucide-react"; import { useEffect, useRef } from "react"; import { toast } from "sonner"; import { useBucketContext } from "../context"; import { bucketCorsSchema, BucketCorsSchema } from "../schema"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Path, useFieldArray, useForm, UseFormReturn } from "react-hook-form"; +import { Path, useForm, UseFormReturn } from "react-hook-form"; import Input, { InputField } from "@/components/ui/input"; import FormControl from "@/components/ui/form-control"; import { useDisclosure } from "@/hooks/useDisclosure"; @@ -17,7 +17,7 @@ import { useQueryClient } from "@tanstack/react-query"; const CorsConfiguration = () => { const queryClient = useQueryClient(); - const { bucketName, cors } = useBucketContext(); + const { bucketName, cors, bucket } = useBucketContext(); const { mutate, isPending } = useBucketCorsMutation({ onSuccess: () => { @@ -31,30 +31,18 @@ const CorsConfiguration = () => { resolver: zodResolver(bucketCorsSchema), defaultValues: { bucketName: bucketName, - rules: - cors.length > 0 - ? cors - : [ - { - allowedHeaders: [], - allowedMethods: [], - allowedOrigins: [], - exposeHeaders: [], - maxAgeSeconds: null, - }, - ], + rules: cors, }, }); - const { fields } = useFieldArray({ - control: form.control, - name: "rules", - }); + const fields = form.watch("rules"); function handleSubmit(data: BucketCorsSchema) { mutate(data); } + const allowConfiguration = bucket.keys.some((key) => key.permissions.owner); + return (
@@ -66,74 +54,82 @@ const CorsConfiguration = () => { icon={CheckCircle} color="primary" type="submit" - disabled={isPending} + disabled={isPending || !allowConfiguration} > Save
- {fields.map((rule, idx) => ( -
- ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - -
- ))} + {!allowConfiguration && ( + } className="mt-5"> + + You must configure an access key with owner permissions for this + bucket before setting up CORS. + + + )} + {allowConfiguration && + fields.map((_, idx) => ( +
+ ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + +
+ ))} );