From 1af03bfbe3822297ed6e0fb448d82b0372558860 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Tue, 12 May 2020 11:12:31 -0400 Subject: [PATCH] api: add ACLs --- api/graph/generated/generated.go | 197 +++++++++++++++++++++++++++---- api/graph/model/acl.go | 93 +++++++++++++++ api/graph/model/models_gen.go | 9 +- api/graph/model/repository.go | 10 +- api/graph/model/user.go | 18 +-- api/graph/schema.graphqls | 12 +- api/graph/schema.resolvers.go | 71 +++++++++-- 7 files changed, 358 insertions(+), 52 deletions(-) create mode 100644 api/graph/model/acl.go diff --git a/api/graph/generated/generated.go b/api/graph/generated/generated.go index 9fe012d..fa28c96 100644 --- a/api/graph/generated/generated.go +++ b/api/graph/generated/generated.go @@ -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) diff --git a/api/graph/model/acl.go b/api/graph/model/acl.go new file mode 100644 index 0000000..6f81898 --- /dev/null +++ b/api/graph/model/acl.go @@ -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 +} diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go index b3d9e28..b29873e 100644 --- a/api/graph/model/models_gen.go +++ b/api/graph/model/models_gen.go @@ -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 { diff --git a/api/graph/model/repository.go b/api/graph/model/repository.go index 2b57b54..ce4d101 100644 --- a/api/graph/model/repository.go +++ b/api/graph/model/repository.go @@ -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, diff --git a/api/graph/model/user.go b/api/graph/model/user.go index b74ec4d..6aff2ce 100644 --- a/api/graph/model/user.go +++ b/api/graph/model/user.go @@ -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) } diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index 953f364..1ca83d8 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -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! diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index e096b83..862dd9c 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -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 } -- 2.38.4