~edwargix/git.sr.ht

1af03bfbe3822297ed6e0fb448d82b0372558860 — Drew DeVault 5 years ago c15d59d
api: add ACLs
M api/graph/generated/generated.go => api/graph/generated/generated.go +174 -23
@@ 37,6 37,7 @@ type Config struct {
}

type ResolverRoot interface {
	ACL() ACLResolver
	Mutation() MutationResolver
	Query() QueryResolver
	Repository() RepositoryResolver


@@ 56,6 57,11 @@ type ComplexityRoot struct {
		Repository func(childComplexity int) int
	}

	ACLCursor struct {
		Cursor  func(childComplexity int) int
		Results func(childComplexity int) int
	}

	Artifact struct {
		Checksum   func(childComplexity int) int
		Created    func(childComplexity int) int


@@ 199,6 205,10 @@ type ComplexityRoot struct {
	}
}

type ACLResolver interface {
	Repository(ctx context.Context, obj *model.ACL) (*model.Repository, error)
	Entity(ctx context.Context, obj *model.ACL) (model.Entity, error)
}
type MutationResolver interface {
	CreateRepository(ctx context.Context, params *model.RepoInput) (*model.Repository, error)
	UpdateRepository(ctx context.Context, id string, params *model.RepoInput) (*model.Repository, error)


@@ 220,7 230,7 @@ type QueryResolver interface {
type RepositoryResolver interface {
	Owner(ctx context.Context, obj *model.Repository) (model.Entity, error)

	AccessControlList(ctx context.Context, obj *model.Repository, cursor *model.Cursor) ([]*model.ACL, error)
	AccessControlList(ctx context.Context, obj *model.Repository, cursor *model.Cursor) (*model.ACLCursor, error)
	References(ctx context.Context, obj *model.Repository, cursor *model.Cursor) ([]*model.Reference, error)
}
type TreeResolver interface {


@@ 280,6 290,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.ACL.Repository(childComplexity), true

	case "ACLCursor.cursor":
		if e.complexity.ACLCursor.Cursor == nil {
			break
		}

		return e.complexity.ACLCursor.Cursor(childComplexity), true

	case "ACLCursor.results":
		if e.complexity.ACLCursor.Results == nil {
			break
		}

		return e.complexity.ACLCursor.Results(childComplexity), true

	case "Artifact.checksum":
		if e.complexity.Artifact.Checksum == nil {
			break


@@ 1189,7 1213,7 @@ type Repository {
  upstreamUrl: String

  # Returns access control list entries for this repository
  accessControlList(cursor: Cursor): [ACL]!
  accessControlList(cursor: Cursor): ACLCursor

  ## Plumbing API:



@@ 1239,6 1263,16 @@ type RepositoryCursor {
  cursor: Cursor
}

# A cursor for enumerating access control list entries
#
# If there are additional results available, the cursor object may be passed
# back into the same endpoint to retrieve another page. If the cursor is null,
# there are no remaining results to return.
type ACLCursor {
  results: [ACL]!
  cursor: Cursor
}

# Access Control List entry
type ACL {
  id: Int!


@@ 1958,13 1992,13 @@ func (ec *executionContext) _ACL_repository(ctx context.Context, field graphql.C
		Object:   "ACL",
		Field:    field,
		Args:     nil,
		IsMethod: false,
		IsMethod: 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 obj.Repository, nil
		return ec.resolvers.ACL().Repository(rctx, obj)
	})
	if err != nil {
		ec.Error(ctx, err)


@@ 1992,13 2026,13 @@ func (ec *executionContext) _ACL_entity(ctx context.Context, field graphql.Colle
		Object:   "ACL",
		Field:    field,
		Args:     nil,
		IsMethod: false,
		IsMethod: 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 obj.Entity, nil
		return ec.resolvers.ACL().Entity(rctx, obj)
	})
	if err != nil {
		ec.Error(ctx, err)


@@ 2046,6 2080,71 @@ func (ec *executionContext) _ACL_mode(ctx context.Context, field graphql.Collect
	return ec.marshalOAccessMode2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐAccessMode(ctx, field.Selections, res)
}

func (ec *executionContext) _ACLCursor_results(ctx context.Context, field graphql.CollectedField, obj *model.ACLCursor) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:   "ACLCursor",
		Field:    field,
		Args:     nil,
		IsMethod: 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.Results, 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.ACL)
	fc.Result = res
	return ec.marshalNACL2ᚕᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐACL(ctx, field.Selections, res)
}

func (ec *executionContext) _ACLCursor_cursor(ctx context.Context, field graphql.CollectedField, obj *model.ACLCursor) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:   "ACLCursor",
		Field:    field,
		Args:     nil,
		IsMethod: 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.Cursor, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		return graphql.Null
	}
	res := resTmp.(*model.Cursor)
	fc.Result = res
	return ec.marshalOCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐCursor(ctx, field.Selections, res)
}

func (ec *executionContext) _Artifact_id(ctx context.Context, field graphql.CollectedField, obj *model.Artifact) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {


@@ 3835,14 3934,11 @@ func (ec *executionContext) _Repository_accessControlList(ctx context.Context, f
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.([]*model.ACL)
	res := resTmp.(*model.ACLCursor)
	fc.Result = res
	return ec.marshalNACL2ᚕᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐACL(ctx, field.Selections, res)
	return ec.marshalOACLCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐACLCursor(ctx, field.Selections, res)
}

func (ec *executionContext) _Repository_references(ctx context.Context, field graphql.CollectedField, obj *model.Repository) (ret graphql.Marshaler) {


@@ 6619,25 6715,72 @@ func (ec *executionContext) _ACL(ctx context.Context, sel ast.SelectionSet, obj 
		case "id":
			out.Values[i] = ec._ACL_id(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "created":
			out.Values[i] = ec._ACL_created(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "repository":
			out.Values[i] = ec._ACL_repository(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
			}
			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._ACL_repository(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		case "entity":
			out.Values[i] = ec._ACL_entity(ctx, field, obj)
			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._ACL_entity(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		case "mode":
			out.Values[i] = ec._ACL_mode(ctx, field, obj)
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}
	}
	out.Dispatch()
	if invalids > 0 {
		return graphql.Null
	}
	return out
}

var aCLCursorImplementors = []string{"ACLCursor"}

func (ec *executionContext) _ACLCursor(ctx context.Context, sel ast.SelectionSet, obj *model.ACLCursor) graphql.Marshaler {
	fields := graphql.CollectFields(ec.OperationContext, sel, aCLCursorImplementors)

	out := graphql.NewFieldSet(fields)
	var invalids uint32
	for i, field := range fields {
		switch field.Name {
		case "__typename":
			out.Values[i] = graphql.MarshalString("ACLCursor")
		case "results":
			out.Values[i] = ec._ACLCursor_results(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
			}
		case "mode":
			out.Values[i] = ec._ACL_mode(ctx, field, obj)
		case "cursor":
			out.Values[i] = ec._ACLCursor_cursor(ctx, field, obj)
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}


@@ 7120,9 7263,6 @@ func (ec *executionContext) _Repository(ctx context.Context, sel ast.SelectionSe
					}
				}()
				res = ec._Repository_accessControlList(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		case "references":


@@ 8535,6 8675,17 @@ func (ec *executionContext) marshalOACL2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsr
	return ec._ACL(ctx, sel, v)
}

func (ec *executionContext) marshalOACLCursor2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐACLCursor(ctx context.Context, sel ast.SelectionSet, v model.ACLCursor) graphql.Marshaler {
	return ec._ACLCursor(ctx, sel, &v)
}

func (ec *executionContext) marshalOACLCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐACLCursor(ctx context.Context, sel ast.SelectionSet, v *model.ACLCursor) graphql.Marshaler {
	if v == nil {
		return graphql.Null
	}
	return ec._ACLCursor(ctx, sel, v)
}

func (ec *executionContext) unmarshalOAccessMode2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐAccessMode(ctx context.Context, v interface{}) (model.AccessMode, error) {
	var res model.AccessMode
	return res, res.UnmarshalGQL(v)

A api/graph/model/acl.go => api/graph/model/acl.go +93 -0
@@ 0,0 1,93 @@
package model

import (
	"context"
	"database/sql"
	"strconv"
	"time"

	sq "github.com/Masterminds/squirrel"

	"git.sr.ht/~sircmpwn/git.sr.ht/api/database"
)

// TODO: Drop updated column from database
type ACL struct {
	ID         int         `json:"id"`
	Created    time.Time   `json:"created"`
	Mode       *AccessMode `json:"mode"`

	RepoID int
	UserID int

	alias  string
}

func (acl *ACL) As(alias string) *ACL {
	acl.alias = alias
	return acl
}

func (acl *ACL) Select(ctx context.Context) []string {
	cols := database.ColumnsFor(ctx, acl.alias, map[string]string{
		"id":      "id",
		"created": "created",
		"mode":    "mode",
	})
	return append(cols,
		database.WithAlias(acl.alias, "id"),
		database.WithAlias(acl.alias, "repo_id"),
		database.WithAlias(acl.alias, "user_id"))
}

func (acl *ACL) Fields(ctx context.Context) []interface{} {
	fields := database.FieldsFor(ctx, map[string]interface{}{
		"id":      &acl.ID,
		"created": &acl.Created,
		"mode":    &acl.Mode,
	})
	return append(fields, &acl.ID, &acl.RepoID, &acl.UserID)
}

func (acl *ACL) QueryWithCursor(ctx context.Context,
	db *sql.DB, q sq.SelectBuilder, cur *Cursor) ([]*ACL, *Cursor) {
	var (
		err  error
		rows *sql.Rows
	)

	if cur.Next != "" {
		next, _ := strconv.Atoi(cur.Next)
		q = q.Where(database.WithAlias(acl.alias, "id") + "<= ?", next)
	}
	q = q.
		OrderBy(database.WithAlias(acl.alias, "id") + " DESC").
		Limit(uint64(cur.Count + 1))

	if rows, err = q.RunWith(db).QueryContext(ctx); err != nil {
		panic(err)
	}
	defer rows.Close()

	var acls []*ACL
	for rows.Next() {
		var acl ACL
		if err := rows.Scan(acl.Fields(ctx)...); err != nil {
			panic(err)
		}
		acls = append(acls, &acl)
	}

	if len(acls) > cur.Count {
		cur = &Cursor{
			Count:  cur.Count,
			Next:   strconv.Itoa(acls[len(acls)-1].ID),
			Search: cur.Search,
		}
		acls = acls[:cur.Count]
	} else {
		cur = nil
	}

	return acls, cur
}

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +3 -6
@@ 13,12 13,9 @@ type Entity interface {
	IsEntity()
}

type ACL struct {
	ID         int         `json:"id"`
	Created    time.Time   `json:"created"`
	Repository *Repository `json:"repository"`
	Entity     Entity      `json:"entity"`
	Mode       *AccessMode `json:"mode"`
type ACLCursor struct {
	Results []*ACL  `json:"results"`
	Cursor  *Cursor `json:"cursor"`
}

type Artifact struct {

M api/graph/model/repository.go => api/graph/model/repository.go +5 -5
@@ 54,6 54,11 @@ func (r *Repository) Head() *Reference {
	return &Reference{Ref: ref, Repo: r.repo}
}

func (r *Repository) As(alias string) *Repository {
	r.alias = alias
	return r
}

func (r *Repository) Select(ctx context.Context) []string {
	cols := database.ColumnsFor(ctx, r.alias, map[string]string{
		"id":          "id",


@@ 70,11 75,6 @@ func (r *Repository) Select(ctx context.Context) []string {
		database.WithAlias(r.alias, "updated"))
}

func (r *Repository) As(alias string) *Repository {
	r.alias = alias
	return r
}

func (r *Repository) Fields(ctx context.Context) []interface{} {
	fields := database.FieldsFor(ctx, map[string]interface{}{
		"id":           &r.ID,

M api/graph/model/user.go => api/graph/model/user.go +11 -7
@@ 26,8 26,13 @@ func (u *User) CanonicalName() string {
	return "~" + u.Username
}

func (u *User) As(alias string) *User {
	u.alias = alias
	return u
}

func (u *User) Select(ctx context.Context) []string {
	return database.ColumnsFor(ctx, u.alias, map[string]string{
	cols := database.ColumnsFor(ctx, u.alias, map[string]string{
		"id":       "id",
		"created":  "created",
		"updated":  "updated",


@@ 37,15 42,13 @@ func (u *User) Select(ctx context.Context) []string {
		"location": "location",
		"bio":      "bio",
	})
}

func (u *User) As(alias string) database.Selectable {
	u.alias = alias
	return u
	return append(cols,
		database.WithAlias(u.alias, "id"),
		database.WithAlias(u.alias, "username"))
}

func (u *User) Fields(ctx context.Context) []interface{} {
	return database.FieldsFor(ctx, map[string]interface{}{
	fields := database.FieldsFor(ctx, map[string]interface{}{
		"id":       &u.ID,
		"created":  &u.Created,
		"updated":  &u.Updated,


@@ 55,4 58,5 @@ func (u *User) Fields(ctx context.Context) []interface{} {
		"location": &u.Location,
		"bio":      &u.Bio,
	})
	return append(fields, &u.ID, &u.Username)
}

M api/graph/schema.graphqls => api/graph/schema.graphqls +11 -1
@@ 75,7 75,7 @@ type Repository {
  upstreamUrl: String

  # Returns access control list entries for this repository
  accessControlList(cursor: Cursor): [ACL]!
  accessControlList(cursor: Cursor): ACLCursor

  ## Plumbing API:



@@ 125,6 125,16 @@ type RepositoryCursor {
  cursor: Cursor
}

# A cursor for enumerating access control list entries
#
# If there are additional results available, the cursor object may be passed
# back into the same endpoint to retrieve another page. If the cursor is null,
# there are no remaining results to return.
type ACLCursor {
  results: [ACL]!
  cursor: Cursor
}

# Access Control List entry
type ACL {
  id: Int!

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +61 -10
@@ 19,31 19,31 @@ import (
)

func (r *mutationResolver) CreateRepository(ctx context.Context, params *model.RepoInput) (*model.Repository, error) {
	panic(fmt.Errorf("not implemented"))
	panic(fmt.Errorf("createRepository: not implemented"))
}

func (r *mutationResolver) UpdateRepository(ctx context.Context, id string, params *model.RepoInput) (*model.Repository, error) {
	panic(fmt.Errorf("not implemented"))
	panic(fmt.Errorf("updateRepository: not implemented"))
}

func (r *mutationResolver) DeleteRepository(ctx context.Context, id string) (*model.Repository, error) {
	panic(fmt.Errorf("not implemented"))
	panic(fmt.Errorf("deleteRepository: not implemented"))
}

func (r *mutationResolver) UpdateACL(ctx context.Context, repoID string, mode model.AccessMode, entity string) (*model.ACL, error) {
	panic(fmt.Errorf("not implemented"))
	panic(fmt.Errorf("updateACL: not implemented"))
}

func (r *mutationResolver) DeleteACL(ctx context.Context, repoID int, entity string) (*model.ACL, error) {
	panic(fmt.Errorf("not implemented"))
	panic(fmt.Errorf("deleteACL: not implemented"))
}

func (r *mutationResolver) UploadArtifact(ctx context.Context, repoID int, revspec string, file graphql.Upload) (*model.Artifact, error) {
	panic(fmt.Errorf("not implemented"))
	panic(fmt.Errorf("uploadArtifact: not implemented"))
}

func (r *mutationResolver) DeleteArtifact(ctx context.Context, id int) (*model.Artifact, error) {
	panic(fmt.Errorf("not implemented"))
	panic(fmt.Errorf("deleteArtifact: not implemented"))
}

func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {


@@ 110,8 110,55 @@ func (r *repositoryResolver) Owner(ctx context.Context, obj *model.Repository) (
	return loaders.ForContext(ctx).UsersByID.Load(obj.OwnerID)
}

func (r *repositoryResolver) AccessControlList(ctx context.Context, obj *model.Repository, cursor *model.Cursor) ([]*model.ACL, error) {
	panic(fmt.Errorf("not implemented"))
func (r *repositoryResolver) AccessControlList(ctx context.Context, obj *model.Repository, cursor *model.Cursor) (*model.ACLCursor, error) {
	if cursor == nil {
		cursor = model.NewCursor(nil)
	}

	acl := (&model.ACL{}).As(`acl`)
	query := database.
		Select(ctx, acl).
		From(`access acl`).
		Join(`repository repo ON acl.repo_id = repo.id`).
		Where(`acl.repo_id = ?`, obj.ID).
		Where(`repo.owner_id = ?`, auth.ForContext(ctx).ID)

	acls, cursor := acl.QueryWithCursor(ctx, r.DB, query, cursor)
	return &model.ACLCursor{acls, cursor}, nil
}

func (r *aCLResolver) Repository(ctx context.Context, obj *model.ACL) (*model.Repository, error) {
	// XXX This could be moved into a loader, but it's unlikely to be a
	// frequently utilized endpoint, so I'm not especially interested in the
	// extra work/cruft.
	repo := (&model.Repository{}).As(`repo`)
	query := database.
		Select(ctx, repo).
		From(`repository repo`).
		Join(`access acl ON acl.repo_id = repo.id`).
		Where(`acl.id = ?`, obj.ID)
	row := query.RunWith(r.DB).QueryRow()
	if err := row.Scan(repo.Fields(ctx)...); err != nil {
		panic(err)
	}
	return repo, nil
}

func (r *aCLResolver) Entity(ctx context.Context, obj *model.ACL) (model.Entity, error) {
	// XXX This could be moved into a loader, but it's unlikely to be a
	// frequently utilized endpoint, so I'm not especially interested in the
	// extra work/cruft.
	user := (&model.User{}).As(`u`)
	query := database.
		Select(ctx, user).
		From(`"user" u`).
		Join(`access acl ON acl.user_id = u.id`).
		Where(`acl.id = ?`, obj.ID)
	row := query.RunWith(r.DB).QueryRow()
	if err := row.Scan(user.Fields(ctx)...); err != nil {
		panic(err)
	}
	return user, nil
}

func (r *repositoryResolver) References(ctx context.Context, obj *model.Repository, cursor *model.Cursor) ([]*model.Reference, error) {


@@ 136,7 183,7 @@ func (r *repositoryResolver) References(ctx context.Context, obj *model.Reposito
}

func (r *treeResolver) Entries(ctx context.Context, obj *model.Tree, cursor *model.Cursor) ([]*model.TreeEntry, error) {
	panic(fmt.Errorf("not implemented"))
	panic(fmt.Errorf("tree.entries: not implemented"))
}

func (r *userResolver) Repositories(ctx context.Context, obj *model.User, cursor *model.Cursor, filter *model.Filter) (*model.RepositoryCursor, error) {


@@ 154,6 201,9 @@ func (r *userResolver) Repositories(ctx context.Context, obj *model.User, cursor
	return &model.RepositoryCursor{repos, cursor}, nil
}

// ACL returns generated.ACLResolver implementation.
func (r *Resolver) ACL() generated.ACLResolver { return &aCLResolver{r} }

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }



@@ 169,6 219,7 @@ func (r *Resolver) Tree() generated.TreeResolver { return &treeResolver{r} }
// User returns generated.UserResolver implementation.
func (r *Resolver) User() generated.UserResolver { return &userResolver{r} }

type aCLResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type repositoryResolver struct{ *Resolver }