tenseleyflow/shithub / 0da6e59

Browse files

S26: policy.ActionWatchSet + permissions.md table entry

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
0da6e599da4bb19f7b2b956217f3c8f3feb9ef97
Parents
8d3abff
Tree
3eb3517

4 changed files

StatusFile+-
M docs/internal/permissions.md 1 0
M internal/auth/policy/actions.go 8 0
M internal/auth/policy/policy.go 15 4
M internal/auth/policy/policy_test.go 3 2
docs/internal/permissions.mdmodified
@@ -68,6 +68,7 @@ The complete map (also enforced by the matrix test):
68
 | `pull:close`                          | `write`          |
68
 | `pull:close`                          | `write`          |
69
 | `star:create`                         | logged in        |
69
 | `star:create`                         | logged in        |
70
 | `fork:create`                         | logged in        |
70
 | `fork:create`                         | logged in        |
71
+| `watch:set`                           | logged in        |
71
 
72
 
72
 Read actions on **public** repos are short-circuited to allow before the
73
 Read actions on **public** repos are short-circuited to allow before the
73
 role check — anyone (anonymous or otherwise) can read a public repo.
74
 role check — anyone (anonymous or otherwise) can read a public repo.
internal/auth/policy/actions.gomodified
@@ -62,6 +62,13 @@ const (
62
 const (
62
 const (
63
 	ActionStarCreate Action = "star:create"
63
 	ActionStarCreate Action = "star:create"
64
 	ActionForkCreate Action = "fork:create"
64
 	ActionForkCreate Action = "fork:create"
65
+
66
+	// S26 social actions. WatchSet covers both setting an explicit
67
+	// level and unsetting (deleting the row). Both require a logged-in
68
+	// user with read access to the repo — the policy.Can engine
69
+	// enforces visibility before reaching the role check, so a
70
+	// non-collab on a private repo deny-leaks as 404.
71
+	ActionWatchSet Action = "watch:set"
65
 )
72
 )
66
 
73
 
67
 // AllActions is the canonical list. The matrix test iterates this so a
74
 // AllActions is the canonical list. The matrix test iterates this so a
@@ -74,6 +81,7 @@ var AllActions = []Action{
74
 	ActionIssueRead, ActionIssueCreate, ActionIssueComment, ActionIssueClose, ActionIssueLabel, ActionIssueAssign,
81
 	ActionIssueRead, ActionIssueCreate, ActionIssueComment, ActionIssueClose, ActionIssueLabel, ActionIssueAssign,
75
 	ActionPullRead, ActionPullCreate, ActionPullMerge, ActionPullReview, ActionPullClose,
82
 	ActionPullRead, ActionPullCreate, ActionPullMerge, ActionPullReview, ActionPullClose,
76
 	ActionStarCreate, ActionForkCreate,
83
 	ActionStarCreate, ActionForkCreate,
84
+	ActionWatchSet,
77
 }
85
 }
78
 
86
 
79
 // isWriteAction returns true when the action mutates state. Used by the
87
 // isWriteAction returns true when the action mutates state. Used by the
internal/auth/policy/policy.gomodified
@@ -137,9 +137,13 @@ func Can(ctx context.Context, d Deps, actor Actor, action Action, repo RepoRef)
137
 		return deny(DenyRoleTooLow, "role too low")
137
 		return deny(DenyRoleTooLow, "role too low")
138
 	}
138
 	}
139
 
139
 
140
-	// 9. Login-required actions: star/fork need any logged-in user.
140
+	// 9. Login-required actions: star/fork/watch-set need any
141
-	if (action == ActionStarCreate || action == ActionForkCreate) && actor.IsAnonymous {
141
+	//    logged-in user. Anonymous reaches here only on a public repo
142
-		return deny(DenyAnonymous, "anonymous cannot star/fork")
142
+	//    (see step 4); we deny with the anonymous code so the handler
143
+	//    can render a friendly "log in to star" prompt.
144
+	if (action == ActionStarCreate || action == ActionForkCreate || action == ActionWatchSet) &&
145
+		actor.IsAnonymous {
146
+		return deny(DenyAnonymous, "anonymous cannot star/fork/watch")
143
 	}
147
 	}
144
 
148
 
145
 	return allow("granted")
149
 	return allow("granted")
@@ -251,7 +255,14 @@ func minRoleFor(action Action) Role {
251
 		return RoleAdmin
255
 		return RoleAdmin
252
 
256
 
253
 	// Login-required but no role required (any logged-in user).
257
 	// Login-required but no role required (any logged-in user).
254
-	case ActionStarCreate, ActionForkCreate:
258
+	// Star/fork: any logged-in user can star or fork any repo they
259
+	// can read — the visibility short-circuit above already gates
260
+	// private-repo access, and the role check below short-circuits
261
+	// to allow when minRole is RoleNone.
262
+	// WatchSet: same shape — any logged-in user with read access
263
+	// can choose their watch level. The downstream notifications
264
+	// fan-out (S29) is what enforces level-based delivery.
265
+	case ActionStarCreate, ActionForkCreate, ActionWatchSet:
255
 		return RoleNone
266
 		return RoleNone
256
 
267
 
257
 	default:
268
 	default:
internal/auth/policy/policy_test.gomodified
@@ -183,7 +183,8 @@ func expect(actor actorKind, repo repoKind, action policy.Action) bool {
183
 	}
183
 	}
184
 
184
 
185
 	// Login-only social actions.
185
 	// Login-only social actions.
186
-	if action == policy.ActionStarCreate || action == policy.ActionForkCreate {
186
+	if action == policy.ActionStarCreate || action == policy.ActionForkCreate ||
187
+		action == policy.ActionWatchSet {
187
 		return have != policy.RoleNone || (actor != actorAnonymous)
188
 		return have != policy.RoleNone || (actor != actorAnonymous)
188
 	}
189
 	}
189
 
190
 
@@ -213,7 +214,7 @@ func mirrorMinRoleFor(a policy.Action) policy.Role {
213
 		policy.ActionRepoArchive, policy.ActionRepoDelete, policy.ActionRepoTransfer, policy.ActionRepoVisibility,
214
 		policy.ActionRepoArchive, policy.ActionRepoDelete, policy.ActionRepoTransfer, policy.ActionRepoVisibility,
214
 		policy.ActionPullMerge:
215
 		policy.ActionPullMerge:
215
 		return policy.RoleAdmin
216
 		return policy.RoleAdmin
216
-	case policy.ActionStarCreate, policy.ActionForkCreate:
217
+	case policy.ActionStarCreate, policy.ActionForkCreate, policy.ActionWatchSet:
217
 		return policy.RoleNone
218
 		return policy.RoleNone
218
 	}
219
 	}
219
 	return policy.RoleAdmin
220
 	return policy.RoleAdmin