Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement asset-level permissions with global writes. #19

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`.
Expand Down
124 changes: 80 additions & 44 deletions permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,45 @@ 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)
}

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 {
Expand Down Expand Up @@ -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))

Expand All @@ -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
Expand All @@ -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"`
}{}
{
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading