From 3b0783f56270ba3cf8dcb4e99fcfaf5370d13c78 Mon Sep 17 00:00:00 2001 From: LTLA Date: Fri, 31 Jan 2025 11:29:51 -0800 Subject: [PATCH] Implement asset-level permissions with global writes. --- README.md | 12 ++- permissions.go | 124 ++++++++++++++++--------- permissions_test.go | 216 +++++++++++++++++++++++++++----------------- upload.go | 31 +++++-- upload_test.go | 89 ++++++++++++------ 5 files changed, 309 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 1e99910..2f92b1a 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,14 @@ This is a JSON-formatted file that contains a JSON object with the following pro If not specified, the uploader is untrusted by default. - `global_write` (optional): a boolean indicating whether "global writes" are enabled. With global writes enabled, any user of the filesystem can create a new asset within this project. - Once the asset is created, its creating user is added to the `uploaders` array with `asset` set to the name of the new asset and `trusted` set to `true`. + Once the asset is created, its creating user is added as a trusted uploader to the `{project}/{asset}/..permissions` file (see below). If not specified, global writes are disabled by default. +Additional uploader permissions for a specific asset can be specified in a `{project}/{asset}/..permissions` file. +This should be a JSON-formatted file that contains a JSON object with the `uploaders` property as described above. +Specifying an uploader in this file is equivalent to specifying an uploader in the project-level permissions with the `asset` property set to the name of the relevant asset. +During [upload requests](#uploads-and-updates), any `uploaders` in this file will be appended to the `uploaders` in `{project}/..permissions` before authorization checks. + User identities are defined by the UIDs on the operating system. All users are authenticated by examining the ownership of files provided to the Gobbler. Note that, when switching from the Gobbler to **gypsum**, the project permissions need to be updated from UIDs to GitHub user names. @@ -230,9 +235,12 @@ This ensures that the Gobbler instance is able to free up space by periodically Users should create a file with the `request-set_permissions-` prefix, which should be JSON-formatted with the following properties: - `project`: string containing the name of the project. -- `permissions`: an object containing either or both of `owners` and `uploaders`. +- `asset` (optional): string containing the name of an asset. + If provided, asset-level uploader permissions will be modified instead of project-level permissions. +- `permissions`: an object containing zero, one or more of `owners`, `uploaders` and `global_write`. Each of these properties has the same type as described [above](#permissions). If any property is missing, the value in the existing permissions is used. + If `asset` is provided, only `uploaders` will be used. On success, the permissions in the registry are modified. The HTTP response will contain a JSON object with the `status` property set to `SUCCESS`. diff --git a/permissions.go b/permissions.go index 33363ef..69d1731 100644 --- a/permissions.go +++ b/permissions.go @@ -48,13 +48,13 @@ func identifyUser(path string) (string, error) { } func readPermissions(path string) (*permissionsMetadata, error) { - handle, err := os.ReadFile(filepath.Join(path, permissionsFileName)) + contents, err := os.ReadFile(filepath.Join(path, permissionsFileName)) if err != nil { return nil, fmt.Errorf("failed to read %q; %w", path, err) } var output permissionsMetadata - err = json.Unmarshal(handle, &output) + err = json.Unmarshal(contents, &output) if err != nil { return nil, fmt.Errorf("failed to parse JSON from %q; %w", path, err) } @@ -62,6 +62,31 @@ func readPermissions(path string) (*permissionsMetadata, error) { return &output, nil } +func addAssetPermissions(existing *permissionsMetadata, asset_dir, asset string) error { + path := filepath.Join(asset_dir, permissionsFileName) + contents, err := os.ReadFile(path) + + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } else { + return fmt.Errorf("failed to read %q; %w", path, err) + } + } + + var loaded permissionsMetadata + err = json.Unmarshal(contents, &loaded) + if err != nil { + return fmt.Errorf("failed to parse JSON from %q; %w", path, err) + } + + for _, up := range loaded.Uploaders { + up.Asset = &asset + existing.Uploaders = append(existing.Uploaders, up) + } + return nil +} + func isAuthorizedToAdmin(username string, administrators []string) bool { if administrators != nil { for _, s := range administrators { @@ -124,31 +149,6 @@ func isAuthorizedToUpload(username string, administrators []string, permissions return false, false } -func prepareGlobalWriteNewAsset(username string, permissions *permissionsMetadata, asset string, project_dir string) (bool, error) { - if permissions.GlobalWrite == nil || !*(permissions.GlobalWrite) { - return false, nil - } - - asset_dir := filepath.Join(project_dir, asset) - _, err := os.Stat(asset_dir) - - if err == nil || !errors.Is(err, os.ErrNotExist) { - return false, nil - } - - // Updating the permissions in memory and on disk. - is_trusted := true - permissions.Uploaders = append(permissions.Uploaders, uploaderEntry{ Id: username, Asset: &asset, Trusted: &is_trusted }) - - perm_path := filepath.Join(project_dir, permissionsFileName) - err = dumpJson(perm_path, permissions) - if err != nil { - return false, err - } - - return true, nil -} - func sanitizeUploaders(uploaders []unsafeUploaderEntry) ([]uploaderEntry, error) { output := make([]uploaderEntry, len(uploaders)) @@ -168,7 +168,7 @@ func sanitizeUploaders(uploaders []unsafeUploaderEntry) ([]uploaderEntry, error) output[i].Asset = u.Asset output[i].Version = u.Version output[i].Until = u.Until - output[i]. Trusted = u.Trusted + output[i].Trusted = u.Trusted } return output, nil @@ -191,6 +191,7 @@ type unsafePermissionsMetadata struct { func setPermissionsHandler(reqpath string, globals *globalConfiguration) error { incoming := struct { Project *string `json:"project"` + Asset *string `json:"asset"` Permissions *unsafePermissionsMetadata `json:"permissions"` }{} { @@ -206,7 +207,14 @@ func setPermissionsHandler(reqpath string, globals *globalConfiguration) error { err = isMissingOrBadName(incoming.Project) if err != nil { - return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'project' property in %q; %w", reqpath, err)) + return newHttpError(http.StatusBadRequest, fmt.Errorf("missing or invalid 'project' property in %q; %w", reqpath, err)) + } + + if incoming.Asset != nil { + err := isBadName(*(incoming.Asset)) + if err != nil { + return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'asset' property in %q; %w", reqpath, err)) + } } if incoming.Permissions == nil { @@ -240,24 +248,52 @@ func setPermissionsHandler(reqpath string, globals *globalConfiguration) error { return newHttpError(http.StatusForbidden, fmt.Errorf("user %q is not authorized to modify permissions for %q", source_user, project)) } - if incoming.Permissions.Owners != nil { - existing.Owners = incoming.Permissions.Owners - } - if incoming.Permissions.Uploaders != nil { - san, err := sanitizeUploaders(incoming.Permissions.Uploaders) + if incoming.Asset == nil { + if incoming.Permissions.Owners != nil { + existing.Owners = incoming.Permissions.Owners + } + if incoming.Permissions.Uploaders != nil { + san, err := sanitizeUploaders(incoming.Permissions.Uploaders) + if err != nil { + return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'permissions.uploaders' in request; %w", err)) + } + existing.Uploaders = san + } + if incoming.Permissions.GlobalWrite != nil { + existing.GlobalWrite = incoming.Permissions.GlobalWrite + } + + perm_path := filepath.Join(project_dir, permissionsFileName) + err = dumpJson(perm_path, existing) if err != nil { - return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'permissions.uploaders' in request; %w", err)) + return fmt.Errorf("failed to write permissions for %q; %w", project, err) } - existing.Uploaders = san - } - if incoming.Permissions.GlobalWrite != nil { - existing.GlobalWrite = incoming.Permissions.GlobalWrite - } - perm_path := filepath.Join(project_dir, permissionsFileName) - err = dumpJson(perm_path, existing) - if err != nil { - return fmt.Errorf("failed to write permissions for %q; %w", project, err) + } else { + asset_dir := filepath.Join(project_dir, *(incoming.Asset)) + if _, err := os.Stat(asset_dir); errors.Is(err, os.ErrNotExist) { + err = os.Mkdir(asset_dir, 0755) + if err != nil { + return fmt.Errorf("failed to create new asset directory at %q; %w", asset_dir, err) + } + } + + if incoming.Permissions.Uploaders != nil { + san, err := sanitizeUploaders(incoming.Permissions.Uploaders) + if err != nil { + return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'permissions.uploaders' in request; %w", err)) + } + for i, _ := range san { + san[i].Asset = nil + } + + aperms := &permissionsMetadata{ Uploaders: san } + perm_path := filepath.Join(asset_dir, permissionsFileName) + err = dumpJson(perm_path, aperms) + if err != nil { + return fmt.Errorf("failed to write asset-level permissions for %q; %w", asset_dir, err) + } + } } return nil diff --git a/permissions_test.go b/permissions_test.go index e16d34b..35a0a7a 100644 --- a/permissions_test.go +++ b/permissions_test.go @@ -9,6 +9,7 @@ import ( "time" "fmt" "strings" + "encoding/json" ) func TestIdentifyUser(t *testing.T) { @@ -40,12 +41,12 @@ func TestReadPermissions(t *testing.T) { err = os.WriteFile(filepath.Join(f, permissionsFileName), []byte(`{ "owners": ["A", "B", "CC"], "uploaders": [ { "id": "excel" } ] }`), 0644) if err != nil { - t.Fatalf("failed to create test ..latest; %v", err) + t.Fatalf("failed to create test ..permissions; %v", err) } out, err := readPermissions(f) if err != nil { - t.Fatalf("failed to read test ..latest; %v", err) + t.Fatalf("failed to read test ..permissions; %v", err) } if out.Owners == nil || len(out.Owners) != 3 || out.Owners[0] != "A" || out.Owners[1] != "B" || out.Owners[2] != "CC" { @@ -57,6 +58,53 @@ func TestReadPermissions(t *testing.T) { } } +func TestAddAssetPermissions(t *testing.T) { + f, err := os.MkdirTemp("", "test-") + if err != nil { + t.Fatalf("failed to create tempdir; %v", err) + } + + gw := true + existing := &permissionsMetadata{ + Owners: []string{ "foo", "bar" }, + Uploaders: []uploaderEntry{ uploaderEntry{ Id: "blah" } }, + GlobalWrite: &gw, + } + + // Existing attributes are not affected if the asset-level permissions file does not exist. + err = addAssetPermissions(existing, f, "some_asset") + if err != nil { + t.Fatal(err) + } + + if len(existing.Owners) != 2 || !*(existing.GlobalWrite) || len(existing.Uploaders) != 1 || existing.Uploaders[0].Id != "blah" { + t.Fatalf("permissions were changed in ways they should not have been; %v", existing) + } + + // Alright, trying again. + err = os.WriteFile(filepath.Join(f, permissionsFileName), []byte(`{ "uploaders": [ { "id": "excel" }, { "id": "foo", "version": "bar" } ] }`), 0644) + if err != nil { + t.Fatalf("failed to create test ..permissions; %v", err) + } + + err = addAssetPermissions(existing, f, "some_asset") + if err != nil { + t.Fatal(err) + } + + if len(existing.Owners) != 2 || !*(existing.GlobalWrite) { + t.Fatal("permissions were changed in ways they should not have been") + } + + if existing.Uploaders == nil || + len(existing.Uploaders) != 3 || + existing.Uploaders[0].Id != "blah" || existing.Uploaders[0].Asset != nil || + existing.Uploaders[1].Id != "excel" || *(existing.Uploaders[1].Asset) != "some_asset" || + existing.Uploaders[2].Id != "foo" || *(existing.Uploaders[2].Asset) != "some_asset" || *(existing.Uploaders[2].Version) != "bar" { + t.Fatalf("unexpected 'uploaders' value; %v", existing.Uploaders) + } +} + func TestIsAuthorizedToAdmin(t *testing.T) { if isAuthorizedToAdmin("may", []string{"erika"}) { t.Fatalf("unexpected authorization for non-admin") @@ -177,58 +225,6 @@ func TestIsAuthorizedToUpload(t *testing.T) { } } -func TestPrepareGlobalWriteNewAsset(t *testing.T) { - reg, err := os.MkdirTemp("", "") - if err != nil { - t.Fatalf("failed to create the registry; %v", err) - } - - project := "tentacruel" - project_dir := filepath.Join(reg, project) - err = os.Mkdir(project_dir, 0755) - if err != nil { - t.Fatalf("failed to create a project directory; %v", err) - } - - perms := permissionsMetadata { - Owners: []string{ "erika", "sabrina", "misty" }, - Uploaders: []uploaderEntry{}, - } - - can_global_write, err := prepareGlobalWriteNewAsset("foobar", &perms, "FOOBAR", project_dir) - if err != nil || can_global_write { - t.Fatal("expected no global write support") - } - - // Enabling global write. - global_write := true - perms.GlobalWrite = &global_write - can_global_write, err = prepareGlobalWriteNewAsset("foobar", &perms, "FOOBAR", project_dir) - if err != nil || !can_global_write { - t.Fatal("expected global write support") - } - if len(perms.Uploaders) != 1 || perms.Uploaders[0].Id != "foobar" || *(perms.Uploaders[0].Asset) != "FOOBAR" { - t.Fatal("expected user to be added to the uploaders via global write") - } - reperms, err := readPermissions(project_dir) - if err != nil { - t.Fatalf("failed to re-read permissions; %v", err) - } - if len(reperms.Uploaders) != 1 || reperms.Uploaders[0].Id != "foobar" || *(reperms.Uploaders[0].Asset) != "FOOBAR" { - t.Fatal("expected global write's updates to the permissions to be saved to file") - } - - // Global write switches off if the asset already exists, though. - err = os.Mkdir(filepath.Join(project_dir, "FOOBAR"), 0755) - if err != nil { - t.Fatalf("failed to create the asset directory; %v", err) - } - can_global_write, err = prepareGlobalWriteNewAsset("foobar", &perms, "FOOBAR", project_dir) - if err != nil || can_global_write { - t.Fatal("expected no global write support") - } -} - func TestSanitizeUploaders(t *testing.T) { id1 := "may" uploaders := []unsafeUploaderEntry{ unsafeUploaderEntry{ Id: &id1 }, unsafeUploaderEntry{ Id: nil } } @@ -283,19 +279,18 @@ func TestSetPermissionsHandlerHandler(t *testing.T) { t.Fatalf("failed to create a project directory; %v", err) } - err = os.WriteFile( - filepath.Join(project_dir, permissionsFileName), - []byte(fmt.Sprintf(`{ "owners": [ "brock", "ash", "oak", "%s" ], "uploaders": [ { "id": "lance" } ] }`, self)), - 0644, - ) - if err != nil { - t.Fatalf("failed to create some mock permissions; %v", err) - } - globals := newGlobalConfiguration(reg) - // Pure owners. - { + t.Run("owners only", func(t *testing.T) { + err = os.WriteFile( + filepath.Join(project_dir, permissionsFileName), + []byte(fmt.Sprintf(`{ "owners": [ "brock", "ash", "oak", "%s" ], "uploaders": [ { "id": "lance" } ] }`, self)), + 0644, + ) + if err != nil { + t.Fatalf("failed to create some mock permissions; %v", err) + } + reqpath, err := dumpRequest( "set_permissions", fmt.Sprintf(`{ "project": "%s", "permissions": { "owners": [ "%s", "gary" ] } }`, project, self), @@ -320,10 +315,18 @@ func TestSetPermissionsHandlerHandler(t *testing.T) { if perms.Uploaders == nil || len(perms.Uploaders) != 1 || perms.Uploaders[0].Id != "lance" { t.Fatal("uploaders were not preserved as expected") } - } + }) + + t.Run("uploaders only", func(t *testing.T) { + err = os.WriteFile( + filepath.Join(project_dir, permissionsFileName), + []byte(fmt.Sprintf(`{ "owners": [ "brock", "ash", "oak", "%s" ], "uploaders": [ { "id": "lance" } ] }`, self)), + 0644, + ) + if err != nil { + t.Fatalf("failed to create some mock permissions; %v", err) + } - // Pure uploaders. - { reqpath, err := dumpRequest( "set_permissions", fmt.Sprintf(`{ "project": "%s", "permissions": { "uploaders": [ { "id": "lorelei", "until": "2022-02-02T20:20:20Z" }, { "id": "karen" } ] } }`, project), @@ -342,16 +345,24 @@ func TestSetPermissionsHandlerHandler(t *testing.T) { t.Fatalf("failed to read the new permissions; %v", err) } - if perms.Owners == nil || len(perms.Owners) != 2 { - t.Fatal("owners were not preservfed as expected") + if perms.Owners == nil || len(perms.Owners) != 4 { + t.Fatal("owners were not preserved as expected") } if perms.Uploaders == nil || len(perms.Uploaders) != 2 || perms.Uploaders[0].Id != "lorelei" || perms.Uploaders[0].Until == nil || perms.Uploaders[1].Id != "karen" { t.Fatal("uploaders were not preserved as expected") } - } + }) + + t.Run("invalid uploaders", func(t *testing.T) { + err = os.WriteFile( + filepath.Join(project_dir, permissionsFileName), + []byte(fmt.Sprintf(`{ "owners": [ "brock", "ash", "oak", "%s" ], "uploaders": [ { "id": "lance" } ] }`, self)), + 0644, + ) + if err != nil { + t.Fatalf("failed to create some mock permissions; %v", err) + } - // Invalid uploaders. - { reqpath, err := dumpRequest( "set_permissions", fmt.Sprintf(`{ "project": "%s", "permissions": { "uploaders": [ { } ] } }`, project), @@ -377,10 +388,9 @@ func TestSetPermissionsHandlerHandler(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "invalid 'permissions.uploaders'") { t.Fatal("expected a permissions failure for invalid uploaders") } - } + }) - // Not authorized. - { + t.Run("not authorized", func(t *testing.T) { err = os.WriteFile( filepath.Join(project_dir, permissionsFileName), []byte(`{ "owners": [ "brock" ], "uploaders": [ { "id": "lance" } ] } `), @@ -402,10 +412,9 @@ func TestSetPermissionsHandlerHandler(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "not authorized") { t.Fatalf("unexpected authorization for a non-owner") } - } + }) - // Global write is recognized. - { + t.Run("global write", func(t *testing.T) { err = os.WriteFile( filepath.Join(project_dir, permissionsFileName), []byte(fmt.Sprintf(`{ "owners": [ "%s" ] }`, self)), @@ -425,7 +434,7 @@ func TestSetPermissionsHandlerHandler(t *testing.T) { err = setPermissionsHandler(reqpath, &globals) if err != nil { - t.Fatalf("failed to write permissions with global write") + t.Fatalf("failed to write permissions with global write; %v", err) } perms, err := readPermissions(project_dir) @@ -435,5 +444,50 @@ func TestSetPermissionsHandlerHandler(t *testing.T) { if perms.GlobalWrite == nil || !(*perms.GlobalWrite) { t.Fatal("expected global write to be enabled") } - } + }) + + t.Run("asset level", func(t *testing.T) { + err = os.WriteFile( + filepath.Join(project_dir, permissionsFileName), + []byte(fmt.Sprintf(`{ "owners": [ "%s" ] }`, self)), + 0644, + ) + if err != nil { + t.Fatalf("failed to create some mock permissions") + } + + reqpath, err := dumpRequest( + "set_permissions", + fmt.Sprintf(`{ "project": "%s", "asset": "BLAH", "permissions": { "uploaders": [ { "id": "YAY", "trusted": true }, { "id": "foo", "asset": "bar", "version": "stuff" } ] } }`, project), + ) + if err != nil { + t.Fatalf("failed to dump a request type; %v", err) + } + + err = setPermissionsHandler(reqpath, &globals) + if err != nil { + t.Fatalf("failed to write asset-level permissions; %v", err) + } + + perms := &permissionsMetadata{} + content, err := os.ReadFile(filepath.Join(project_dir, "BLAH", permissionsFileName)) + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(content, perms) + if err != nil { + t.Fatal(err) + } + if len(perms.Uploaders) != 2 || + perms.Uploaders[0].Id != "YAY" || perms.Uploaders[0].Asset != nil || !*(perms.Uploaders[0].Trusted) || + perms.Uploaders[1].Id != "foo" || perms.Uploaders[1].Asset != nil || *(perms.Uploaders[1].Version) != "stuff" { + t.Errorf("unexpected uploaders after asset-level permissions setting; %v", perms.Uploaders) + } + + // Still works when directory already exists. + err = setPermissionsHandler(reqpath, &globals) + if err != nil { + t.Fatalf("failed to rewrite asset-level permissions; %v", err) + } + }) } diff --git a/upload.go b/upload.go index 5f933bf..4cbe7ad 100644 --- a/upload.go +++ b/upload.go @@ -113,13 +113,18 @@ func uploadHandler(reqpath string, globals *globalConfiguration) error { return fmt.Errorf("failed to read permissions for %q; %w", project, err) } - // Check if this upload is authorized via the global write permissions. - global_write_new_asset, err := prepareGlobalWriteNewAsset(req_user, perms, *(request.Asset), project_dir) - if err != nil { - return fmt.Errorf("failed to update the permissions for a new asset with global write; %w", err) - } + asset := *(request.Asset) + asset_dir := filepath.Join(project_dir, asset) + _, err = os.Stat(asset_dir) + asset_exists := !(err != nil && errors.Is(err, os.ErrNotExist)) + + use_global_write := perms.GlobalWrite != nil && *(perms.GlobalWrite) && !asset_exists + if !use_global_write { + err := addAssetPermissions(perms, asset_dir, asset) + if err != nil { + return fmt.Errorf("failed to read permissions for asset %q in %q; %w", asset, project, err) + } - if !global_write_new_asset { ok, trusted := isAuthorizedToUpload(req_user, globals.Administrators, perms, request.Asset, request.Version) if !ok { return newHttpError(http.StatusForbidden, fmt.Errorf("user '" + req_user + "' is not authorized to upload to '" + project + "'")) @@ -130,15 +135,23 @@ func uploadHandler(reqpath string, globals *globalConfiguration) error { } // Configuring the asset and version. - asset := *(request.Asset) - asset_dir := filepath.Join(project_dir, asset) - if _, err := os.Stat(asset_dir); errors.Is(err, os.ErrNotExist) { + if !asset_exists { err = os.Mkdir(asset_dir, 0755) if err != nil { return fmt.Errorf("failed to create a new asset directory inside %q; %w", asset_dir, err) } } + if use_global_write { // adding asset-level permissions. + is_trusted := true + asset_permissions := &permissionsMetadata{ Uploaders: []uploaderEntry{ uploaderEntry{ Id: req_user, Trusted: &is_trusted } } } + perm_path := filepath.Join(asset_dir, permissionsFileName) + err := dumpJson(perm_path, asset_permissions) + if err != nil { + return fmt.Errorf("failed to create new permissions for asset %q in %q; %w", asset, project, err) + } + } + version := *(request.Version) version_dir := filepath.Join(asset_dir, version) if _, err := os.Stat(version_dir); err == nil { diff --git a/upload_test.go b/upload_test.go index 9ed4ec7..54ba572 100644 --- a/upload_test.go +++ b/upload_test.go @@ -495,36 +495,71 @@ func TestUploadHandlerGlobalWrite(t *testing.T) { t.Fatalf("failed to create the project; %v", err) } - // Now making the request. - asset := "BAR" - version := "whee" - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s" }`, filepath.Base(src), project, asset, version) - reqname, err := dumpRequest("upload", req_string) - if err != nil { - t.Fatalf("failed to create upload request; %v", err) - } + t.Run("okay", func(t *testing.T) { + asset := "BAR" + version := "whee" + req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s" }`, filepath.Base(src), project, asset, version) + reqname, err := dumpRequest("upload", req_string) + if err != nil { + t.Fatalf("failed to create upload request; %v", err) + } - err = uploadHandler(reqname, &globals) - if err != nil { - t.Fatalf("failed to perform a global write upload; %v", err) - } + err = uploadHandler(reqname, &globals) + if err != nil { + t.Fatalf("failed to perform a global write upload; %v", err) + } - // Checking that permissions are set up correctly. - perms, err := readPermissions(filepath.Join(globals.Registry, project)) - if err != nil { - t.Fatalf("failed to read the new permissions; %v", err) - } - if len(perms.Uploaders) != 1 || perms.Uploaders[0].Id != self.Username || *(perms.Uploaders[0].Asset) != asset { - t.Fatalf("global write upload did not update the uploaders; %v", err) - } + // Checking that permissions are set up correctly. + perms := &permissionsMetadata{} + err = addAssetPermissions(perms, filepath.Join(globals.Registry, project, asset), asset) + if err != nil { + t.Fatalf("failed to read the new permissions; %v", err) + } + if len(perms.Uploaders) != 1 || perms.Uploaders[0].Id != self.Username || *(perms.Uploaders[0].Asset) != asset { + t.Fatalf("global write upload did not update the uploaders; %v", err) + } - // Check that the upload completed. - destination := filepath.Join(reg, project, asset, version) - man, err := readManifest(destination) - info, ok := man["evolution"] - if !ok || int(info.Size) != len("haunter") || info.Link != nil { - t.Fatal("unexpected manifest entry for 'evolution'") - } + // Check that the upload completed. + destination := filepath.Join(reg, project, asset, version) + man, err := readManifest(destination) + info, ok := man["evolution"] + if !ok || int(info.Size) != len("haunter") || info.Link != nil { + t.Fatal("unexpected manifest entry for 'evolution'") + } + + // Checking that the new permissions are acknowledged so that we can add another version of the same asset. + version = "whee2" + req_string = fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s" }`, filepath.Base(src), project, asset, version) + reqname, err = dumpRequest("upload", req_string) + if err != nil { + t.Fatalf("failed to create upload request; %v", err) + } + + err = uploadHandler(reqname, &globals) + if err != nil { + t.Fatalf("failed to perform another upload to the same asset; %v", err) + } + }) + + t.Run("already there", func(t *testing.T) { + asset := "FOO" + err := os.Mkdir(filepath.Join(reg, project, asset), 0755) + if err != nil { + t.Fatal(err) + } + + version := "whee" + req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s" }`, filepath.Base(src), project, asset, version) + reqname, err := dumpRequest("upload", req_string) + if err != nil { + t.Fatalf("failed to create upload request; %v", err) + } + + err = uploadHandler(reqname, &globals) + if err == nil || !strings.Contains(err.Error(), "not authorized") { + t.Fatal("global write should have failed if asset already exists") + } + }) } func TestUploadHandlerProbation(t *testing.T) {