fix(authz): allow project members stage chat without document duties
Live workflows still restrict document actions to duty holders, but AllowedStageKeys now include active/completed stages so members can chat. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -62,25 +62,20 @@ func (s *Service) ResolveStageAccess(ctx context.Context, projectID int64, userI
|
||||
continue
|
||||
}
|
||||
status := stageStatus[stPlan.StageKey]
|
||||
hasDuty := stageDutyForUser(stPlan, userID)
|
||||
hasUnmanaged := stageHasUnmanagedDoc(stPlan)
|
||||
// View exemption: every enabled doc is listable. Action keys stay duty/creator-none.
|
||||
for _, dt := range stPlan.DocTasks {
|
||||
if dt.Enabled {
|
||||
out.ViewableTaskKeys[dt.TaskKey] = true
|
||||
}
|
||||
}
|
||||
allowStage := false
|
||||
// Stage chat/history: any caller who already passed project membership may
|
||||
// see active/completed stages. Document actions remain duty-scoped below.
|
||||
switch status {
|
||||
case StageActive, StageCompleted:
|
||||
allowStage = hasDuty || (isCreator && hasUnmanaged)
|
||||
out.AllowedStageKeys[stPlan.StageKey] = true
|
||||
default:
|
||||
allowStage = false
|
||||
}
|
||||
if !allowStage {
|
||||
continue
|
||||
}
|
||||
out.AllowedStageKeys[stPlan.StageKey] = true
|
||||
for _, dt := range stPlan.DocTasks {
|
||||
if !dt.Enabled {
|
||||
continue
|
||||
@@ -93,15 +88,6 @@ func (s *Service) ResolveStageAccess(ctx context.Context, projectID int64, userI
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func stageDutyForUser(st StagePlan, userID int64) bool {
|
||||
for _, dt := range st.DocTasks {
|
||||
if dt.Enabled && docTaskDutyForUser(dt, userID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func docTaskDutyMode(dt DocTaskPlan) DutyMode {
|
||||
mode, _, _ := ResolveDocTaskDuty(dt)
|
||||
return mode
|
||||
@@ -117,15 +103,6 @@ func docTaskDutyForUser(dt DocTaskPlan, userID int64) bool {
|
||||
effective.ApproverUserID == userID
|
||||
}
|
||||
|
||||
func stageHasUnmanagedDoc(st StagePlan) bool {
|
||||
for _, dt := range st.DocTasks {
|
||||
if dt.Enabled && docTaskDutyMode(dt) == DutyModeNone {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TaskKeyAllowed 判断 task_key 是否可发起非 view 动作。
|
||||
func (a *StageAccess) TaskKeyAllowed(taskKey string) bool {
|
||||
if a == nil || !a.Restricted {
|
||||
|
||||
@@ -38,9 +38,12 @@ func TestResolveStageAccess_ActiveStageDutyOnly(t *testing.T) {
|
||||
t.Fatal("creator should access active stage")
|
||||
}
|
||||
|
||||
outsiderAccess, _ := svc.ResolveStageAccess(ctx, 42, 9, false)
|
||||
if outsiderAccess.StageKeyAllowed("V0.1_triage") {
|
||||
t.Fatal("user without duty should not access active stage chat")
|
||||
memberAccess, _ := svc.ResolveStageAccess(ctx, 42, 9, false)
|
||||
if !memberAccess.StageKeyAllowed("V0.1_triage") {
|
||||
t.Fatal("project member without duty should access active stage chat")
|
||||
}
|
||||
if memberAccess.TaskKeyAllowed("intake") {
|
||||
t.Fatal("member without duty must not act on governed intake task")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,22 +113,25 @@ func TestResolveStageAccess_NoLiveInstanceUnrestricted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveStageAccess_LiveOutsiderStillDenied(t *testing.T) {
|
||||
func TestResolveStageAccess_LiveMemberChatWithoutDuty(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newMemStore()
|
||||
_ = SeedProjectMeta(store, 70, LifecyclePlanning, "", 1, 0)
|
||||
svc := NewService(store, testResolver(), nil, nil)
|
||||
mustActivateTestPlan(t, svc, 70, 1)
|
||||
|
||||
outsider, err := svc.ResolveStageAccess(ctx, 70, 9, false)
|
||||
member, err := svc.ResolveStageAccess(ctx, 70, 9, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !outsider.Restricted {
|
||||
if !member.Restricted {
|
||||
t.Fatal("live instance must restrict")
|
||||
}
|
||||
if outsider.TaskKeyAllowed("intake") || outsider.StageKeyAllowed("V0.1_triage") {
|
||||
t.Fatal("outsider must not access live active stage")
|
||||
if !member.StageKeyAllowed("V0.1_triage") {
|
||||
t.Fatal("member without duty should access active stage chat")
|
||||
}
|
||||
if member.TaskKeyAllowed("intake") {
|
||||
t.Fatal("member without duty must not launch governed intake")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user