API: expand tracker access controls

This commit is contained in:
Drew DeVault 2021-02-15 11:53:14 -05:00
parent aa68b89863
commit b82659cd0b
5 changed files with 218 additions and 13 deletions

View File

@ -176,6 +176,7 @@ type ComplexityRoot struct {
}
Ticket struct {
ACL func(childComplexity int) int
Assignees func(childComplexity int) int
Authenticity func(childComplexity int) int
Created func(childComplexity int) int
@ -212,6 +213,7 @@ type ComplexityRoot struct {
}
Tracker struct {
ACL func(childComplexity int) int
Acls func(childComplexity int, cursor *model1.Cursor) int
Created func(childComplexity int) int
DefaultACLs func(childComplexity int) int
@ -328,6 +330,7 @@ type TicketResolver interface {
Assignees(ctx context.Context, obj *model.Ticket) ([]model.Entity, error)
Events(ctx context.Context, obj *model.Ticket, cursor *model1.Cursor) (*model.EventCursor, error)
Subscription(ctx context.Context, obj *model.Ticket) (*model.TicketSubscription, error)
ACL(ctx context.Context, obj *model.Ticket) (model.ACL, error)
}
type TicketMentionResolver interface {
Ticket(ctx context.Context, obj *model.TicketMention) (*model.Ticket, error)
@ -345,6 +348,7 @@ type TrackerResolver interface {
Acls(ctx context.Context, obj *model.Tracker, cursor *model1.Cursor) (*model.ACLCursor, error)
Subscription(ctx context.Context, obj *model.Tracker) (*model.TrackerSubscription, error)
ACL(ctx context.Context, obj *model.Tracker) (model.ACL, error)
}
type TrackerSubscriptionResolver interface {
Tracker(ctx context.Context, obj *model.TrackerSubscription) (*model.Tracker, error)
@ -875,6 +879,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.SubscriptionCursor.Results(childComplexity), true
case "Ticket.acl":
if e.complexity.Ticket.ACL == nil {
break
}
return e.complexity.Ticket.ACL(childComplexity), true
case "Ticket.assignees":
if e.complexity.Ticket.Assignees == nil {
break
@ -1048,6 +1059,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.TicketSubscription.Ticket(childComplexity), true
case "Tracker.acl":
if e.complexity.Tracker.ACL == nil {
break
}
return e.complexity.Tracker.ACL(childComplexity), true
case "Tracker.acls":
if e.complexity.Tracker.Acls == nil {
break
@ -1509,6 +1527,10 @@ type Tracker {
# If the authenticated user is subscribed to this tracker, this is that
# subscription.
subscription: TrackerSubscription @access(scope: SUBSCRIPTIONS, kind: RO)
# The access control list entry (or the default ACL) which describes the
# authenticated user's permissions with respect to this tracker.
acl: ACL
}
enum TicketStatus {
@ -1566,6 +1588,10 @@ type Ticket {
# If the authenticated user is subscribed to this ticket, this is that
# subscription.
subscription: TicketSubscription @access(scope: SUBSCRIPTIONS, kind: RO)
# The access control list entry (or the default ACL) which describes the
# authenticated user's permissions with respect to this ticket.
acl: ACL
}
interface ACL {
@ -5502,6 +5528,38 @@ func (ec *executionContext) _Ticket_subscription(ctx context.Context, field grap
return ec.marshalOTicketSubscription2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋtodoᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐTicketSubscription(ctx, field.Selections, res)
}
func (ec *executionContext) _Ticket_acl(ctx context.Context, field graphql.CollectedField, obj *model.Ticket) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Ticket",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Ticket().ACL(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(model.ACL)
fc.Result = res
return ec.marshalOACL2gitᚗsrᚗhtᚋאsircmpwnᚋtodoᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐACL(ctx, field.Selections, res)
}
func (ec *executionContext) _TicketCursor_results(ctx context.Context, field graphql.CollectedField, obj *model.TicketCursor) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -6351,6 +6409,38 @@ func (ec *executionContext) _Tracker_subscription(ctx context.Context, field gra
return ec.marshalOTrackerSubscription2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋtodoᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐTrackerSubscription(ctx, field.Selections, res)
}
func (ec *executionContext) _Tracker_acl(ctx context.Context, field graphql.CollectedField, obj *model.Tracker) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Tracker",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Tracker().ACL(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(model.ACL)
fc.Result = res
return ec.marshalOACL2gitᚗsrᚗhtᚋאsircmpwnᚋtodoᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐACL(ctx, field.Selections, res)
}
func (ec *executionContext) _TrackerACL_id(ctx context.Context, field graphql.CollectedField, obj *model.TrackerACL) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -9801,6 +9891,17 @@ func (ec *executionContext) _Ticket(ctx context.Context, sel ast.SelectionSet, o
res = ec._Ticket_subscription(ctx, field, obj)
return res
})
case "acl":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Ticket_acl(ctx, field, obj)
return res
})
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@ -10058,6 +10159,17 @@ func (ec *executionContext) _Tracker(ctx context.Context, sel ast.SelectionSet,
res = ec._Tracker_subscription(ctx, field, obj)
return res
})
case "acl":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Tracker_acl(ctx, field, obj)
return res
})
default:
panic("unknown field " + strconv.Quote(field.Name))
}

