API: expand tracker access controls
This commit is contained in:
parent
aa68b89863
commit
b82659cd0b
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue