taskcafe/internal/graph/graph.go
Jordan Knott c2ef8a7d56 feat: replace config system with viper based system
allows for config settings to be easily set through ENV variables,
config files, or CLI flags

adds flag to run migration on web server start (fixes #29)
2020-08-12 22:30:53 -05:00

191 lines
5.4 KiB
Go

package graph
import (
"context"
"errors"
"net/http"
"os"
"reflect"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/auth"
"github.com/jordanknott/taskcafe/internal/db"
log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// NewHandler returns a new graphql endpoint handler.
func NewHandler(repo db.Repository) http.Handler {
c := Config{
Resolvers: &Resolver{
Repository: repo,
},
}
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
role, ok := GetUserRole(ctx)
if !ok {
return nil, errors.New("user ID is missing")
}
if role == "admin" {
return next(ctx)
} else if level == ActionLevelOrg {
return nil, errors.New("must be an org admin")
}
var subjectID uuid.UUID
in := graphql.GetResolverContext(ctx).Args["input"]
if typeArg == ObjectTypeProject || typeArg == ObjectTypeTeam {
val := reflect.ValueOf(in) // could be any underlying type
fieldName := "ProjectID"
if typeArg == ObjectTypeTeam {
fieldName = "TeamID"
}
subjectID, ok = val.FieldByName(fieldName).Interface().(uuid.UUID)
if !ok {
return nil, errors.New("error while casting subject uuid")
}
}
if level == ActionLevelProject {
roles, err := GetProjectRoles(ctx, repo, subjectID)
if err != nil {
return nil, err
}
if roles.TeamRole == "admin" || roles.ProjectRole == "admin" {
log.WithFields(log.Fields{"teamRole": roles.TeamRole, "projectRole": roles.ProjectRole}).Info("is team or project role")
return next(ctx)
}
return nil, errors.New("must be a team or project admin")
} else if level == ActionLevelTeam {
userID, ok := GetUserID(ctx)
if !ok {
return nil, errors.New("user id is missing")
}
role, err := repo.GetTeamRoleForUserID(ctx, db.GetTeamRoleForUserIDParams{UserID: userID, TeamID: subjectID})
if err != nil {
return nil, err
}
if role.RoleCode == "admin" {
return next(ctx)
}
return nil, errors.New("must be a team admin")
}
return nil, errors.New("invalid path")
}
srv := handler.New(NewExecutableSchema(c))
srv.AddTransport(transport.Websocket{
KeepAlivePingInterval: 10 * time.Second,
})
srv.AddTransport(transport.Options{})
srv.AddTransport(transport.GET{})
srv.AddTransport(transport.POST{})
srv.AddTransport(transport.MultipartForm{})
srv.SetQueryCache(lru.New(1000))
srv.Use(extension.AutomaticPersistedQuery{
Cache: lru.New(100),
})
if isProd := os.Getenv("PRODUCTION") == "true"; isProd {
srv.Use(extension.FixedComplexityLimit(10))
} else {
srv.Use(extension.Introspection{})
}
return srv
}
// NewPlaygroundHandler returns a new GraphQL Playground handler.
func NewPlaygroundHandler(endpoint string) http.Handler {
return playground.Handler("GraphQL Playground", endpoint)
}
func GetUserID(ctx context.Context) (uuid.UUID, bool) {
userID, ok := ctx.Value("userID").(uuid.UUID)
return userID, ok
}
func GetUserRole(ctx context.Context) (auth.Role, bool) {
role, ok := ctx.Value("org_role").(auth.Role)
return role, ok
}
func GetUser(ctx context.Context) (uuid.UUID, auth.Role, bool) {
userID, userOK := GetUserID(ctx)
role, roleOK := GetUserRole(ctx)
return userID, role, userOK && roleOK
}
func GetRestrictedMode(ctx context.Context) (auth.RestrictedMode, bool) {
restricted, ok := ctx.Value("restricted_mode").(auth.RestrictedMode)
return restricted, ok
}
func GetProjectRoles(ctx context.Context, r db.Repository, projectID uuid.UUID) (db.GetUserRolesForProjectRow, error) {
userID, ok := GetUserID(ctx)
if !ok {
return db.GetUserRolesForProjectRow{}, errors.New("user ID is not found")
}
return r.GetUserRolesForProject(ctx, db.GetUserRolesForProjectParams{UserID: userID, ProjectID: projectID})
}
func ConvertToRoleCode(r string) RoleCode {
if r == RoleCodeAdmin.String() {
return RoleCodeAdmin
}
if r == RoleCodeMember.String() {
return RoleCodeMember
}
return RoleCodeObserver
}
func RequireTeamAdmin(ctx context.Context, r db.Repository, teamID uuid.UUID) error {
userID, role, ok := GetUser(ctx)
if !ok {
return errors.New("internal: user id is not set")
}
teamRole, err := r.GetTeamRoleForUserID(ctx, db.GetTeamRoleForUserIDParams{UserID: userID, TeamID: teamID})
isAdmin := role == auth.RoleAdmin
isTeamAdmin := err == nil && ConvertToRoleCode(teamRole.RoleCode) == RoleCodeAdmin
if !(isAdmin || isTeamAdmin) {
return &gqlerror.Error{
Message: "organization or team admin role required",
Extensions: map[string]interface{}{
"code": "2-400",
},
}
} else if err != nil {
return err
}
return nil
}
func RequireProjectOrTeamAdmin(ctx context.Context, r db.Repository, projectID uuid.UUID) error {
role, ok := GetUserRole(ctx)
if !ok {
return errors.New("user ID is not set")
}
if role == auth.RoleAdmin {
return nil
}
roles, err := GetProjectRoles(ctx, r, projectID)
if err != nil {
return err
}
if !(roles.ProjectRole == "admin" || roles.TeamRole == "admin") {
return &gqlerror.Error{
Message: "You must be a team or project admin",
Extensions: map[string]interface{}{
"code": "4-400",
},
}
}
return nil
}