View File

@ -2,16 +2,28 @@ package model
import (
"context"
"errors"
"database/sql"
"strconv"
"time"
sq "github.com/Masterminds/squirrel"
"git.sr.ht/~sircmpwn/core-go/auth"
"git.sr.ht/~sircmpwn/core-go/database"
"git.sr.ht/~sircmpwn/core-go/model"
)
const (
ACCESS_NONE = 0
ACCESS_BROWSE = 1
ACCESS_SUBMIT = 2
ACCESS_COMMENT = 4
ACCESS_EDIT = 8
ACCESS_TRIAGE = 16
ACCESS_ALL = 1 | 2 | 4 | 8 | 16
)
type Tracker struct {
ID int `json:"id"`
Created time.Time `json:"created"`
@ -21,6 +33,7 @@ type Tracker struct {
DefaultACLs *DefaultACLs `json:"defaultACLs"`
OwnerID int
Access int
alias string
fields *database.ModelFields
@ -43,7 +56,6 @@ func (t *Tracker) Fields() *database.ModelFields {
if t.fields != nil {
return t.fields
}
// TODO: Fetch ACLs
t.fields = &database.ModelFields{
Fields: []*database.FieldMap{
{ "id", "id", &t.ID },
@ -71,9 +83,19 @@ func (t *Tracker) QueryWithCursor(ctx context.Context, runner sq.BaseRunner,
next, _ := strconv.ParseInt(cur.Next, 10, 64)
q = q.Where(database.WithAlias(t.alias, "id")+"<= ?", next)
}
auser := auth.ForContext(ctx)
q = q.
OrderBy(database.WithAlias(t.alias, "id")).
Limit(uint64(cur.Count + 1))
Limit(uint64(cur.Count + 1)).
LeftJoin(`user_access tr_ua ON tr_ua.tracker_id = tr.id`).
Column(`COALESCE(
tr_ua.permissions,
CASE WHEN tr.owner_id = ?
THEN ?
ELSE tr.default_user_perms
END)`,
ACCESS_ALL, auser.UserID).
Where(`COALESCE(tr_ua.user_id, ?) = ?`, auser.UserID, auser.UserID)
if rows, err = q.RunWith(runner).QueryContext(ctx); err != nil {
panic(err)
@ -83,7 +105,8 @@ func (t *Tracker) QueryWithCursor(ctx context.Context, runner sq.BaseRunner,
var trackers []*Tracker
for rows.Next() {
var tracker Tracker
if err := rows.Scan(database.Scan(ctx, &tracker)...); err != nil {
if err := rows.Scan(append(database.Scan(
ctx, &tracker), &tracker.Access)...); err != nil {
panic(err)
}
trackers = append(trackers, &tracker)
@ -102,3 +125,38 @@ func (t *Tracker) QueryWithCursor(ctx context.Context, runner sq.BaseRunner,
return trackers, cur
}
func (t *Tracker) CanBrowse() bool {
if t.Access == ACCESS_NONE {
panic(errors.New("Invariant broken: tracker access is 0"))
}
return t.Access & ACCESS_BROWSE == ACCESS_BROWSE
}
func (t *Tracker) CanSubmit() bool {
if t.Access == ACCESS_NONE {
panic(errors.New("Invariant broken: tracker access is 0"))
}
return t.Access & ACCESS_SUBMIT == ACCESS_SUBMIT
}
func (t *Tracker) CanComment() bool {
if t.Access == ACCESS_NONE {
panic(errors.New("Invariant broken: tracker access is 0"))
}
return t.Access & ACCESS_COMMENT == ACCESS_COMMENT
}
func (t *Tracker) CanEdit() bool {
if t.Access == ACCESS_NONE {
panic(errors.New("Invariant broken: tracker access is 0"))
}
return t.Access & ACCESS_EDIT == ACCESS_EDIT
}
func (t *Tracker) CanTriage() bool {
if t.Access == ACCESS_NONE {
panic(errors.New("Invariant broken: tracker access is 0"))
}
return t.Access & ACCESS_TRIAGE == ACCESS_TRIAGE
}

View File

@ -88,6 +88,10 @@ type Tracker {
# If the authenticated user is subscribed to this tracker, this is that
# subscription.
subscription: TrackerSubscription @access(scope: SUBSCRIPTIONS, kind: RO)
# The access control list entry (or the default ACL) which describes the
# authenticated user's permissions with respect to this tracker.
acl: ACL
}
enum TicketStatus {
@ -145,6 +149,10 @@ type Ticket {
# If the authenticated user is subscribed to this ticket, this is that
# subscription.
subscription: TicketSubscription @access(scope: SUBSCRIPTIONS, kind: RO)
# The access control list entry (or the default ACL) which describes the
# authenticated user's permissions with respect to this ticket.
acl: ACL
}
interface ACL {

View File

@ -6,6 +6,7 @@ package graph
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
@ -333,6 +334,10 @@ func (r *ticketResolver) Subscription(ctx context.Context, obj *model.Ticket) (*
panic(fmt.Errorf("not implemented"))
}
func (r *ticketResolver) ACL(ctx context.Context, obj *model.Ticket) (model.ACL, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *ticketMentionResolver) Ticket(ctx context.Context, obj *model.TicketMention) (*model.Ticket, error) {
return loaders.ForContext(ctx).TicketsByID.Load(obj.TicketID)
}
@ -354,6 +359,10 @@ func (r *trackerResolver) Owner(ctx context.Context, obj *model.Tracker) (model.
}
func (r *trackerResolver) Tickets(ctx context.Context, obj *model.Tracker, cursor *coremodel.Cursor) (*model.TicketCursor, error) {
if !obj.CanBrowse() {
return nil, errors.New("You do not have permission to browse this tracker")
}
if cursor == nil {
cursor = coremodel.NewCursor(nil)
}
@ -409,6 +418,10 @@ func (r *trackerResolver) Subscription(ctx context.Context, obj *model.Tracker)
panic(fmt.Errorf("not implemented"))
}
func (r *trackerResolver) ACL(ctx context.Context, obj *model.Tracker) (model.ACL, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *trackerSubscriptionResolver) Tracker(ctx context.Context, obj *model.TrackerSubscription) (*model.Tracker, error) {
return loaders.ForContext(ctx).TrackersByID.Load(obj.TrackerID)
}

View File

@ -145,17 +145,23 @@ func fetchTrackersByID(ctx context.Context) func(ids []int) ([]*model.Tracker, [
err error
rows *sql.Rows
)
// TODO: Stash the ACL details in case they're useful later?
auser := auth.ForContext(ctx)
query := database.
Select(ctx, (&model.Tracker{}).As(`t`)).
From(`"tracker" t`).
LeftJoin(`user_access ua ON ua.tracker_id = t.id`).
Select(ctx, (&model.Tracker{}).As(`tr`)).
From(`"tracker" tr`).
LeftJoin(`user_access ua ON ua.tracker_id = tr.id`).
Column(`COALESCE(
ua.permissions,
CASE WHEN tr.owner_id = ?
THEN ?
ELSE tr.default_user_perms
END)`,
model.ACCESS_ALL, auser.UserID).
Where(sq.And{
sq.Expr(`t.id = ANY(?)`, pq.Array(ids)),
sq.Expr(`tr.id = ANY(?)`, pq.Array(ids)),
sq.Or{
sq.Expr(`t.owner_id = ?`, auser.UserID),
sq.Expr(`t.default_user_perms > 0`),
sq.Expr(`tr.owner_id = ?`, auser.UserID),
sq.Expr(`tr.default_user_perms > 0`),
sq.And{
sq.Expr(`ua.user_id = ?`, auser.UserID),
sq.Expr(`ua.permissions > 0`),
@ -170,7 +176,8 @@ func fetchTrackersByID(ctx context.Context) func(ids []int) ([]*model.Tracker, [
trackersByID := map[int]*model.Tracker{}
for rows.Next() {
tracker := model.Tracker{}
if err := rows.Scan(database.Scan(ctx, &tracker)...); err != nil {
if err := rows.Scan(append(database.Scan(
ctx, &tracker), &tracker.Access)...); err != nil {
return err
}
trackersByID[tracker.ID] = &tracker
@ -273,6 +280,13 @@ func fetchTrackersByOwnerName(ctx context.Context) func(tuples [][2]string) ([]*
Join(`"tracker" tr ON ut.tracker = tr.name
AND u.id = tr.owner_id`).
LeftJoin(`user_access ua ON ua.tracker_id = tr.id`).
Column(`COALESCE(
ua.permissions,
CASE WHEN tr.owner_id = ?
THEN ?
ELSE tr.default_user_perms
END)`,
model.ACCESS_ALL, auser.UserID).
Where(sq.Or{
sq.Expr(`tr.owner_id = ?`, auser.UserID),
sq.Expr(`tr.default_user_perms > 0`),
@ -290,8 +304,8 @@ func fetchTrackersByOwnerName(ctx context.Context) func(tuples [][2]string) ([]*
for rows.Next() {
var ownerName string
tracker := model.Tracker{}
if err := rows.Scan(append(
database.Scan(ctx, &tracker), &ownerName)...); err != nil {
if err := rows.Scan(append(database.Scan(ctx, &tracker),
&ownerName, &tracker.Access)...); err != nil {
return err
}
trackersByOwnerName[[2]string{ownerName, tracker.Name}] = &tracker