feat: must have a 'owner' access key before configuring CORS

This commit is contained in:
jpedro-cf 2025-11-07 20:51:12 -03:00
parent 9344247d19
commit 48211dd50e
7 changed files with 154 additions and 117 deletions

View File

@ -33,7 +33,7 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) {
limit = 100 limit = 100
} }
client, err := getS3Client(bucket) client, err := getS3Client(bucket, nil)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
@ -88,7 +88,7 @@ func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) {
thumbnail := queryParams.Get("thumb") == "1" thumbnail := queryParams.Get("thumb") == "1"
download := queryParams.Get("dl") == "1" download := queryParams.Get("dl") == "1"
client, err := getS3Client(bucket) client, err := getS3Client(bucket, nil)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
@ -184,7 +184,7 @@ func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) {
defer file.Close() defer file.Close()
} }
client, err := getS3Client(bucket) client, err := getS3Client(bucket, nil)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
@ -220,7 +220,7 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
recursive := r.URL.Query().Get("recursive") == "true" recursive := r.URL.Query().Get("recursive") == "true"
isDirectory := strings.HasSuffix(key, "/") isDirectory := strings.HasSuffix(key, "/")
client, err := getS3Client(bucket) client, err := getS3Client(bucket, nil)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
@ -284,10 +284,15 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
utils.ResponseSuccess(w, res) 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) 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 { if cacheData != nil {
return cacheData.(aws.CredentialsProvider), nil return cacheData.(aws.CredentialsProvider), nil
} }
@ -305,7 +310,10 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
var key schema.KeyElement var key schema.KeyElement
for _, k := range bucketData.Keys { 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 continue
} }
@ -319,14 +327,18 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
break 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, "") credential := credentials.NewStaticCredentialsProvider(key.AccessKeyID, key.SecretAccessKey, "")
utils.Cache.Set(cacheKey, credential, time.Hour) utils.Cache.Set(cacheKey, credential, time.Hour)
return credential, nil return credential, nil
} }
func getS3Client(bucket string) (*s3.Client, error) { func getS3Client(bucket string, permission *string) (*s3.Client, error) {
creds, err := getBucketCredentials(bucket) creds, err := getBucketCredentials(bucket, permission)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucket, err) return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucket, err)
} }

View File

@ -61,20 +61,33 @@ func (b *Buckets) GetAll(w http.ResponseWriter, r *http.Request) {
func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){ func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){
bucket := r.PathValue("bucket") bucket := r.PathValue("bucket")
client, err := getS3Client(bucket) perm := "owner"
client, err := getS3Client(bucket, &perm)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
} }
out, err := client.GetBucketCors(context.Background(), &s3.GetBucketCorsInput{ cors, err := client.GetBucketCors(context.Background(), &s3.GetBucketCorsInput{
Bucket: aws.String(bucket), Bucket: aws.String(bucket),
}) })
res := []schema.BucketCors{} if err != nil {
utils.ResponseError(w, err)
return
}
for _, rule := range out.CORSRules { var defaultCors = []schema.BucketCors{{
AllowedOrigins: []string{},
AllowedMethods: []string{},
AllowedHeaders: []string{},
ExposeHeaders: []string{},
MaxAgeSeconds: nil,
}}
res := make([]schema.BucketCors, 0, len(cors.CORSRules))
for _, rule := range cors.CORSRules {
res = append(res, schema.BucketCors{ res = append(res, schema.BucketCors{
AllowedOrigins: rule.AllowedOrigins, AllowedOrigins: rule.AllowedOrigins,
AllowedMethods: rule.AllowedMethods, AllowedMethods: rule.AllowedMethods,
@ -84,9 +97,8 @@ func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){
}) })
} }
if err != nil { if len(res) == 0 {
utils.ResponseError(w, err) res = defaultCors
return
} }
utils.ResponseSuccess(w, res) utils.ResponseSuccess(w, res)
@ -95,7 +107,6 @@ func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){
func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){ func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){
bucket := r.PathValue("bucket") bucket := r.PathValue("bucket")
var body struct { var body struct {
Rules []schema.BucketCors `json:"rules"` Rules []schema.BucketCors `json:"rules"`
} }
@ -105,7 +116,8 @@ func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){
return return
} }
client, err := getS3Client(bucket) perm := "owner"
client, err := getS3Client(bucket, &perm)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
@ -127,7 +139,7 @@ func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){
ExposeHeaders: utils.NilIfEmpty(rule.ExposeHeaders), ExposeHeaders: utils.NilIfEmpty(rule.ExposeHeaders),
MaxAgeSeconds: rule.MaxAgeSeconds, MaxAgeSeconds: rule.MaxAgeSeconds,
}) })
} }
res, err := client.PutBucketCors(context.Background(), &s3.PutBucketCorsInput{ res, err := client.PutBucketCors(context.Background(), &s3.PutBucketCorsInput{
Bucket: aws.String(bucket), Bucket: aws.String(bucket),

View File

@ -60,3 +60,16 @@ type WebsiteConfig struct {
IndexDocument string `json:"indexDocument"` IndexDocument string `json:"indexDocument"`
ErrorDocument string `json:"errorDocument"` 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
}
}

View File

@ -117,6 +117,7 @@ export const useBucketCors = (bucketName?: string) => {
queryKey: ["bucket_cors", bucketName], queryKey: ["bucket_cors", bucketName],
queryFn: () => api.get<BucketCors[]>(`/buckets/${bucketName}/cors`), queryFn: () => api.get<BucketCors[]>(`/buckets/${bucketName}/cors`),
enabled: !!bucketName, enabled: !!bucketName,
retry: false,
}); });
}; };

