Overhaul default access lists (API)

This commit is contained in:
Drew DeVault 2021-09-22 12:29:51 +02:00
parent d2f57b98c1
commit ffe1c9b652
7 changed files with 152 additions and 86 deletions

View File

@ -171,7 +171,6 @@ type ComplexityRoot struct {
}
Ticket struct {
ACL func(childComplexity int) int
Assignees func(childComplexity int) int
Authenticity func(childComplexity int) int
Body func(childComplexity int) int
@ -219,6 +218,7 @@ type ComplexityRoot struct {
Subscription func(childComplexity int) int
Tickets func(childComplexity int, cursor *model1.Cursor) int
Updated func(childComplexity int) int
Visibility func(childComplexity int) int
}
TrackerACL struct {
@ -324,7 +324,6 @@ 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)
@ -855,13 +854,6 @@ 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
@ -1127,6 +1119,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Tracker.Updated(childComplexity), true
case "Tracker.visibility":
if e.complexity.Tracker.Visibility == nil {
break
}
return e.complexity.Tracker.Visibility(childComplexity), true
case "TrackerACL.browse":
if e.complexity.TrackerACL.Browse == nil {
break
@ -1481,6 +1480,12 @@ type ExternalUser implements Entity {
externalUrl: String
}
enum Visibility {
PUBLIC
UNLISTED
PRIVATE
}
type Tracker {
id: Int!
created: Time!
@ -1488,6 +1493,7 @@ type Tracker {
owner: Entity! @access(scope: PROFILE, kind: RO)
name: String!
description: String
visibility: Visibility!
tickets(cursor: Cursor): TicketCursor! @access(scope: TICKETS, kind: RO)
labels(cursor: Cursor): LabelCursor!
@ -1559,10 +1565,6 @@ 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 {
@ -1800,6 +1802,7 @@ type Query {
# ACLs or (2) has implicit access to either by ownership or group membership.
trackers(cursor: Cursor): TrackerCursor @access(scope: TRACKERS, kind: RO)
# Returns a specific tracker by ID.
tracker(id: Int!): Tracker @access(scope: TRACKERS, kind: RO)
# Returns a specific tracker, owned by the authenticated user.
@ -5692,38 +5695,6 @@ 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 {
@ -6383,6 +6354,41 @@ func (ec *executionContext) _Tracker_description(ctx context.Context, field grap
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _Tracker_visibility(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: false,
IsResolver: false,
}
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 obj.Visibility, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(model.Visibility)
fc.Result = res
return ec.marshalNVisibility2gitᚗsrᚗhtᚋאsircmpwnᚋtodoᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐVisibility(ctx, field.Selections, res)
}
func (ec *executionContext) _Tracker_tickets(ctx context.Context, field graphql.CollectedField, obj *model.Tracker) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -10182,17 +10188,6 @@ 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))
}
@ -10395,6 +10390,11 @@ func (ec *executionContext) _Tracker(ctx context.Context, sel ast.SelectionSet,
}
case "description":
out.Values[i] = ec._Tracker_description(ctx, field, obj)
case "visibility":
out.Values[i] = ec._Tracker_visibility(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
case "tickets":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
@ -11688,6 +11688,16 @@ func (ec *executionContext) marshalNVersion2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋtodo
return ec._Version(ctx, sel, v)
}
func (ec *executionContext) unmarshalNVisibility2gitᚗsrᚗhtᚋאsircmpwnᚋtodoᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐVisibility(ctx context.Context, v interface{}) (model.Visibility, error) {
var res model.Visibility
err := res.UnmarshalGQL(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNVisibility2gitᚗsrᚗhtᚋאsircmpwnᚋtodoᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐVisibility(ctx context.Context, sel ast.SelectionSet, v model.Visibility) graphql.Marshaler {
return v
}
func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler {
return ec.___Directive(ctx, sel, &v)
}

View File

@ -343,3 +343,46 @@ func (e *TicketStatus) UnmarshalGQL(v interface{}) error {
func (e TicketStatus) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type Visibility string
const (
VisibilityPublic Visibility = "PUBLIC"
VisibilityUnlisted Visibility = "UNLISTED"
VisibilityPrivate Visibility = "PRIVATE"
)
var AllVisibility = []Visibility{
VisibilityPublic,
VisibilityUnlisted,
VisibilityPrivate,
}
func (e Visibility) IsValid() bool {
switch e {
case VisibilityPublic, VisibilityUnlisted, VisibilityPrivate:
return true
}
return false
}
func (e Visibility) String() string {
return string(e)
}
func (e *Visibility) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = Visibility(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid Visibility", str)
}
return nil
}
func (e Visibility) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

View File

@ -25,11 +25,12 @@ const (
)
type Tracker struct {
ID int `json:"id"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Name string `json:"name"`
Description *string `json:"description"`
ID int `json:"id"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Name string `json:"name"`
Description *string `json:"description"`
Visibility Visibility `json:"visibility"`
OwnerID int
Access int
@ -63,6 +64,7 @@ func (t *Tracker) Fields() *database.ModelFields {
{ "updated", "updated", &t.Updated },
{ "name", "name", &t.Name },
{ "description", "description", &t.Description },
{ "visibility", "visibility", &t.Visibility },
// Always fetch:
{ "id", "", &t.ID },
@ -92,7 +94,7 @@ func (t *Tracker) QueryWithCursor(ctx context.Context, runner sq.BaseRunner,
tr_ua.permissions,
CASE WHEN tr.owner_id = ?
THEN ?
ELSE tr.default_user_perms
ELSE tr.default_access
END)`,
auser.UserID, ACCESS_ALL).
Column(`tr_ua.id`).

View File

@ -73,6 +73,12 @@ type ExternalUser implements Entity {
externalUrl: String
}
enum Visibility {
PUBLIC
UNLISTED
PRIVATE
}
type Tracker {
id: Int!
created: Time!
@ -80,6 +86,7 @@ type Tracker {
owner: Entity! @access(scope: PROFILE, kind: RO)
name: String!
description: String
visibility: Visibility!
tickets(cursor: Cursor): TicketCursor! @access(scope: TICKETS, kind: RO)
labels(cursor: Cursor): LabelCursor!
@ -151,10 +158,6 @@ 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 {
@ -392,6 +395,7 @@ type Query {
# ACLs or (2) has implicit access to either by ownership or group membership.
trackers(cursor: Cursor): TrackerCursor @access(scope: TRACKERS, kind: RO)
# Returns a specific tracker by ID.
tracker(id: Int!): Tracker @access(scope: TRACKERS, kind: RO)
# Returns a specific tracker, owned by the authenticated user.

View File

@ -91,6 +91,8 @@ func (r *labelResolver) Tickets(ctx context.Context, obj *model.Label, cursor *c
Isolation: 0,
ReadOnly: true,
}, func(tx *sql.Tx) error {
// No authentication necessary: if you have access to the label you
// have access to the tickets.
ticket := (&model.Ticket{}).As(`tk`)
query := database.
Select(ctx, ticket).
@ -345,10 +347,6 @@ func (r *ticketResolver) Subscription(ctx context.Context, obj *model.Ticket) (*
return loaders.ForContext(ctx).SubsByTicketIDUnsafe.Load(obj.PKID)
}
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)
}
@ -370,10 +368,6 @@ 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)
}
@ -384,10 +378,23 @@ func (r *trackerResolver) Tickets(ctx context.Context, obj *model.Tracker, curso
ReadOnly: true,
}, func(tx *sql.Tx) error {
ticket := (&model.Ticket{}).As(`tk`)
query := database.
Select(ctx, ticket).
From(`ticket tk`).
Where(`tk.tracker_id = ?`, obj.ID)
var query sq.SelectBuilder
if obj.CanBrowse() {
query = database.
Select(ctx, ticket).
From(`ticket tk`).
Where(`tk.tracker_id = ?`, obj.ID)
} else {
user := auth.ForContext(ctx)
query = database.
Select(ctx, ticket).
From(`ticket tk`).
Join(`participant p ON p.user_id = ?`, user.UserID).
Where(sq.And{
sq.Expr(`tk.tracker_id = ?`, obj.ID),
sq.Expr(`tk.submitter_id = p.id`),
})
}
tickets, cursor = ticket.QueryWithCursor(ctx, tx, query, cursor)
return nil
}); err != nil {
@ -527,7 +534,7 @@ func (r *userResolver) Trackers(ctx context.Context, obj *model.User, cursor *co
sq.Expr(`tr.owner_id = ?`, obj.ID),
sq.Or{
sq.Expr(`tr.owner_id = ?`, auser.UserID),
sq.Expr(`tr.default_user_perms > 0`),
sq.Expr(`tr.visibility = 'PUBLIC'`),
sq.And{
sq.Expr(`ua.user_id = ?`, auser.UserID),
sq.Expr(`ua.permissions > 0`),

View File

@ -159,7 +159,7 @@ func fetchTrackersByID(ctx context.Context) func(ids []int) ([]*model.Tracker, [
ua.permissions,
CASE WHEN tr.owner_id = ?
THEN ?
ELSE tr.default_user_perms
ELSE tr.default_access
END)`,
auser.UserID, model.ACCESS_ALL).
Column(`ua.id`).
@ -167,7 +167,7 @@ func fetchTrackersByID(ctx context.Context) func(ids []int) ([]*model.Tracker, [
sq.Expr(`tr.id = ANY(?)`, pq.Array(ids)),
sq.Or{
sq.Expr(`tr.owner_id = ?`, auser.UserID),
sq.Expr(`tr.default_user_perms > 0`),
sq.Expr(`tr.visibility != 'PRIVATE'`),
sq.And{
sq.Expr(`ua.user_id = ?`, auser.UserID),
sq.Expr(`ua.permissions > 0`),
@ -291,12 +291,12 @@ func fetchTrackersByOwnerName(ctx context.Context) func(tuples [][2]string) ([]*
ua.permissions,
CASE WHEN tr.owner_id = ?
THEN ?
ELSE tr.default_user_perms
ELSE tr.default_access
END)`,
model.ACCESS_ALL, auser.UserID).
Where(sq.Or{
sq.Expr(`tr.owner_id = ?`, auser.UserID),
sq.Expr(`tr.default_user_perms > 0`),
sq.Expr(`tr.visibility != 'PRIVATE'`),
sq.And{
sq.Expr(`ua.user_id = ?`, auser.UserID),
sq.Expr(`ua.permissions > 0`),
@ -356,7 +356,7 @@ func fetchTicketsByID(ctx context.Context) func(ids []int) ([]*model.Ticket, []e
sq.Expr(`ti.id = ANY(?)`, pq.Array(ids)),
sq.Or{
sq.Expr(`tr.owner_id = ?`, auser.UserID),
sq.Expr(`tr.default_user_perms > 0`),
sq.Expr(`tr.visibility != 'PRIVATE'`),
sq.And{
sq.Expr(`ua.user_id = ?`, auser.UserID),
sq.Expr(`ua.permissions > 0`),
@ -568,7 +568,7 @@ func fetchLabelsByID(ctx context.Context) func(ids []int) ([]*model.Label, []err
sq.Expr(`l.id = ANY(?)`, pq.Array(ids)),
sq.Or{
sq.Expr(`tr.owner_id = ?`, auser.UserID),
sq.Expr(`tr.default_user_perms > 0`),
sq.Expr(`tr.visibility != 'PRIVATE'`),
sq.And{
sq.Expr(`ua.user_id = ?`, auser.UserID),
sq.Expr(`ua.permissions > 0`),

View File

@ -19,7 +19,7 @@ def user_trackers_GET(username):
trackers = Tracker.query.filter(Tracker.owner_id == user.id)
if current_token.user_id != user.id:
# TODO: proper ACLs
trackers = trackers.filter(Tracker.default_user_perms > 0)
trackers = trackers.filter(Tracker.default_access > 0)
return paginated_response(Tracker.id, trackers)
@trackers.route("/api/trackers", methods=["POST"])