From f5f1869b5a917b24c2996553567dc903ce677ef6 Mon Sep 17 00:00:00 2001 From: Lou Knauer Date: Tue, 15 Mar 2022 11:04:54 +0100 Subject: [PATCH] Add user name/email to GraphQL API --- auth/auth.go | 19 ++ frontend | 2 +- graph/generated/generated.go | 408 +++++++++++++++++++++++++++++++---- graph/model/models_gen.go | 6 + graph/schema.graphqls | 12 +- graph/schema.resolvers.go | 12 +- routes.go | 9 +- 7 files changed, 417 insertions(+), 51 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index 9ec40c8..fd6df5d 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/ClusterCockpit/cc-backend/graph/model" "github.com/ClusterCockpit/cc-backend/log" sq "github.com/Masterminds/squirrel" "github.com/golang-jwt/jwt/v4" @@ -233,6 +234,24 @@ func (auth *Authentication) FetchUser(username string) (*User, error) { return user, nil } +func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) { + me := GetUser(ctx) + if me != nil && !me.HasRole(RoleAdmin) && me.Username != username { + return nil, errors.New("forbidden") + } + + user := &model.User{Username: username} + if err := sq.Select("name", "email").From("user").Where("user.username = ?", username). + RunWith(db).QueryRow().Scan(&user.Name, &user.Email); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + + return nil, err + } + return user, nil +} + // Handle a POST request that should log the user in, starting a new session. func (auth *Authentication) Login(onsuccess http.Handler, onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { diff --git a/frontend b/frontend index 7dbabf1..c0d1eac 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit 7dbabf140d704b38a1fe29b8248e4226ce0c1b23 +Subproject commit c0d1eac12517fb4ab995324584ea9bc1eee39c94 diff --git a/graph/generated/generated.go b/graph/generated/generated.go index 877ec42..e891d46 100644 --- a/graph/generated/generated.go +++ b/graph/generated/generated.go @@ -103,6 +103,7 @@ type ComplexityRoot struct { SubCluster func(childComplexity int) int Tags func(childComplexity int) int User func(childComplexity int) int + UserData func(childComplexity int) int Walltime func(childComplexity int) int } @@ -182,6 +183,7 @@ type ComplexityRoot struct { NodeMetrics func(childComplexity int, cluster string, partition *string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) int RooflineHeatmap func(childComplexity int, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) int Tags func(childComplexity int) int + User func(childComplexity int, username string) int } Resource struct { @@ -236,14 +238,22 @@ type ComplexityRoot struct { Node func(childComplexity int) int Socket func(childComplexity int) int } + + User struct { + Email func(childComplexity int) int + Name func(childComplexity int) int + Username func(childComplexity int) int + } } type ClusterResolver interface { Partitions(ctx context.Context, obj *model.Cluster) ([]string, error) } type JobResolver interface { - MetaData(ctx context.Context, obj *schema.Job) (interface{}, error) Tags(ctx context.Context, obj *schema.Job) ([]*schema.Tag, error) + + MetaData(ctx context.Context, obj *schema.Job) (interface{}, error) + UserData(ctx context.Context, obj *schema.Job) (*model.User, error) } type MutationResolver interface { CreateTag(ctx context.Context, typeArg string, name string) (*schema.Tag, error) @@ -255,6 +265,7 @@ type MutationResolver interface { type QueryResolver interface { Clusters(ctx context.Context) ([]*model.Cluster, error) Tags(ctx context.Context) ([]*schema.Tag, error) + User(ctx context.Context, username string) (*model.User, error) Job(ctx context.Context, id string) (*schema.Job, error) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope) ([]*model.JobMetricWithName, error) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) ([]*model.MetricFootprints, error) @@ -539,6 +550,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Job.User(childComplexity), true + case "Job.userData": + if e.complexity.Job.UserData == nil { + break + } + + return e.complexity.Job.UserData(childComplexity), true + case "Job.walltime": if e.complexity.Job.Walltime == nil { break @@ -947,6 +965,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Tags(childComplexity), true + case "Query.user": + if e.complexity.Query.User == nil { + break + } + + args, err := ec.field_Query_user_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.User(childComplexity, args["username"].(string)), true + case "Resource.accelerators": if e.complexity.Resource.Accelerators == nil { break @@ -1171,6 +1201,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Topology.Socket(childComplexity), true + case "User.email": + if e.complexity.User.Email == nil { + break + } + + return e.complexity.User.Email(childComplexity), true + + case "User.name": + if e.complexity.User.Name == nil { + break + } + + return e.complexity.User.Name(childComplexity), true + + case "User.username": + if e.complexity.User.Username == nil { + break + } + + return e.complexity.User.Username(childComplexity), true + } return 0, false } @@ -1261,9 +1312,11 @@ type Job { arrayJobId: Int! monitoringStatus: Int! state: JobState! - metaData: Any tags: [Tag!]! resources: [Resource!]! + + metaData: Any + userData: User } type Cluster { @@ -1375,10 +1428,18 @@ type Count { count: Int! } +type User { + username: String! + name: String! + email: String! +} + type Query { clusters: [Cluster!]! # List of all clusters tags: [Tag!]! # List of all tags + user(username: String!): User + job(id: ID!): Job jobMetrics(id: ID!, metrics: [String!], scopes: [MetricScope!]): [JobMetricWithName!]! jobsFootprints(filter: [JobFilter!], metrics: [String!]!): [MetricFootprints]! @@ -1915,6 +1976,21 @@ func (ec *executionContext) field_Query_rooflineHeatmap_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Query_user_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["username"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("username")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["username"] = arg0 + return args, nil +} + func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3178,38 +3254,6 @@ func (ec *executionContext) _Job_state(ctx context.Context, field graphql.Collec return ec.marshalNJobState2githubᚗcomᚋClusterCockpitᚋccᚑbackendᚋschemaᚐJobState(ctx, field.Selections, res) } -func (ec *executionContext) _Job_metaData(ctx context.Context, field graphql.CollectedField, obj *schema.Job) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Job", - 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.Job().MetaData(rctx, obj) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(interface{}) - fc.Result = res - return ec.marshalOAny2interface(ctx, field.Selections, res) -} - func (ec *executionContext) _Job_tags(ctx context.Context, field graphql.CollectedField, obj *schema.Job) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3280,6 +3324,70 @@ func (ec *executionContext) _Job_resources(ctx context.Context, field graphql.Co return ec.marshalNResource2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋschemaᚐResourceᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _Job_metaData(ctx context.Context, field graphql.CollectedField, obj *schema.Job) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Job", + 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.Job().MetaData(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(interface{}) + fc.Result = res + return ec.marshalOAny2interface(ctx, field.Selections, res) +} + +func (ec *executionContext) _Job_userData(ctx context.Context, field graphql.CollectedField, obj *schema.Job) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Job", + 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.Job().UserData(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.User) + fc.Result = res + return ec.marshalOUser2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) +} + func (ec *executionContext) _JobMetric_unit(ctx context.Context, field graphql.CollectedField, obj *schema.JobMetric) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4697,6 +4805,45 @@ func (ec *executionContext) _Query_tags(ctx context.Context, field graphql.Colle return ec.marshalNTag2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋschemaᚐTagᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _Query_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Query_user_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().User(rctx, args["username"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.User) + fc.Result = res + return ec.marshalOUser2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_job(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6188,6 +6335,111 @@ func (ec *executionContext) _Topology_accelerators(ctx context.Context, field gr return ec.marshalOAccelerator2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋgraphᚋmodelᚐAcceleratorᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _User_username(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + 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.Username, 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.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _User_name(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + 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.Name, 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.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _User_email(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + 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.Email, 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.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -7954,17 +8206,6 @@ func (ec *executionContext) _Job(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } - case "metaData": - 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._Job_metaData(ctx, field, obj) - return res - }) case "tags": field := field out.Concurrently(i, func() (res graphql.Marshaler) { @@ -7984,6 +8225,28 @@ func (ec *executionContext) _Job(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } + case "metaData": + 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._Job_metaData(ctx, field, obj) + return res + }) + case "userData": + 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._Job_userData(ctx, field, obj) + return res + }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -8412,6 +8675,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) + case "user": + 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._Query_user(ctx, field) + return res + }) case "job": field := field out.Concurrently(i, func() (res graphql.Marshaler) { @@ -8817,6 +9091,43 @@ func (ec *executionContext) _Topology(ctx context.Context, sel ast.SelectionSet, return out } +var userImplementors = []string{"User"} + +func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *model.User) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, userImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("User") + case "username": + out.Values[i] = ec._User_username(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "name": + out.Values[i] = ec._User_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "email": + out.Values[i] = ec._User_email(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var __DirectiveImplementors = []string{"__Directive"} func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { @@ -10852,6 +11163,13 @@ func (ec *executionContext) unmarshalOTimeRange2ᚖgithubᚗcomᚋClusterCockpit return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalOUser2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋgraphᚋmodelᚐUser(ctx context.Context, sel ast.SelectionSet, v *model.User) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._User(ctx, sel, v) +} + func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/graph/model/models_gen.go b/graph/model/models_gen.go index 95f58b0..c94b908 100644 --- a/graph/model/models_gen.go +++ b/graph/model/models_gen.go @@ -161,6 +161,12 @@ type Topology struct { Accelerators []*Accelerator `json:"accelerators"` } +type User struct { + Username string `json:"username"` + Name string `json:"name"` + Email string `json:"email"` +} + type Aggregate string const ( diff --git a/graph/schema.graphqls b/graph/schema.graphqls index 9c0e341..7f7f60a 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -24,9 +24,11 @@ type Job { arrayJobId: Int! monitoringStatus: Int! state: JobState! - metaData: Any tags: [Tag!]! resources: [Resource!]! + + metaData: Any + userData: User } type Cluster { @@ -138,10 +140,18 @@ type Count { count: Int! } +type User { + username: String! + name: String! + email: String! +} + type Query { clusters: [Cluster!]! # List of all clusters tags: [Tag!]! # List of all tags + user(username: String!): User + job(id: ID!): Job jobMetrics(id: ID!, metrics: [String!], scopes: [MetricScope!]): [JobMetricWithName!]! jobsFootprints(filter: [JobFilter!], metrics: [String!]!): [MetricFootprints]! diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index fdd1937..c878950 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -22,12 +22,16 @@ func (r *clusterResolver) Partitions(ctx context.Context, obj *model.Cluster) ([ return r.Repo.Partitions(obj.Name) } +func (r *jobResolver) Tags(ctx context.Context, obj *schema.Job) ([]*schema.Tag, error) { + return r.Repo.GetTags(&obj.ID) +} + func (r *jobResolver) MetaData(ctx context.Context, obj *schema.Job) (interface{}, error) { return r.Repo.FetchMetadata(obj) } -func (r *jobResolver) Tags(ctx context.Context, obj *schema.Job) ([]*schema.Tag, error) { - return r.Repo.GetTags(&obj.ID) +func (r *jobResolver) UserData(ctx context.Context, obj *schema.Job) (*model.User, error) { + return auth.FetchUser(ctx, r.DB, obj.User) } func (r *mutationResolver) CreateTag(ctx context.Context, typeArg string, name string) (*schema.Tag, error) { @@ -102,6 +106,10 @@ func (r *queryResolver) Tags(ctx context.Context) ([]*schema.Tag, error) { return r.Repo.GetTags(nil) } +func (r *queryResolver) User(ctx context.Context, username string) (*model.User, error) { + return auth.FetchUser(ctx, r.DB, username) +} + func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error) { numericId, err := strconv.ParseInt(id, 10, 64) if err != nil { diff --git a/routes.go b/routes.go index 243a4e7..81669d4 100644 --- a/routes.go +++ b/routes.go @@ -92,8 +92,13 @@ func setupJobRoute(i InfoType, r *http.Request) InfoType { } func setupUserRoute(i InfoType, r *http.Request) InfoType { - i["id"] = mux.Vars(r)["id"] - i["username"] = mux.Vars(r)["id"] + username := mux.Vars(r)["id"] + i["id"] = username + i["username"] = username + if user, _ := auth.FetchUser(r.Context(), jobRepo.DB, username); user != nil { + i["name"] = user.Name + i["email"] = user.Email + } return i }