View File

@ -39,7 +39,9 @@ const tabs: Tab[] = [
const ManageBucketPage = () => { const ManageBucketPage = () => {
const { id } = useParams(); const { id } = useParams();
const { data, error, isLoading, refetch } = useBucket(id); 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]; const name = data?.globalAliases[0];
@ -63,7 +65,7 @@ const ManageBucketPage = () => {
</Alert> </Alert>
)} )}
{data && cors && ( {data && !corsLoading && (
<div className="container"> <div className="container">
<BucketContext.Provider <BucketContext.Provider
value={{ value={{

View File

@ -19,7 +19,7 @@ type Props = {
}; };
const AllowKeyDialog = ({ currentKeys }: Props) => { const AllowKeyDialog = ({ currentKeys }: Props) => {
const { bucket } = useBucketContext(); const { bucket, bucketName } = useBucketContext();
const { dialogRef, isOpen, onOpen, onClose } = useDisclosure(); const { dialogRef, isOpen, onOpen, onClose } = useDisclosure();
const { data: keys } = useKeys(); const { data: keys } = useKeys();
const form = useForm<AllowKeysSchema>({ const form = useForm<AllowKeysSchema>({
@ -37,6 +37,7 @@ const AllowKeyDialog = ({ currentKeys }: Props) => {
onClose(); onClose();
toast.success("Key allowed!"); toast.success("Key allowed!");
queryClient.invalidateQueries({ queryKey: ["bucket", bucket.id] }); queryClient.invalidateQueries({ queryKey: ["bucket", bucket.id] });
queryClient.invalidateQueries({ queryKey: ["bucket_cors", bucketName] });
}, },
onError: handleError, onError: handleError,
}); });

View File

@ -1,12 +1,12 @@
import { Card, Modal } from "react-daisyui"; import { Alert, Card, Modal } from "react-daisyui";
import Button from "@/components/ui/button"; 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 { useEffect, useRef } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { useBucketContext } from "../context"; import { useBucketContext } from "../context";
import { bucketCorsSchema, BucketCorsSchema } from "../schema"; import { bucketCorsSchema, BucketCorsSchema } from "../schema";
import { zodResolver } from "@hookform/resolvers/zod"; 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 Input, { InputField } from "@/components/ui/input";
import FormControl from "@/components/ui/form-control"; import FormControl from "@/components/ui/form-control";
import { useDisclosure } from "@/hooks/useDisclosure"; import { useDisclosure } from "@/hooks/useDisclosure";
@ -17,7 +17,7 @@ import { useQueryClient } from "@tanstack/react-query";
const CorsConfiguration = () => { const CorsConfiguration = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { bucketName, cors } = useBucketContext(); const { bucketName, cors, bucket } = useBucketContext();
const { mutate, isPending } = useBucketCorsMutation({ const { mutate, isPending } = useBucketCorsMutation({
onSuccess: () => { onSuccess: () => {
@ -31,30 +31,18 @@ const CorsConfiguration = () => {
resolver: zodResolver(bucketCorsSchema), resolver: zodResolver(bucketCorsSchema),
defaultValues: { defaultValues: {
bucketName: bucketName, bucketName: bucketName,
rules: rules: cors,
cors.length > 0
? cors
: [
{
allowedHeaders: [],
allowedMethods: [],
allowedOrigins: [],
exposeHeaders: [],
maxAgeSeconds: null,
},
],
}, },
}); });
const { fields } = useFieldArray({ const fields = form.watch("rules");
control: form.control,
name: "rules",
});
function handleSubmit(data: BucketCorsSchema) { function handleSubmit(data: BucketCorsSchema) {
mutate(data); mutate(data);
} }
const allowConfiguration = bucket.keys.some((key) => key.permissions.owner);
return ( return (
<Card className="card-body"> <Card className="card-body">
<form onSubmit={form.handleSubmit(handleSubmit)}> <form onSubmit={form.handleSubmit(handleSubmit)}>
@ -66,15 +54,24 @@ const CorsConfiguration = () => {
icon={CheckCircle} icon={CheckCircle}
color="primary" color="primary"
type="submit" type="submit"
disabled={isPending} disabled={isPending || !allowConfiguration}
> >
Save Save
</Button> </Button>
</div> </div>
{fields.map((rule, idx) => ( {!allowConfiguration && (
<Alert status="warning" icon={<CircleXIcon />} className="mt-5">
<span>
You must configure an access key with owner permissions for this
bucket before setting up CORS.
</span>
</Alert>
)}
{allowConfiguration &&
fields.map((_, idx) => (
<div <div
className="grid md:grid-cols-2 xl:grid-cols-5 gap-5 mt-5" className="grid md:grid-cols-2 xl:grid-cols-5 gap-5 mt-5"
key={rule.id} key={idx}
> >
<FormControl <FormControl
form={form} form={form}
@ -113,7 +110,6 @@ const CorsConfiguration = () => {
)} )}
/> />
<FormControl <FormControl
key={rule.id}
form={form} form={form}
name={`rules.${idx}.exposeHeaders`} name={`rules.${idx}.exposeHeaders`}
title={"Expose Headers"} title={"Expose Headers"}