From cf896e8054c85cc29170adc8324109b2fdaf5036 Mon Sep 17 00:00:00 2001 From: Aaron Lun Date: Tue, 9 Apr 2024 09:42:58 -0700 Subject: [PATCH] Limit creation of new projects to administrators, a la gypsum. (#10) This aligns with the gypsum philosophy and gives administrators more control by preventing users from just creating loads of projects; any projects that are created must have some owners that are held accountable for usage. The change also simplifies matters by separating the upload from the setup of permissions. We also eliminated the idea of incremental projects. Given that an administrator has to create the project anyway, there isn't much benefit to automating the name creation. While we're here, we might as well get rid of the incremental versions and simplify the code. --- README.md | 39 ++-- create.go | 100 +++++++++ create_test.go | 203 +++++++++++++++++ main.go | 10 +- upload.go | 331 +++++----------------------- upload_test.go | 585 +++++++++++-------------------------------------- 6 files changed, 514 insertions(+), 754 deletions(-) create mode 100644 create.go create mode 100644 create_test.go diff --git a/README.md b/README.md index ee02cc8..cecc28e 100644 --- a/README.md +++ b/README.md @@ -151,9 +151,24 @@ Specifically, users should write the JSON request body to a file inside the stag Once the write is complete, this file can be renamed to a file with said prefix. This ensures that the Gobbler does not read a partially-written file. +### Creating projects (admin) + +Administrators are responsible for creating new projects within the registry. +This is done using the write-and-rename paradigm to create a file with the `request-create_project-` prefix. +This file should be JSON-formatted with the following properties: + +- `project`: string containing the name of the new project. + This should not contain `/`, `\`, or `.`. +- `permissions` (optional): an object containing either or both of `owners` and `uploaders`. + Each of these properties has the same type as described [above](#permissions). + If `owners` is not supplied, it is automatically set to a length-1 array containing only the uploading user. + This property is ignored when uploading a new version of an asset to an existing project. + +On success, a new project is created with the designated permissions and a JSON formatted file will be created in `responses` with the `status` property set to `SUCCESS`. + ### Uploads and updates -To upload a new version of an asset of a project (either in an existing project, or in a new project), users should create a temporary directory within the staging directory. +To upload a new version of an asset of a project, users should create a temporary directory within the staging directory. The directory may have any name but should avoid starting with `request-`. Files within this temporary directory will be transferred to the appropriate subdirectory within the registry, subject to the following rules: @@ -164,26 +179,16 @@ Files within this temporary directory will be transferred to the appropriate sub Once this directory is constructed and populated, the user should use the write-and-rename paradigm to create a file with the `request-upload-` prefix. This file should be JSON-formatted with the following properties: -- `project`: string containing the explicit name of the project. - The string should start with a lower-case letter to distinguish from prefixed names constructed by incrementing series. - This may also be missing, in which case `prefix` should be supplied. -- `prefix` (optional): string containing an all-uppercase project prefix, - used to derive a project name from an incrementing series if `project` is not supplied. - Ignored if `project` is provided. +- `project`: string containing the name of an existing project. - `asset`: string containing the name of the asset. -- `version` (optional): string containing the name of the version. - This may be missing, in which case the version is named according to an incrementing series for that project-asset combination. + This should not contain `/`, `\`, or `.`. +- `version`: string containing the name of the version. + This should not contain `/`, `\`, or `.`. - `source`: string containing the name of the temporary directory, itself containing the files to be uploaded for this version of the asset. This temporary directory is expected to be inside the staging directory. -- `permissions` (optional): an object containing either or both of `owners` and `uploaders`. - Each of these properties has the same type as described [above](#permissions). - If `owners` is not supplied, it is automatically set to a length-1 array containing only the uploading user. - This property is ignored when uploading a new version of an asset to an existing project. - `on_probation` (optional): boolean specifying whether this version of the asset should be considered as probational. -On success, the files will be transferred to the registry and a JSON formatted file will be created in `responses`. -This will have the `status` property set to `SUCCESS` and the `project` and `version` strings set to the names of the project and version, respectively. -These will be the same as the request parameters if supplied, otherwise they are created from the relevant incrementing series. +On success, the files will be transferred to the registry and a JSON formatted file will be created in `responses` with the `status` property set to `SUCCESS`. ### Setting permissions @@ -195,7 +200,7 @@ This file should be JSON-formatted with the following properties: 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. -On success, the permissions in the registry are modified and a JSON formatted file will be created in `responses` with the `status` property set to `SUCCESS`.. +On success, the permissions in the registry are modified and a JSON formatted file will be created in `responses` with the `status` property set to `SUCCESS`. ### Handling probation diff --git a/create.go b/create.go new file mode 100644 index 0000000..ce56a58 --- /dev/null +++ b/create.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "time" + "path/filepath" + "os" + "encoding/json" + "strconv" + "errors" +) + +func createProjectHandler(reqpath string, globals *globalConfiguration) error { + req_user, err := identifyUser(reqpath) + if err != nil { + return fmt.Errorf("failed to find owner of %q; %w", reqpath, err) + } + if !isAuthorizedToAdmin(req_user, globals.Administrators) { + return fmt.Errorf("user %q is not authorized to create a project", req_user) + } + + request := struct { + Project *string `json:"project"` + Permissions *unsafePermissionsMetadata `json:"permissions"` + }{} + + // Reading in the request. + handle, err := os.ReadFile(reqpath) + if err != nil { + return fmt.Errorf("failed to read %q; %w", reqpath, err) + } + err = json.Unmarshal(handle, &request) + if err != nil { + return fmt.Errorf("failed to parse JSON from %q; %w", reqpath, err) + } + + if request.Project == nil { + return fmt.Errorf("expected a 'project' property in %q", reqpath) + } + project := *(request.Project) + + return createProject(project, request.Permissions, req_user, globals) +} + +func createProject(project string, inperms *unsafePermissionsMetadata, req_user string, globals *globalConfiguration) error { + err := isBadName(project) + if err != nil { + return fmt.Errorf("invalid project name; %w", err) + } + + // Creating a new project from a pre-supplied name. + project_dir := filepath.Join(globals.Registry, project) + if _, err = os.Stat(project_dir); !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("project %q already exists", project) + } + + // No need to lock before MkdirAll, it just no-ops if the directory already exists. + err = os.MkdirAll(project_dir, 0755) + + globals.Locks.LockPath(project_dir, 1000 * time.Second) + if err != nil { + return fmt.Errorf("failed to acquire the lock on %q; %w", project_dir, err) + } + defer globals.Locks.UnlockPath(project_dir) + + perms := permissionsMetadata{} + if inperms != nil && inperms.Owners != nil { + perms.Owners = inperms.Owners + } else { + perms.Owners = []string{ req_user } + } + if inperms != nil && inperms.Uploaders != nil { + san, err := sanitizeUploaders(inperms.Uploaders) + if err != nil { + return fmt.Errorf("invalid 'permissions.uploaders' in the request details; %w", err) + } + perms.Uploaders = san + } else { + perms.Uploaders = []uploaderEntry{} + } + + err = dumpJson(filepath.Join(project_dir, permissionsFileName), &perms) + if err != nil { + return fmt.Errorf("failed to write permissions for %q; %w", project_dir, err) + } + + // Dumping a mock quota and usage file for consistency with gypsum. + // Note that the quota isn't actually enforced yet. + err = os.WriteFile(filepath.Join(project_dir, "..quota"), []byte("{ \"baseline\": 1000000000, \"growth_rate\": 1000000000, \"year\": " + strconv.Itoa(time.Now().Year()) + " }"), 0755) + if err != nil { + return fmt.Errorf("failed to write quota for '" + project_dir + "'; %w", err) + } + + err = os.WriteFile(filepath.Join(project_dir, usageFileName), []byte("{ \"total\": 0 }"), 0755) + if err != nil { + return fmt.Errorf("failed to write usage for '" + project_dir + "'; %w", err) + } + + return nil +} diff --git a/create_test.go b/create_test.go new file mode 100644 index 0000000..c31cfc5 --- /dev/null +++ b/create_test.go @@ -0,0 +1,203 @@ +package main + +import ( + "testing" + "path/filepath" + "fmt" + "os/user" + "strings" +) + +func TestCreateProjectSimple(t *testing.T) { + reg, err := constructMockRegistry() + if err != nil { + t.Fatalf("failed to create the registry; %v", err) + } + globals := newGlobalConfiguration(reg) + + self, err := user.Current() + if err != nil { + t.Fatalf("failed to determine the current user; %v", err) + } + globals.Administrators = append(globals.Administrators, self.Username) + + project := "foo" + req_string := fmt.Sprintf(`{ "project": "%s"}`, project) + reqname, err := dumpRequest("create_project", req_string) + if err != nil { + t.Fatalf("failed to create 'create_project' request; %v", err) + } + + err = createProjectHandler(reqname, &globals) + if err != nil { + t.Fatalf("failed to create project; %v", err) + } + + usage, err := readUsage(filepath.Join(reg, project)) + if err != nil { + t.Fatalf("failed to read usage file; %v", err) + } + if usage.Total != 0 { + t.Fatalf("usage should be zero for a newly created project; %v", err) + } +} + +func TestCreateProjectFailures(t *testing.T) { + reg, err := constructMockRegistry() + if err != nil { + t.Fatalf("failed to create the registry; %v", err) + } + globals := newGlobalConfiguration(reg) + + { + project := "foo" + req_string := fmt.Sprintf(`{ "project": "%s"}`, project) + reqname, err := dumpRequest("create_project", req_string) + if err != nil { + t.Fatalf("failed to create 'create_project' request; %v", err) + } + + err = createProjectHandler(reqname, &globals) + if err == nil || !strings.Contains(err.Error(), "not authorized") { + t.Fatalf("creation should have failed without no permissions; %v", err) + } + } + + self, err := user.Current() + if err != nil { + t.Fatalf("failed to determine the current user; %v", err) + } + globals.Administrators = append(globals.Administrators, self.Username) + + { + project := "..foo" + req_string := fmt.Sprintf(`{ "project": "%s"}`, project) + reqname, err := dumpRequest("create_project", req_string) + if err != nil { + t.Fatalf("failed to create 'create_project' request; %v", err) + } + + err = createProjectHandler(reqname, &globals) + if err == nil || !strings.Contains(err.Error(), "invalid project name") { + t.Fatalf("creation should have failed with an invalid project; %v", err) + } + } + + { + project := "foo" + req_string := fmt.Sprintf(`{ "project": "%s"}`, project) + reqname, err := dumpRequest("create_project", req_string) + if err != nil { + t.Fatalf("failed to create 'create_project' request; %v", err) + } + + err = createProjectHandler(reqname, &globals) + if err != nil { + t.Fatalf("project creation failed; %v", err) + } + + err = createProjectHandler(reqname, &globals) + if err == nil || !strings.Contains(err.Error(), "already exists") { + t.Fatalf("duplicate project creation should have failed; %v", err) + } + } +} + +func TestCreateProjectNewPermissions(t *testing.T) { + reg, err := constructMockRegistry() + if err != nil { + t.Fatalf("failed to create the registry; %v", err) + } + globals := newGlobalConfiguration(reg) + + self, err := user.Current() + if err != nil { + t.Fatalf("failed to determine the current user; %v", err) + } + globals.Administrators = append(globals.Administrators, self.Username) + + // Checking that owners are respected. + { + project := "indigo_league" + perm_string := `{ "owners": [ "YAY", "NAY" ] }` + req_string := fmt.Sprintf(`{ "project": "%s", "permissions": %s }`, project, perm_string) + reqname, err := dumpRequest("create_project", req_string) + if err != nil { + t.Fatalf("failed to create 'create_project' request; %v", err) + } + + err = createProjectHandler(reqname, &globals) + if err != nil { + t.Fatalf("failed to create a project; %v", err) + } + + perms, err := readPermissions(filepath.Join(reg, project)) + if err != nil { + t.Fatalf("failed to read the permissions; %v", err) + } + if len(perms.Owners) != 2 || perms.Owners[0] != "YAY" || perms.Owners[1] != "NAY" { + t.Fatal("failed to set the owners correctly in the project permissions") + } + if len(perms.Uploaders) != 0 { + t.Fatal("failed to set the uploaders correctly in the project permissions") + } + } + + { + project := "johto_journeys" + new_id := "foo" + perm_string := fmt.Sprintf(`{ "uploaders": [ { "id": "%s" } ] }`, new_id) + + req_string := fmt.Sprintf(`{ "project": "%s", "permissions": %s }`, project, perm_string) + reqname, err := dumpRequest("create_project", req_string) + if err != nil { + t.Fatalf("failed to create 'create_project' request; %v", err) + } + + err = createProjectHandler(reqname, &globals) + if err != nil { + t.Fatalf("failed to create a project; %v", err) + } + + perms, err := readPermissions(filepath.Join(reg, project)) + if err != nil { + t.Fatalf("failed to read the permissions; %v", err) + } + if len(perms.Owners) != 1 { // switches to the creating user. + t.Fatal("failed to set the owners correctly in the project permissions") + } + if len(perms.Uploaders) != 1 || perms.Uploaders[0].Id != new_id { + t.Fatal("failed to set the uploaders correctly in the project permissions") + } + } + + // Check that uploaders in the permissions are validated. + { + project := "battle_frontier" + req_string := fmt.Sprintf(`{ "project": "%s", "permissions": { "uploaders": [{}] } }`, project) + reqname, err := dumpRequest("create_project", req_string) + if err != nil { + t.Fatalf("failed to create 'create_project' request; %v", err) + } + + err = createProjectHandler(reqname, &globals) + if err == nil || !strings.Contains(err.Error(), "invalid 'permissions.uploaders'") { + t.Fatalf("expected project creation to fail from invalid 'uploaders'") + } + } + + { + project := "sinnoh_league" + perm_string := `{ "uploaders": [ { "id": "argle", "until": "bargle" } ] }` + req_string := fmt.Sprintf(`{ "project": "%s", "permissions": %s }`, project, perm_string) + reqname, err := dumpRequest("create_project", req_string) + if err != nil { + t.Fatalf("failed to create 'create_project' request; %v", err) + } + + err = createProjectHandler(reqname, &globals) + if err == nil || !strings.Contains(err.Error(), "invalid 'permissions.uploaders'") { + t.Fatalf("expected project creation to fail from invalid 'uploaders'") + } + } +} diff --git a/main.go b/main.go index 98d6417..6dde445 100644 --- a/main.go +++ b/main.go @@ -98,13 +98,7 @@ func main() { payload := map[string]interface{}{} if strings.HasPrefix(reqtype, "upload-") { - config, err0 := uploadHandler(reqpath, &globals) - if err0 == nil { - payload["project"] = config.Project - payload["version"] = config.Version - } else { - reportable_err = err0 - } + reportable_err = uploadHandler(reqpath, &globals) } else if strings.HasPrefix(reqtype, "refresh_latest-") { res, err0 := refreshLatestHandler(reqpath, &globals) @@ -130,6 +124,8 @@ func main() { reportable_err = approveProbationHandler(reqpath, &globals) } else if strings.HasPrefix(reqtype, "reject_probation-") { reportable_err = rejectProbationHandler(reqpath, &globals) + } else if strings.HasPrefix(reqtype, "create_project-") { + reportable_err = createProjectHandler(reqpath, &globals) } else if strings.HasPrefix(reqtype, "delete_project-") { reportable_err = deleteProjectHandler(reqpath, &globals) } else if strings.HasPrefix(reqtype, "delete_asset-") { diff --git a/upload.go b/upload.go index 4ff9850..0702f95 100644 --- a/upload.go +++ b/upload.go @@ -6,271 +6,51 @@ import ( "path/filepath" "os" "encoding/json" - "strconv" - "strings" "errors" - "regexp" - "unicode" ) -func incrementSeriesPath(prefix string, dir string) string { - if prefix == "" { - return filepath.Join(dir, "..series") - } else { - return filepath.Join(dir, "..series_" + prefix) - } -} - -func incrementSeries(prefix string, dir string) (string, error) { - series_path := incrementSeriesPath(prefix, dir) - - num := 0 - if _, err := os.Stat(series_path); err == nil { - content, err := os.ReadFile(series_path) - if err != nil { - return "", fmt.Errorf("failed to read '" + series_path + "'; %w", err) - } - num, err = strconv.Atoi(string(content)) - if err != nil { - return "", fmt.Errorf("failed to determine latest number from '" + series_path + "; %w", err) - } - } - - num += 1 - as_str := strconv.Itoa(num) - candidate_name := prefix + as_str - - // Checking that it doesn't already exist. - if _, err := os.Stat(filepath.Join(dir, candidate_name)); err == nil { - dhandle, err := os.Open(dir) - if err != nil { - return "", fmt.Errorf("failed to obtain a handle for the output directory; %w", err) - } - - all_names, err := dhandle.Readdirnames(-1) - if err != nil { - return "", fmt.Errorf("failed to read subdirectories of the output directory; %w", err) - } - if prefix != "" { - for _, subdir := range all_names { - if strings.HasPrefix(subdir, prefix) { - curnum, err := strconv.Atoi(strings.TrimPrefix(subdir, prefix)) - if err != nil && curnum > num { - num = curnum - } - } - } - } else { - for _, subdir := range all_names { - curnum, err := strconv.Atoi(subdir) - if err != nil && curnum > num { - num = curnum - } - } - } - - num += 1 - as_str = strconv.Itoa(num) - candidate_name = prefix + as_str - } - - // Updating the series. - err := os.WriteFile(series_path, []byte(as_str), 0644) +func configureAsset(project_dir string, asset string) error { + err := isBadName(asset) if err != nil { - return "", fmt.Errorf("failed to update the series counter for '" + prefix + "'; %w", err) + return fmt.Errorf("invalid asset name %q; %w", asset, err) } - return candidate_name, nil -} - -func configureProject(registry string, username string, project, prefix *string, locks *pathLocks) (string, bool, error) { - // Creating a new project from a series. - if project == nil { - if prefix == nil { - return "", false, errors.New("expected a 'prefix' property in the request details") - } - prefix_str := *(prefix) - re, _ := regexp.Compile("^[A-Z]+$") - if !re.MatchString(prefix_str) { - return "", false, fmt.Errorf("prefix must contain only uppercase letters (got %q)", prefix_str) - } - - // Obtaining a global lock to avoid simultaneous increments. - prefix_path := filepath.Join(registry, prefix_str) - err := locks.LockPath(prefix_path, 1000 * time.Second) - if err != nil { - return "", false, fmt.Errorf("failed to acquire the global registry lock; %w", err) - } - defer locks.UnlockPath(prefix_path) - - candidate_name, err := incrementSeries(prefix_str, registry) - if err != nil { - return "", false, err - } - - err = os.Mkdir(filepath.Join(registry, candidate_name), 0755) - if err != nil { - return "", false, fmt.Errorf("failed to make a new directory'; %w", err) - } - - return candidate_name, true, nil - } - - project_str := *project - err := isBadName(project_str) - if err != nil { - return "", false, fmt.Errorf("invalid project name; %w", err) - } - - // Creating a new project from a pre-supplied name. - project_dir := filepath.Join(registry, project_str) - info, err := os.Stat(project_dir) - if errors.Is(err, os.ErrNotExist) { - if unicode.IsUpper(rune(project_str[0])) { - return "", false, errors.New("new user-supplied project names should not start with an uppercase letter") - } - - // No need to lock here, MkdirAll just no-ops if the directory already exists. - err := os.MkdirAll(filepath.Join(registry, project_str), 0755) - if err != nil { - return "", false, fmt.Errorf("failed to make a new directory'; %w", err) - } - - return project_str, true, nil - } - - // Updating an existing directory. - if err != nil || !info.IsDir() { - return "", false, fmt.Errorf("failed to inspect an existing project directory %q; %w", project_str, err) - } - - return project_str, false, nil -} - -func populateNewProjectDirectory(dir string, username string, permissions *unsafePermissionsMetadata) error { - // Adding permissions. - perms := permissionsMetadata{} - if permissions != nil && permissions.Owners != nil { - perms.Owners = permissions.Owners - } else { - perms.Owners = []string{ username } - } - if permissions != nil && permissions.Uploaders != nil { - san, err := sanitizeUploaders(permissions.Uploaders) - if err != nil { - return fmt.Errorf("invalid 'permissions.uploaders' in the request details; %w", err) - } - perms.Uploaders = san - } else { - perms.Uploaders = []uploaderEntry{} - } - - err := dumpJson(filepath.Join(dir, permissionsFileName), &perms) - if err != nil { - return fmt.Errorf("failed to write permissions for %q; %w", dir, err) - } - - // Dumping a mock quota and usage file for consistency with gypsum. - // Note that the quota isn't actually enforced yet. - err = os.WriteFile(filepath.Join(dir, "..quota"), []byte("{ \"baseline\": 1000000000, \"growth_rate\": 1000000000, \"year\": " + strconv.Itoa(time.Now().Year()) + " }"), 0755) - if err != nil { - return fmt.Errorf("failed to write quota for '" + dir + "'; %w", err) - } - - err = os.WriteFile(filepath.Join(dir, usageFileName), []byte("{ \"total\": 0 }"), 0755) - if err != nil { - return fmt.Errorf("failed to write usage for '" + dir + "'; %w", err) - } - - return nil -} - -func configureAsset(project_dir string, asset *string) (string, bool, error) { - if asset == nil { - return "", false, errors.New("expected an 'asset' property in the request details") - } - - asset_str := *asset - err := isBadName(asset_str) - if err != nil { - return "", false, fmt.Errorf("invalid asset name %q; %w", asset_str, err) - } - - asset_dir := filepath.Join(project_dir, asset_str) - is_new := false + asset_dir := filepath.Join(project_dir, asset) if _, err := os.Stat(asset_dir); errors.Is(err, os.ErrNotExist) { err = os.Mkdir(asset_dir, 0755) if err != nil { - return "", false, fmt.Errorf("failed to create a new asset directory inside %q; %w", asset_dir, err) + return fmt.Errorf("failed to create a new asset directory inside %q; %w", asset_dir, err) } - is_new = true } - return asset_str, is_new, nil + return nil } -func configureVersion(asset_dir string, is_new_project bool, version *string) (string, error) { - series_path := incrementSeriesPath("", asset_dir) - - // Creating a new version from a series. - if version == nil { - if _, err := os.Stat(series_path); errors.Is(err, os.ErrNotExist) { - if !is_new_project { // check it's not a newly created project, in which case it wouldn't have a series yet. - return "", errors.New("must provide 'version' in '" + asset_dir + "' initialized without a version series") - } - } - - candidate_name, err := incrementSeries("", asset_dir) - if err != nil { - return "", err - } - - candidate_path := filepath.Join(asset_dir, candidate_name) - err = os.Mkdir(candidate_path, 0755) - if err != nil { - return "", fmt.Errorf("failed to create a new version directory at '" + candidate_path + "'; %w", err) - } - - return candidate_name, nil - } - - // Otherwise using the user-supplied version name. - if _, err := os.Stat(series_path); err == nil { - return "", errors.New("cannot use user-supplied 'version' in '" + asset_dir + "' initialized with a version series") - } - - version_str := *version - err := isBadName(version_str) +func configureVersion(asset_dir string, version string) error { + err := isBadName(version) if err != nil { - return "", fmt.Errorf("invalid version name %q; %w", version_str, err) + return fmt.Errorf("invalid version name %q; %w", version, err) } - candidate_path := filepath.Join(asset_dir, version_str) + candidate_path := filepath.Join(asset_dir, version) if _, err := os.Stat(candidate_path); err == nil { - return "", fmt.Errorf("version %q already exists in %q", version_str, asset_dir) + return fmt.Errorf("version %q already exists in %q", version, asset_dir) } err = os.Mkdir(candidate_path, 0755) if err != nil { - return "", fmt.Errorf("failed to create a new version directory at %q; %w", candidate_path, err) + return fmt.Errorf("failed to create a new version directory at %q; %w", candidate_path, err) } - return version_str, nil -} - -type uploadConfiguration struct { - Project string - Version string + return nil } -func uploadHandler(reqpath string, globals *globalConfiguration) (*uploadConfiguration, error) { +func uploadHandler(reqpath string, globals *globalConfiguration) error { request := struct { Source *string `json:"source"` - Prefix *string `json:"prefix"` Project *string `json:"project"` Asset *string `json:"asset"` Version *string `json:"version"` - Permissions *unsafePermissionsMetadata `json:"permissions"` OnProbation *bool `json:"on_probation"` }{} @@ -278,7 +58,7 @@ func uploadHandler(reqpath string, globals *globalConfiguration) (*uploadConfigu req_user, err := identifyUser(reqpath) if err != nil { - return nil, fmt.Errorf("failed to find owner of %q; %w", reqpath, err) + return fmt.Errorf("failed to find owner of %q; %w", reqpath, err) } var source string @@ -287,75 +67,78 @@ func uploadHandler(reqpath string, globals *globalConfiguration) (*uploadConfigu { handle, err := os.ReadFile(reqpath) if err != nil { - return nil, fmt.Errorf("failed to read %q; %w", reqpath, err) + return fmt.Errorf("failed to read %q; %w", reqpath, err) } err = json.Unmarshal(handle, &request) if err != nil { - return nil, fmt.Errorf("failed to parse JSON from %q; %w", reqpath, err) + return fmt.Errorf("failed to parse JSON from %q; %w", reqpath, err) } if request.Source == nil { - return nil, fmt.Errorf("expected a 'source' property in %q; %w", reqpath, err) + return fmt.Errorf("expected a 'source' property in %q; %w", reqpath, err) } source = *(request.Source) if source != filepath.Base(source) { - return nil, fmt.Errorf("expected 'source' to be in the same directory as %q", reqpath) + return fmt.Errorf("expected 'source' to be in the same directory as %q", reqpath) } source = filepath.Join(filepath.Dir(reqpath), source) source_user, err := identifyUser(source) if err != nil { - return nil, fmt.Errorf("failed to find owner of %q; %w", source, err) + return fmt.Errorf("failed to find owner of %q; %w", source, err) } if source_user != req_user { - return nil, fmt.Errorf("requesting user must be the same as the owner of the 'source' directory (%s vs %s)", source_user, req_user) + return fmt.Errorf("requesting user must be the same as the owner of the 'source' directory (%s vs %s)", source_user, req_user) } } on_probation := request.OnProbation != nil && *(request.OnProbation) // Configuring the project; we apply a lock to the project to avoid concurrent changes. - project, is_new_project, err := configureProject(globals.Registry, req_user, request.Project, request.Prefix, &(globals.Locks)) - if err != nil { - return nil, fmt.Errorf("failed to process the project for '" + source + "'; %w", err) + if request.Project == nil { + return fmt.Errorf("expected a 'project' property in %q", reqpath) } + project := *(request.Project) project_dir := filepath.Join(globals.Registry, project) err = globals.Locks.LockPath(project_dir, 1000 * time.Second) if err != nil { - return nil, fmt.Errorf("failed to acquire the lock on %q; %w", project_dir, err) + return fmt.Errorf("failed to acquire the lock on %q; %w", project_dir, err) } defer globals.Locks.UnlockPath(project_dir) - if !is_new_project { - perms, err := readPermissions(project_dir) - if err != nil { - return nil, fmt.Errorf("failed to read permissions for %q; %w", project, err) - } - ok, trusted := isAuthorizedToUpload(req_user, globals.Administrators, perms, request.Asset, request.Version) - if !ok { - return nil, fmt.Errorf("user '" + req_user + "' is not authorized to upload to '" + project + "'") - } - if !trusted { - on_probation = true - } - } else { - err := populateNewProjectDirectory(project_dir, req_user, request.Permissions) - if err != nil { - return nil, fmt.Errorf("failed to populate project metadata for request %q; %w", reqpath, err) - } + perms, err := readPermissions(project_dir) + if err != nil { + return fmt.Errorf("failed to read permissions for %q; %w", project, err) + } + ok, trusted := isAuthorizedToUpload(req_user, globals.Administrators, perms, request.Asset, request.Version) + if !ok { + return fmt.Errorf("user '" + req_user + "' is not authorized to upload to '" + project + "'") + } + if !trusted { + on_probation = true } // Configuring the asset and version. - asset, is_new_asset, err := configureAsset(project_dir, request.Asset) + if request.Asset == nil { + return fmt.Errorf("expected an 'asset' property in %q", reqpath) + } + asset := *(request.Asset) + + err = configureAsset(project_dir, asset) if err != nil { - return nil, fmt.Errorf("failed to configure the asset for request %q; %w", reqpath, err) + return fmt.Errorf("failed to configure the asset for request %q; %w", reqpath, err) } asset_dir := filepath.Join(project_dir, asset) - version, err := configureVersion(asset_dir, is_new_asset, request.Version) + if request.Version == nil { + return fmt.Errorf("expected a 'version' property in %q", reqpath) + } + version := *(request.Version) + + err = configureVersion(asset_dir, version) if err != nil { - return nil, fmt.Errorf("failed to configure the version for request %q; %w", reqpath, err) + return fmt.Errorf("failed to configure the version for request %q; %w", reqpath, err) } // Now transferring all the files. This involves setting up an abort loop @@ -369,7 +152,7 @@ func uploadHandler(reqpath string, globals *globalConfiguration) (*uploadConfigu err = Transfer(source, globals.Registry, project, asset, version) if err != nil { - return nil, fmt.Errorf("failed to transfer files from %q; %w", source, err) + return fmt.Errorf("failed to transfer files from %q; %w", source, err) } // Writing out the various pieces of metadata. This should be, in theory, @@ -389,19 +172,19 @@ func uploadHandler(reqpath string, globals *globalConfiguration) (*uploadConfigu summary_path := filepath.Join(version_dir, summaryFileName) err := dumpJson(summary_path, &summary) if err != nil { - return nil, fmt.Errorf("failed to save summary for %q; %w", asset_dir, err) + return fmt.Errorf("failed to save summary for %q; %w", asset_dir, err) } } { extra, err := computeUsage(version_dir, true) if err != nil { - return nil, fmt.Errorf("failed to compute usage for the new version at %q; %w", version_dir, err) + return fmt.Errorf("failed to compute usage for the new version at %q; %w", version_dir, err) } usage, err := readUsage(project_dir) if err != nil { - return nil, fmt.Errorf("failed to read existing usage for project %q; %w", project, err) + return fmt.Errorf("failed to read existing usage for project %q; %w", project, err) } usage.Total += extra @@ -410,7 +193,7 @@ func uploadHandler(reqpath string, globals *globalConfiguration) (*uploadConfigu usage_path := filepath.Join(project_dir, usageFileName) err = dumpJson(usage_path, &usage) if err != nil { - return nil, fmt.Errorf("failed to save usage for %q; %w", project_dir, err) + return fmt.Errorf("failed to save usage for %q; %w", project_dir, err) } } @@ -423,7 +206,7 @@ func uploadHandler(reqpath string, globals *globalConfiguration) (*uploadConfigu latest_path := filepath.Join(asset_dir, latestFileName) err := dumpJson(latest_path, &latest) if err != nil { - return nil, fmt.Errorf("failed to save latest version for %q; %w", asset_dir, err) + return fmt.Errorf("failed to save latest version for %q; %w", asset_dir, err) } // Adding a log. @@ -436,10 +219,10 @@ func uploadHandler(reqpath string, globals *globalConfiguration) (*uploadConfigu } err = dumpLog(globals.Registry, log_info) if err != nil { - return nil, fmt.Errorf("failed to save log file; %w", err) + return fmt.Errorf("failed to save log file; %w", err) } } has_failed = false - return &uploadConfiguration{ Project: project, Version: version }, nil + return nil } diff --git a/upload_test.go b/upload_test.go index 15dc77c..e70d596 100644 --- a/upload_test.go +++ b/upload_test.go @@ -12,58 +12,6 @@ import ( "strings" ) -func TestIncrementSeries(t *testing.T) { - for _, prefix := range []string{ "V", "" } { - dir, err := os.MkdirTemp("", "") - if err != nil { - t.Fatalf("failed to create the temporary directory; %v", err) - } - - candidate, err := incrementSeries(prefix, dir) - if err != nil { - t.Fatalf("failed to initialize the series; %v", err) - } - if candidate != prefix + "1" { - t.Fatalf("initial value of the series should be 1, got %s", candidate) - } - - candidate, err = incrementSeries(prefix, dir) - if err != nil { - t.Fatalf("failed to update the series; %v", err) - } - if candidate != prefix + "2" { - t.Fatalf("next value of the series should be 2, not %s", candidate) - } - - // Works after conflict. - _, err = os.Create(filepath.Join(dir, prefix + "3")) - if err != nil { - t.Fatalf("failed to create a conflicting file") - } - candidate, err = incrementSeries(prefix, dir) - if err != nil { - t.Fatalf("failed to update the series after conflict; %v", err) - } - if candidate != prefix + "4" { - t.Fatal("next value of the series should be 4") - } - - // Injecting a different value. - series_path := incrementSeriesPath(prefix, dir) - err = os.WriteFile(series_path, []byte("100"), 0644) - if err != nil { - t.Fatalf("failed to overwrite the series file") - } - candidate, err = incrementSeries(prefix, dir) - if err != nil { - t.Fatalf("failed to update the series after overwrite; %v", err) - } - if candidate != prefix + "101" { - t.Fatal("next value of the series should be 101") - } - } -} - func setupSourceForUploadTest() (string, error) { src, err := os.MkdirTemp("", "") if err != nil { @@ -83,6 +31,20 @@ func setupSourceForUploadTest() (string, error) { return src, nil } +func setupProjectForUploadTest(project string, globals *globalConfiguration) error { + self, err := user.Current() + if err != nil { + return fmt.Errorf("failed to determine the current user; %w", err) + } + + err = createProject(project, nil, self.Username, globals) + if err != nil { + return err + } + + return nil +} + func TestUploadHandlerSimple(t *testing.T) { project := "original_series" asset := "gastly" @@ -99,26 +61,24 @@ func TestUploadHandlerSimple(t *testing.T) { t.Fatalf("failed to set up test directories; %v", err) } + err = setupProjectForUploadTest(project, &globals) + if err != nil { + t.Fatalf("failed to set up project directory; %v", err) + } + 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) } - // Executing the upload. - config, err := uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err != nil { t.Fatalf("failed to perform the upload; %v", err) } - if config.Project != project { - t.Fatalf("unexpected project name %q", config.Project) - } - if config.Version != version { - t.Fatalf("unexpected version name %q", config.Version) - } // Checking a few manifest entries and files. - destination := filepath.Join(reg, config.Project, asset, config.Version) + destination := filepath.Join(reg, project, asset, version) man, err := readManifest(destination) if err != nil { t.Fatalf("failed to read the manifest; %v", err) @@ -163,7 +123,7 @@ func TestUploadHandlerSimple(t *testing.T) { } // Checking out the usage. - project_dir := filepath.Join(reg, config.Project) + project_dir := filepath.Join(reg, project) used, err := readUsage(project_dir) if err != nil { t.Fatalf("failed to read the usage; %v", err) @@ -184,15 +144,15 @@ func TestUploadHandlerSimple(t *testing.T) { } // Checking out the latest version. - latest, err := readLatest(filepath.Join(reg, config.Project, asset)) + latest, err := readLatest(filepath.Join(reg, project, asset)) if err != nil { t.Fatalf("failed to read the latest; %v", err) } - if latest.Version != config.Version { - t.Fatalf("unexpected latest version (expected %q, got %q)", latest.Version, config.Version) + if latest.Version != version { + t.Fatalf("unexpected latest version (expected %q, got %q)", latest.Version, version) } - quota_raw, err := os.ReadFile(filepath.Join(reg, config.Project, "..quota")) + quota_raw, err := os.ReadFile(filepath.Join(reg, project, "..quota")) if err != nil { t.Fatalf("failed to read the quota; %v", err) } @@ -243,13 +203,18 @@ func TestUploadHandlerSimpleFailures(t *testing.T) { asset := "gastly" version := "lavender" + err := setupProjectForUploadTest(project, &globals) + if err != nil { + t.Fatalf("failed to set up project directory; %v", err) + } + req_string := fmt.Sprintf(`{ "project": "%s", "asset": "%s", "version": "%s" }`, 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) + err = uploadHandler(reqname, &globals) if err == nil || !strings.Contains(err.Error(), "expected a 'source'") { t.Fatalf("configuration should have failed without a source") } @@ -260,33 +225,21 @@ func TestUploadHandlerSimpleFailures(t *testing.T) { t.Fatalf("failed to create upload request; %v", err) } - _, err = uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err == nil || !strings.Contains(err.Error(), "same directory as") { t.Fatalf("configuration should have failed if the source is a path instead of a name") } } { - project := "FOO" - asset := "gastly" + project := "foobar" + asset := "..gastly" version := "lavender" - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s" }`, filepath.Base(src), project, asset, version) - reqname, err := dumpRequest("upload", req_string) + err := setupProjectForUploadTest(project, &globals) if err != nil { - t.Fatalf("failed to create upload request; %v", err) - } - - _, err = uploadHandler(reqname, &globals) - if err == nil || !strings.Contains(err.Error(), "uppercase") { - t.Fatal("configuration should fail for upper-cased project names") + t.Fatalf("failed to set up project directory; %v", err) } - } - - { - project := "..foo" - asset := "gastly" - version := "lavender" req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s" }`, filepath.Base(src), project, asset, version) reqname, err := dumpRequest("upload", req_string) @@ -294,150 +247,27 @@ func TestUploadHandlerSimpleFailures(t *testing.T) { t.Fatalf("failed to create upload request; %v", err) } - _, err = uploadHandler(reqname, &globals) - if err == nil || !strings.Contains(err.Error(), "invalid project name") { - t.Fatal("configuration should fail for invalid project name") - } - } - - { - project := "foobar" - asset := "..gastly" - version := "lavender" - - 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) + err = uploadHandler(reqname, &globals) if err == nil || !strings.Contains(err.Error(), "invalid asset name") { t.Fatal("configuration should fail for invalid asset name") } - } - { - project := "foobar" - asset := "gastly" - version := "..lavender" + asset = "gastly" + version = "..lavender" - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s" }`, filepath.Base(src), project, asset, version) - reqname, err := dumpRequest("upload", req_string) + 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) + err = uploadHandler(reqname, &globals) if err == nil || !strings.Contains(err.Error(), "invalid version name") { t.Fatal("configuration should fail for invalid version name") } } } -func TestUploadHandlerNewPermissions(t *testing.T) { - reg, err := constructMockRegistry() - if err != nil { - t.Fatalf("failed to create the registry; %v", err) - } - globals := newGlobalConfiguration(reg) - - src, err := setupSourceForUploadTest() - if err != nil { - t.Fatalf("failed to set up test directories; %v", err) - } - - asset := "gastly" - version := "lavender" - - // Checking that owners are respected. - { - project := "indigo_league" - perm_string := `{ "owners": [ "YAY", "NAY" ] }` - - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s", "permissions": %s }`, filepath.Base(src), project, asset, version, perm_string) - reqname, err := dumpRequest("upload", req_string) - if err != nil { - t.Fatalf("failed to create upload request; %v", err) - } - - config, err := uploadHandler(reqname, &globals) - if err != nil { - t.Fatalf("failed to perform the upload; %v", err) - } - - perms, err := readPermissions(filepath.Join(reg, config.Project)) - if err != nil { - t.Fatalf("failed to read the permissions; %v", err) - } - if len(perms.Owners) != 2 || perms.Owners[0] != "YAY" || perms.Owners[1] != "NAY" { - t.Fatal("failed to set the owners correctly in the project permissions") - } - if len(perms.Uploaders) != 0 { - t.Fatal("failed to set the uploaders correctly in the project permissions") - } - } - - { - project := "johto_journeys" - new_id := "foo" - perm_string := fmt.Sprintf(`{ "uploaders": [ { "id": "%s" } ] }`, new_id) - - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s", "permissions": %s }`, filepath.Base(src), project, asset, version, perm_string) - reqname, err := dumpRequest("upload", req_string) - if err != nil { - t.Fatalf("failed to create upload request; %v", err) - } - - config, err := uploadHandler(reqname, &globals) - if err != nil { - t.Fatalf("failed to perform the upload; %v", err) - } - - perms, err := readPermissions(filepath.Join(reg, config.Project)) - if err != nil { - t.Fatalf("failed to read the permissions; %v", err) - } - if len(perms.Owners) != 1 { // switches to the uploading user. - t.Fatal("failed to set the owners correctly in the project permissions") - } - if len(perms.Uploaders) != 1 || perms.Uploaders[0].Id != new_id { - t.Fatal("failed to set the uploaders correctly in the project permissions") - } - } - - // Check that uploaders in the permissions are validated. - { - project := "battle_frontier" - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s", "permissions": { "uploaders": [{}] } }`, 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(), "invalid 'permissions.uploaders'") { - t.Fatalf("expected upload to fail from invalid 'uploaders'") - } - } - - { - project := "sinnoh_league" - perm_string := `{ "uploaders": [ { "id": "argle", "until": "bargle" } ] }` - - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s", "permissions": %s }`, filepath.Base(src), project, asset, version, perm_string) - 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(), "invalid 'permissions.uploaders'") { - t.Fatalf("expected upload to fail from invalid 'uploaders'") - } - } -} - func TestUploadHandlerSimpleUpdate(t *testing.T) { project := "original_series" asset := "gastly" @@ -454,6 +284,11 @@ func TestUploadHandlerSimpleUpdate(t *testing.T) { t.Fatalf("failed to set up test directories; %v", err) } + err = setupProjectForUploadTest(project, &globals) + if err != nil { + t.Fatalf("failed to set up project directory; %v", err) + } + // Uploading the first version. old_usage := int64(0) { @@ -463,12 +298,12 @@ func TestUploadHandlerSimpleUpdate(t *testing.T) { t.Fatalf("failed to create upload request; %v", err) } - config, err := uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err != nil { t.Fatalf("failed to perform the upload; %v", err) } - used, err := readUsage(filepath.Join(reg, config.Project)) + used, err := readUsage(filepath.Join(reg, project)) if err != nil { t.Fatalf("failed to read the usage; %v", err) } @@ -490,15 +325,12 @@ func TestUploadHandlerSimpleUpdate(t *testing.T) { t.Fatalf("failed to update the 'evolution' file; %v", err) } - config, err := uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err != nil { t.Fatalf("failed to perform the upload; %v", err) } - if config.Version != version { - t.Fatalf("unexpected version name %q", config.Version) - } - destination := filepath.Join(reg, config.Project, asset, config.Version) + destination := filepath.Join(reg, project, asset, version) man, err := readManifest(destination) if err != nil { t.Fatalf("failed to read the manifest; %v", err) @@ -517,7 +349,7 @@ func TestUploadHandlerSimpleUpdate(t *testing.T) { } // Ensuring that the usage accumulates. - project_dir := filepath.Join(reg, config.Project) + project_dir := filepath.Join(reg, project) usage, err := readUsage(project_dir) if err != nil { t.Fatalf("failed to read the usage; %v", err) @@ -539,12 +371,12 @@ func TestUploadHandlerSimpleUpdate(t *testing.T) { } // Confirming that we updated to the latest version. - latest, err := readLatest(filepath.Join(reg, config.Project, asset)) + latest, err := readLatest(filepath.Join(reg, project, asset)) if err != nil { t.Fatalf("failed to read the latest; %v", err) } - if latest.Version != config.Version { - t.Fatalf("unexpected latest version (expected %q, got %q)", config.Version, latest.Version) + if latest.Version != version { + t.Fatalf("unexpected latest version (expected %q, got %q)", version, latest.Version) } } } @@ -561,8 +393,13 @@ func TestUploadHandlerSimpleUpdateFailures(t *testing.T) { t.Fatalf("failed to set up test directories; %v", err) } - // Ceating the first version. project := "aaron" + err = setupProjectForUploadTest(project, &globals) + if err != nil { + t.Fatalf("failed to set up project directory; %v", err) + } + + // Creating the first version. asset := "BAR" version := "whee" @@ -572,7 +409,7 @@ func TestUploadHandlerSimpleUpdateFailures(t *testing.T) { t.Fatalf("failed to create upload request; %v", err) } - _, err = uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err != nil { t.Fatalf("failed complete configuration; %v", err) } @@ -581,7 +418,7 @@ func TestUploadHandlerSimpleUpdateFailures(t *testing.T) { } // Trying with an existing version. - _, err = uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err == nil || !strings.Contains(err.Error(), "already exists") { t.Fatal("configuration should fail for an existing version") } @@ -593,108 +430,27 @@ func TestUploadHandlerSimpleUpdateFailures(t *testing.T) { t.Fatalf("failed to create upload request; %v", err) } - _, err = uploadHandler(reqname, &globals) - if err == nil || !strings.Contains(err.Error(), "initialized without a version series") { - t.Fatal("configuration should fail for missing version in a non-series asset") - } -} - -func TestUploadHandlerUpdatePermissions(t *testing.T) { - reg, err := constructMockRegistry() - if err != nil { - t.Fatalf("failed to create the registry; %v", err) - } - globals := newGlobalConfiguration(reg) - - src, err := setupSourceForUploadTest() - if err != nil { - t.Fatalf("failed to set up test directories; %v", err) - } - - // First creating the first version. - project := "aaron" - asset := "BAR" - version := "whee" - - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s", "permissions": { "owners": [] } }`, 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 complete configuration; %v", err) - } - if _, err := os.Stat(filepath.Join(reg, project, asset, version)); err != nil { - t.Fatalf("expected creation of the target version directory") - } - - // Now attempting to create a new version. - req_string = fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "stuff" }`, filepath.Base(src), project, asset) - 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.Fatalf("failed to reject upload from non-authorized user") + err = uploadHandler(reqname, &globals) + if err == nil || !strings.Contains(err.Error(), "expected a 'version'") { + t.Fatal("configuration should fail for missing version") } } -func TestUploadHandlerProjectSeries(t *testing.T) { - reg, err := constructMockRegistry() - if err != nil { - t.Fatalf("failed to create the registry; %v", err) - } - globals := newGlobalConfiguration(reg) - - src, err := setupSourceForUploadTest() - if err != nil { - t.Fatalf("failed to set up test directories; %v", err) - } - - prefix := "FOO" - asset := "gastly" - version := "v1" - - req_string := fmt.Sprintf(`{ "source": "%s", "prefix": "%s", "asset": "%s", "version": "%s" }`, filepath.Base(src), prefix, asset, version) - reqname, err := dumpRequest("upload", req_string) +func setupProjectForUploadTestWithPermissions(project string, owners []string, uploaders []unsafeUploaderEntry, globals *globalConfiguration) error { + self, err := user.Current() if err != nil { - t.Fatalf("failed to create upload request; %v", err) + return fmt.Errorf("failed to determine the current user; %w", err) } - config, err := uploadHandler(reqname, &globals) + err = createProject(project, &unsafePermissionsMetadata{ Owners: owners, Uploaders: uploaders }, self.Username, globals) if err != nil { - t.Fatalf("failed complete configuration; %v", err) - } - if config.Project != "FOO1" { - t.Fatalf("unexpected value for the project name (%s)", config.Project) - } - - // Check that everything was created. - if _, err := os.Stat(filepath.Join(reg, config.Project, "..permissions")); err != nil { - t.Fatalf("permissions file was not created") - } - if _, err := os.Stat(filepath.Join(reg, config.Project, "..usage")); err != nil { - t.Fatalf("usage file was not created") - } - if _, err := os.Stat(filepath.Join(reg, config.Project, "..quota")); err != nil { - t.Fatalf("quota file was not created") + return err } - // Trying again. - config, err = uploadHandler(reqname, &globals) - if err != nil { - t.Fatalf("failed complete configuration; %v", err) - } - if config.Project != "FOO2" { - t.Fatalf("unexpected value for the project name (%s)", config.Project) - } + return nil } -func TestUploadHandlerProjectSeriesFailures(t *testing.T) { +func TestUploadHandlerUpdatePermissions(t *testing.T) { reg, err := constructMockRegistry() if err != nil { t.Fatalf("failed to create the registry; %v", err) @@ -706,102 +462,29 @@ func TestUploadHandlerProjectSeriesFailures(t *testing.T) { t.Fatalf("failed to set up test directories; %v", err) } - { - asset := "gastly" - version := "v1" - - req_string := fmt.Sprintf(`{ "source": "%s", "asset": "%s", "version": "%s" }`, filepath.Base(src), 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(), "expected a 'prefix'") { - t.Fatalf("configuration should have failed without a prefix") - } - } - - { - prefix := "foo" - asset := "gastly" - version := "v1" - - req_string := fmt.Sprintf(`{ "source": "%s", "prefix": "%s", "asset": "%s", "version": "%s" }`, filepath.Base(src), prefix, 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(), "uppercase") { - t.Fatalf("configuration should have failed with non-uppercase prefix") - } - } -} - -func TestUploadHandlerVersionSeries(t *testing.T) { - reg, err := constructMockRegistry() - if err != nil { - t.Fatalf("failed to create the registry; %v", err) - } - globals := newGlobalConfiguration(reg) - - src, err := setupSourceForUploadTest() + // Making a project with explicitly no permissions. + project := "aaron" + err = setupProjectForUploadTestWithPermissions(project, []string{}, nil, &globals) if err != nil { - t.Fatalf("failed to set up test directories; %v", err) + t.Fatalf("failed to create the project; %v", err) } - // First creating the first version. - project := "aaron" + // Now making the request. asset := "BAR" - - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s" }`, filepath.Base(src), project, asset) + 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) } - config, err := uploadHandler(reqname, &globals) - if err != nil { - t.Fatalf("failed complete configuration; %v", err) - } - if config.Version != "1" { - t.Fatalf("expected version series to start at 1"); - } - if _, err := os.Stat(filepath.Join(reg, project, asset, config.Version)); err != nil { - t.Fatalf("expected creation of the first version directory") - } - - // Trying again. - config, err = uploadHandler(reqname, &globals) - if err != nil { - t.Fatalf("failed complete configuration; %v", err) - } - if config.Version != "2" { - t.Fatalf("expected version series to continue to 2"); - } - if _, err := os.Stat(filepath.Join(reg, project, asset, config.Version)); err != nil { - t.Fatalf("expected creation of the second version directory") - } - - // Trying with a version. - req_string = fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "FOO" }`, filepath.Base(src), project, asset) - 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(), "initialized with a version series") { - t.Fatal("configuration should fail for specified version in an asset with seriesc") + err = uploadHandler(reqname, &globals) + if err == nil || !strings.Contains(err.Error(), "not authorized") { + t.Fatalf("failed to reject upload from non-authorized user") } } func TestUploadHandlerNewOnProbation(t *testing.T) { - prefix := "POKEDEX" - asset := "Gastly" - reg, err := constructMockRegistry() if err != nil { t.Fatalf("failed to create the registry; %v", err) @@ -813,25 +496,28 @@ func TestUploadHandlerNewOnProbation(t *testing.T) { t.Fatalf("failed to set up test directories; %v", err) } - req_string := fmt.Sprintf(`{ "source": "%s", "prefix": "%s", "asset": "%s", "version": "FOO", "on_probation": true }`, filepath.Base(src), prefix, asset) + project := "setsuna" + asset := "yuki" + version := "nakagawa" + + err = setupProjectForUploadTest(project, &globals) + if err != nil { + t.Fatalf("failed to set up project directory; %v", err) + } + + req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s", "on_probation": true }`, filepath.Base(src), project, asset, version) reqname, err := dumpRequest("upload", req_string) if err != nil { t.Fatalf("failed to create upload request; %v", err) } - config, err := uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err != nil { t.Fatalf("failed to perform the upload; %v", err) } - if config.Project != "POKEDEX1" { - t.Fatalf("unexpected project name %q", config.Project) - } - if config.Version != "FOO" { - t.Fatalf("unexpected version name %q", config.Version) - } // Summary file states that it's on probation. - summ, err := readSummary(filepath.Join(reg, config.Project, asset, config.Version)) + summ, err := readSummary(filepath.Join(reg, project, asset, version)) if err != nil { t.Fatalf("failed to read the summary; %v", err) } @@ -840,7 +526,7 @@ func TestUploadHandlerNewOnProbation(t *testing.T) { } // No latest file should be created for probational projects. - _, err = readLatest(filepath.Join(reg, config.Project, asset)) + _, err = readLatest(filepath.Join(reg, project, asset)) if err == nil || !errors.Is(err, os.ErrNotExist) { t.Fatal("no ..latest file should be created on probation") } @@ -876,36 +562,25 @@ func TestUploadHandlerUpdateOnProbation(t *testing.T) { // Uploaders are not trusted by default. { project := "ghost" - asset := "gastly" - perm_string := fmt.Sprintf(`{ "owners": [], "uploaders": [ { "id": "%s" } ] }`, self_name) - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "permissions": %s }`, filepath.Base(src), project, asset, perm_string) - reqname, err := dumpRequest("upload", req_string) + err := setupProjectForUploadTestWithPermissions(project, []string{}, []unsafeUploaderEntry{ unsafeUploaderEntry{ Id: &self_name } }, &globals) if err != nil { - t.Fatalf("failed to create upload request; %v", err) + t.Fatalf("failed to create the project; %v", err) } - // First upload to set up the project. - config, err := uploadHandler(reqname, &globals) - if err != nil { - t.Fatalf("failed to perform the upload; %v", err) - } - - summ, err := readSummary(filepath.Join(reg, project, asset, config.Version)) + asset := "gastly" + version := "shadow_ball" + 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 read the summary file; %v", err) - } - - if summ.OnProbation != nil { - t.Fatal("expected no 'on_probation' entry to be present") + t.Fatalf("failed to create upload request; %v", err) } - // Second upload using the previous permissions. - config, err = uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err != nil { t.Fatalf("failed to perform the upload; %v", err) } - summ, err = readSummary(filepath.Join(reg, project, asset, config.Version)) + summ, err := readSummary(filepath.Join(reg, project, asset, version)) if err != nil { t.Fatalf("failed to read the summary file; %v", err) } @@ -918,27 +593,26 @@ func TestUploadHandlerUpdateOnProbation(t *testing.T) { // Checking that trusted uploaders do not get probation. { project := "pokemon_adventures" // changing the project name to get a new project. - asset := "gastly" - perm_string := fmt.Sprintf(`{ "owners": [], "uploaders": [ { "id": "%s", "trusted": true } ] }`, self_name) - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "permissions": %s }`, filepath.Base(src), project, asset, perm_string) - reqname, err := dumpRequest("upload", req_string) + trusted := true + err := setupProjectForUploadTestWithPermissions(project, []string{}, []unsafeUploaderEntry{ unsafeUploaderEntry{ Id: &self_name, Trusted: &trusted } }, &globals) if err != nil { - t.Fatalf("failed to create upload request; %v", err) + t.Fatalf("failed to create the project; %v", err) } - // First upload. - _, err = uploadHandler(reqname, &globals) + asset := "gastly" + version := "dream_eater" + 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 perform the upload; %v", err) + t.Fatalf("failed to create upload request; %v", err) } - // Second upload. - config, err := uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err != nil { t.Fatalf("failed to perform the upload; %v", err) } - summ, err := readSummary(filepath.Join(reg, project, asset, config.Version)) + summ, err := readSummary(filepath.Join(reg, project, asset, version)) if err != nil { t.Fatalf("failed to read the summary file; %v", err) } @@ -948,18 +622,19 @@ func TestUploadHandlerUpdateOnProbation(t *testing.T) { } // ... unless they specifically ask for it. - req_string = fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "on_probation": true }`, filepath.Base(src), project, asset) + version = "hypnosis" + req_string = fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "version": "%s", "on_probation": true }`, filepath.Base(src), project, asset, version) reqname, err = dumpRequest("upload", req_string) if err != nil { t.Fatalf("failed to create upload request; %v", err) } - config, err = uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err != nil { t.Fatalf("failed to perform the upload; %v", err) } - summ, err = readSummary(filepath.Join(reg, project, asset, config.Version)) + summ, err = readSummary(filepath.Join(reg, project, asset, version)) if err != nil { t.Fatalf("failed to read the summary file; %v", err) } @@ -972,27 +647,25 @@ func TestUploadHandlerUpdateOnProbation(t *testing.T) { // Owners are free from probation. { project := "ss_anne" // changing project name again. - asset := "gastly" - perm_string := fmt.Sprintf(`{ "owners": [ "%s" ] }`, self_name) - req_string := fmt.Sprintf(`{ "source": "%s", "project": "%s", "asset": "%s", "permissions": %s }`, filepath.Base(src), project, asset, perm_string) - reqname, err := dumpRequest("upload", req_string) + err := setupProjectForUploadTestWithPermissions(project, []string{ self_name }, nil, &globals) if err != nil { - t.Fatalf("failed to create upload request; %v", err) + t.Fatalf("failed to create the project; %v", err) } - // First upload to set up the project. - _, err = uploadHandler(reqname, &globals) + asset := "gastly" + version := "confuse_ray" + 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 perform the upload; %v", err) + t.Fatalf("failed to create upload request; %v", err) } - // Second upload. - config, err := uploadHandler(reqname, &globals) + err = uploadHandler(reqname, &globals) if err != nil { t.Fatalf("failed to perform the upload; %v", err) } - summ, err := readSummary(filepath.Join(reg, project, asset, config.Version)) + summ, err := readSummary(filepath.Join(reg, project, asset, version)) if err != nil { t.Fatalf("failed to read the summary file; %v", err) }