Skip to content

Commit bc2e2a0

Browse files
authored
Use asset-level permissions to implement global_write. (#19)
This introduces an asset-level ..permissions file containing uploaders that are appended to the project-level permissions during an upload request. It reduces the size of the project-level ..permissions when global_write is enabled, as we don't accumulate everyone's asset permissions into the same file. This should improve scalability for projects that have lots of different uploaders.
1 parent c8b3683 commit bc2e2a0

File tree

5 files changed

+309
-163
lines changed

5 files changed

+309
-163
lines changed

README.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,14 @@ This is a JSON-formatted file that contains a JSON object with the following pro
112112
If not specified, the uploader is untrusted by default.
113113
- `global_write` (optional): a boolean indicating whether "global writes" are enabled.
114114
With global writes enabled, any user of the filesystem can create a new asset within this project.
115-
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`.
115+
Once the asset is created, its creating user is added as a trusted uploader to the `{project}/{asset}/..permissions` file (see below).
116116
If not specified, global writes are disabled by default.
117117

118+
Additional uploader permissions for a specific asset can be specified in a `{project}/{asset}/..permissions` file.
119+
This should be a JSON-formatted file that contains a JSON object with the `uploaders` property as described above.
120+
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.
121+
During [upload requests](#uploads-and-updates), any `uploaders` in this file will be appended to the `uploaders` in `{project}/..permissions` before authorization checks.
122+
118123
User identities are defined by the UIDs on the operating system.
119124
All users are authenticated by examining the ownership of files provided to the Gobbler.
120125
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
230235
Users should create a file with the `request-set_permissions-` prefix, which should be JSON-formatted with the following properties:
231236

232237
- `project`: string containing the name of the project.
233-
- `permissions`: an object containing either or both of `owners` and `uploaders`.
238+
- `asset` (optional): string containing the name of an asset.
239+
If provided, asset-level uploader permissions will be modified instead of project-level permissions.
240+
- `permissions`: an object containing zero, one or more of `owners`, `uploaders` and `global_write`.
234241
Each of these properties has the same type as described [above](#permissions).
235242
If any property is missing, the value in the existing permissions is used.
243+
If `asset` is provided, only `uploaders` will be used.
236244

237245
On success, the permissions in the registry are modified.
238246
The HTTP response will contain a JSON object with the `status` property set to `SUCCESS`.

permissions.go

+80-44
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,45 @@ func identifyUser(path string) (string, error) {
4848
}
4949

5050
func readPermissions(path string) (*permissionsMetadata, error) {
51-
handle, err := os.ReadFile(filepath.Join(path, permissionsFileName))
51+
contents, err := os.ReadFile(filepath.Join(path, permissionsFileName))
5252
if err != nil {
5353
return nil, fmt.Errorf("failed to read %q; %w", path, err)
5454
}
5555

5656
var output permissionsMetadata
57-
err = json.Unmarshal(handle, &output)
57+
err = json.Unmarshal(contents, &output)
5858
if err != nil {
5959
return nil, fmt.Errorf("failed to parse JSON from %q; %w", path, err)
6060
}
6161

6262
return &output, nil
6363
}
6464

65+
func addAssetPermissions(existing *permissionsMetadata, asset_dir, asset string) error {
66+
path := filepath.Join(asset_dir, permissionsFileName)
67+
contents, err := os.ReadFile(path)
68+
69+
if err != nil {
70+
if errors.Is(err, os.ErrNotExist) {
71+
return nil
72+
} else {
73+
return fmt.Errorf("failed to read %q; %w", path, err)
74+
}
75+
}
76+
77+
var loaded permissionsMetadata
78+
err = json.Unmarshal(contents, &loaded)
79+
if err != nil {
80+
return fmt.Errorf("failed to parse JSON from %q; %w", path, err)
81+
}
82+
83+
for _, up := range loaded.Uploaders {
84+
up.Asset = &asset
85+
existing.Uploaders = append(existing.Uploaders, up)
86+
}
87+
return nil
88+
}
89+
6590
func isAuthorizedToAdmin(username string, administrators []string) bool {
6691
if administrators != nil {
6792
for _, s := range administrators {
@@ -124,31 +149,6 @@ func isAuthorizedToUpload(username string, administrators []string, permissions
124149
return false, false
125150
}
126151

127-
func prepareGlobalWriteNewAsset(username string, permissions *permissionsMetadata, asset string, project_dir string) (bool, error) {
128-
if permissions.GlobalWrite == nil || !*(permissions.GlobalWrite) {
129-
return false, nil
130-
}
131-
132-
asset_dir := filepath.Join(project_dir, asset)
133-
_, err := os.Stat(asset_dir)
134-
135-
if err == nil || !errors.Is(err, os.ErrNotExist) {
136-
return false, nil
137-
}
138-
139-
// Updating the permissions in memory and on disk.
140-
is_trusted := true
141-
permissions.Uploaders = append(permissions.Uploaders, uploaderEntry{ Id: username, Asset: &asset, Trusted: &is_trusted })
142-
143-
perm_path := filepath.Join(project_dir, permissionsFileName)
144-
err = dumpJson(perm_path, permissions)
145-
if err != nil {
146-
return false, err
147-
}
148-
149-
return true, nil
150-
}
151-
152152
func sanitizeUploaders(uploaders []unsafeUploaderEntry) ([]uploaderEntry, error) {
153153
output := make([]uploaderEntry, len(uploaders))
154154

@@ -168,7 +168,7 @@ func sanitizeUploaders(uploaders []unsafeUploaderEntry) ([]uploaderEntry, error)
168168
output[i].Asset = u.Asset
169169
output[i].Version = u.Version
170170
output[i].Until = u.Until
171-
output[i]. Trusted = u.Trusted
171+
output[i].Trusted = u.Trusted
172172
}
173173

174174
return output, nil
@@ -191,6 +191,7 @@ type unsafePermissionsMetadata struct {
191191
func setPermissionsHandler(reqpath string, globals *globalConfiguration) error {
192192
incoming := struct {
193193
Project *string `json:"project"`
194+
Asset *string `json:"asset"`
194195
Permissions *unsafePermissionsMetadata `json:"permissions"`
195196
}{}
196197
{
@@ -206,7 +207,14 @@ func setPermissionsHandler(reqpath string, globals *globalConfiguration) error {
206207

207208
err = isMissingOrBadName(incoming.Project)
208209
if err != nil {
209-
return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'project' property in %q; %w", reqpath, err))
210+
return newHttpError(http.StatusBadRequest, fmt.Errorf("missing or invalid 'project' property in %q; %w", reqpath, err))
211+
}
212+
213+
if incoming.Asset != nil {
214+
err := isBadName(*(incoming.Asset))
215+
if err != nil {
216+
return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'asset' property in %q; %w", reqpath, err))
217+
}
210218
}
211219

212220
if incoming.Permissions == nil {
@@ -240,24 +248,52 @@ func setPermissionsHandler(reqpath string, globals *globalConfiguration) error {
240248
return newHttpError(http.StatusForbidden, fmt.Errorf("user %q is not authorized to modify permissions for %q", source_user, project))
241249
}
242250

243-
if incoming.Permissions.Owners != nil {
244-
existing.Owners = incoming.Permissions.Owners
245-
}
246-
if incoming.Permissions.Uploaders != nil {
247-
san, err := sanitizeUploaders(incoming.Permissions.Uploaders)
251+
if incoming.Asset == nil {
252+
if incoming.Permissions.Owners != nil {
253+
existing.Owners = incoming.Permissions.Owners
254+
}
255+
if incoming.Permissions.Uploaders != nil {
256+
san, err := sanitizeUploaders(incoming.Permissions.Uploaders)
257+
if err != nil {
258+
return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'permissions.uploaders' in request; %w", err))
259+
}
260+
existing.Uploaders = san
261+
}
262+
if incoming.Permissions.GlobalWrite != nil {
263+
existing.GlobalWrite = incoming.Permissions.GlobalWrite
264+
}
265+
266+
perm_path := filepath.Join(project_dir, permissionsFileName)
267+
err = dumpJson(perm_path, existing)
248268
if err != nil {
249-
return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'permissions.uploaders' in request; %w", err))
269+
return fmt.Errorf("failed to write permissions for %q; %w", project, err)
250270
}
251-
existing.Uploaders = san
252-
}
253-
if incoming.Permissions.GlobalWrite != nil {
254-
existing.GlobalWrite = incoming.Permissions.GlobalWrite
255-
}
256271

257-
perm_path := filepath.Join(project_dir, permissionsFileName)
258-
err = dumpJson(perm_path, existing)
259-
if err != nil {
260-
return fmt.Errorf("failed to write permissions for %q; %w", project, err)
272+
} else {
273+
asset_dir := filepath.Join(project_dir, *(incoming.Asset))
274+
if _, err := os.Stat(asset_dir); errors.Is(err, os.ErrNotExist) {
275+
err = os.Mkdir(asset_dir, 0755)
276+
if err != nil {
277+
return fmt.Errorf("failed to create new asset directory at %q; %w", asset_dir, err)
278+
}
279+
}
280+
281+
if incoming.Permissions.Uploaders != nil {
282+
san, err := sanitizeUploaders(incoming.Permissions.Uploaders)
283+
if err != nil {
284+
return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'permissions.uploaders' in request; %w", err))
285+
}
286+
for i, _ := range san {
287+
san[i].Asset = nil
288+
}
289+
290+
aperms := &permissionsMetadata{ Uploaders: san }
291+
perm_path := filepath.Join(asset_dir, permissionsFileName)
292+
err = dumpJson(perm_path, aperms)
293+
if err != nil {
294+
return fmt.Errorf("failed to write asset-level permissions for %q; %w", asset_dir, err)
295+
}
296+
}
261297
}
262298

263299
return nil

0 commit comments

Comments
 (0)