-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
6 changed files
with
514 additions
and
754 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.