diff --git a/actions/validate.go b/actions/validate.go index 474f4aa0..cd46d166 100644 --- a/actions/validate.go +++ b/actions/validate.go @@ -26,6 +26,9 @@ func ValidateUpload(ctx context.Context, cfg config.Config, src io.Reader, feedU } } rturls = rturlsok + if len(rturls) > 3 { + rturls = rturls[0:3] + } if feedURL == nil || !checkurl(*feedURL) { feedURL = nil } @@ -74,7 +77,9 @@ func ValidateUpload(ctx context.Context, cfg config.Config, src io.Reader, feedU IncludeServiceLevels: true, IncludeRouteGeometries: true, IncludeEntities: true, - IncludeEntitiesLimit: 10000, + IncludeRealtimeJson: true, + IncludeEntitiesLimit: 10_000, + MaxRTMessageSize: 10_000_000, ValidateRealtimeMessages: rturls, } if cfg.ValidateLargeFiles { @@ -164,6 +169,12 @@ func ValidateUpload(ctx context.Context, cfg config.Config, src io.Reader, feedU for _, v := range r.Stops { result.Stops = append(result.Stops, model.Stop{Stop: v}) } + for _, v := range r.Realtime { + result.Realtime = append(result.Realtime, model.ValidationRealtimeResult{ + Url: v.Url, + Json: v.Json, + }) + } return &result, nil } diff --git a/auth/ancheck/jwt.go b/auth/ancheck/jwt.go index 6fefe4ec..5eb6ba44 100644 --- a/auth/ancheck/jwt.go +++ b/auth/ancheck/jwt.go @@ -14,7 +14,7 @@ import ( ) // JWTMiddleware checks and pulls user information from JWT in Authorization header. -func JWTMiddleware(jwtAudience string, jwtIssuer string, pubKeyPath string) (func(http.Handler) http.Handler, error) { +func JWTMiddleware(jwtAudience string, jwtIssuer string, pubKeyPath string, useEmailAsId bool) (func(http.Handler) http.Handler, error) { var verifyKey *rsa.PublicKey verifyBytes, err := ioutil.ReadFile(pubKeyPath) if err != nil { @@ -27,12 +27,22 @@ func JWTMiddleware(jwtAudience string, jwtIssuer string, pubKeyPath string) (fun return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if tokenString := strings.Split(r.Header.Get("Authorization"), "Bearer "); len(tokenString) == 2 { - jwtUser, err := validateJwt(verifyKey, jwtAudience, jwtIssuer, tokenString[1]) + claims, err := validateJwt(verifyKey, jwtAudience, jwtIssuer, tokenString[1]) if err != nil { log.Error().Err(err).Msgf("invalid jwt token") http.Error(w, util.MakeJsonError(http.StatusText(http.StatusUnauthorized)), http.StatusUnauthorized) return } + if claims == nil { + log.Error().Err(err).Msgf("no claims") + http.Error(w, util.MakeJsonError(http.StatusText(http.StatusUnauthorized)), http.StatusUnauthorized) + return + } + userId := claims.Subject + if useEmailAsId { + userId = claims.Email + } + jwtUser := authn.NewCtxUser(userId, claims.Subject, claims.Email) r = r.WithContext(authn.WithUser(r.Context(), jwtUser)) } next.ServeHTTP(w, r) @@ -49,7 +59,7 @@ func (c *CustomClaimsExample) Valid() error { return nil } -func validateJwt(rsaPublicKey *rsa.PublicKey, jwtAudience string, jwtIssuer string, tokenString string) (authn.User, error) { +func validateJwt(rsaPublicKey *rsa.PublicKey, jwtAudience string, jwtIssuer string, tokenString string) (*CustomClaimsExample, error) { // Parse the token token, err := jwt.ParseWithClaims(tokenString, &CustomClaimsExample{}, func(token *jwt.Token) (interface{}, error) { return rsaPublicKey, nil @@ -64,6 +74,5 @@ func validateJwt(rsaPublicKey *rsa.PublicKey, jwtAudience string, jwtIssuer stri if !claims.VerifyIssuer(jwtIssuer, true) { return nil, errors.New("invalid issuer") } - user := authn.NewCtxUser(claims.Email, claims.Subject, claims.Email) - return user, nil + return claims, nil } diff --git a/auth/ancheck/mw.go b/auth/ancheck/mw.go index 11919e83..57b1d702 100644 --- a/auth/ancheck/mw.go +++ b/auth/ancheck/mw.go @@ -20,6 +20,7 @@ type AuthConfig struct { JwtAudience string JwtIssuer string JwtPublicKeyFile string + JwtUseEmailAsId bool UserHeader string } @@ -32,7 +33,7 @@ func GetUserMiddleware(authType string, cfg AuthConfig, client *redis.Client) (M case "user": return UserDefaultMiddleware(cfg.DefaultUsername), nil case "jwt": - return JWTMiddleware(cfg.JwtAudience, cfg.JwtIssuer, cfg.JwtPublicKeyFile) + return JWTMiddleware(cfg.JwtAudience, cfg.JwtIssuer, cfg.JwtPublicKeyFile, cfg.JwtUseEmailAsId) case "header": return UserHeaderMiddleware(cfg.UserHeader) case "kong": diff --git a/go.mod b/go.mod index b009c2a9..43c39ca1 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 github.com/graph-gophers/dataloader/v7 v7.1.0 github.com/hypirion/go-filecache v0.0.0-20160810125507-e3e6ef6981f0 - github.com/interline-io/transitland-lib v0.12.1-0.20230707231304-e93d39aa84b9 + github.com/interline-io/transitland-lib v0.13.1-0.20231009233939-3f7b51b007d9 github.com/jellydator/ttlcache/v2 v2.11.1 github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.7 diff --git a/go.sum b/go.sum index 3a78cb48..0383df01 100644 --- a/go.sum +++ b/go.sum @@ -261,8 +261,10 @@ github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6 github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/interline-io/transitland-lib v0.12.1-0.20230707231304-e93d39aa84b9 h1:h9rkgNVrOfxdAQ6/3CL+dqU6sMCX5AdmsqccvEtG3kc= -github.com/interline-io/transitland-lib v0.12.1-0.20230707231304-e93d39aa84b9/go.mod h1:EnN1BuWqBAQzyIBOH+Ait7S8i4uC5fLqGzmX8FmQ2a8= +github.com/interline-io/transitland-lib v0.13.1-0.20231007011544-9b1f7cba6634 h1:ICaVmw8XTl9Idpzabg3HtwHASkGlhRdDlqDQHmKk4gU= +github.com/interline-io/transitland-lib v0.13.1-0.20231007011544-9b1f7cba6634/go.mod h1:EnN1BuWqBAQzyIBOH+Ait7S8i4uC5fLqGzmX8FmQ2a8= +github.com/interline-io/transitland-lib v0.13.1-0.20231009233939-3f7b51b007d9 h1:gRAYhdeSN7GsDLSZvKRMNzpmAdulMKMQz8T4LxbFFps= +github.com/interline-io/transitland-lib v0.13.1-0.20231009233939-3f7b51b007d9/go.mod h1:EnN1BuWqBAQzyIBOH+Ait7S8i4uC5fLqGzmX8FmQ2a8= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= diff --git a/internal/generated/gqlout/generated.go b/internal/generated/gqlout/generated.go index 6e0b8d55..a76b0165 100644 --- a/internal/generated/gqlout/generated.go +++ b/internal/generated/gqlout/generated.go @@ -924,6 +924,11 @@ type ComplexityRoot struct { WheelchairAccessible func(childComplexity int) int } + ValidationRealtimeResult struct { + Json func(childComplexity int) int + Url func(childComplexity int) int + } + ValidationResult struct { Agencies func(childComplexity int, limit *int) int EarliestCalendarDate func(childComplexity int) int @@ -932,6 +937,7 @@ type ComplexityRoot struct { FeedInfos func(childComplexity int, limit *int) int Files func(childComplexity int) int LatestCalendarDate func(childComplexity int) int + Realtime func(childComplexity int) int Routes func(childComplexity int, limit *int) int ServiceLevels func(childComplexity int, limit *int, routeID *string) int Sha1 func(childComplexity int) int @@ -5765,6 +5771,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Trip.WheelchairAccessible(childComplexity), true + case "ValidationRealtimeResult.json": + if e.complexity.ValidationRealtimeResult.Json == nil { + break + } + + return e.complexity.ValidationRealtimeResult.Json(childComplexity), true + + case "ValidationRealtimeResult.url": + if e.complexity.ValidationRealtimeResult.Url == nil { + break + } + + return e.complexity.ValidationRealtimeResult.Url(childComplexity), true + case "ValidationResult.agencies": if e.complexity.ValidationResult.Agencies == nil { break @@ -5824,6 +5844,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ValidationResult.LatestCalendarDate(childComplexity), true + case "ValidationResult.realtime": + if e.complexity.ValidationResult.Realtime == nil { + break + } + + return e.complexity.ValidationResult.Realtime(childComplexity), true + case "ValidationResult.routes": if e.complexity.ValidationResult.Routes == nil { break @@ -7471,6 +7498,12 @@ type ValidationResult { routes(limit: Int): [Route!]! stops(limit: Int): [Stop!]! feed_infos(limit: Int): [FeedInfo!]! + realtime: [ValidationRealtimeResult!] +} + +type ValidationRealtimeResult { + url: String! + json: Map } type ValidationResultErrorGroup { @@ -27375,6 +27408,8 @@ func (ec *executionContext) fieldContext_Mutation_validate_gtfs(ctx context.Cont return ec.fieldContext_ValidationResult_stops(ctx, field) case "feed_infos": return ec.fieldContext_ValidationResult_feed_infos(ctx, field) + case "realtime": + return ec.fieldContext_ValidationResult_realtime(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ValidationResult", field.Name) }, @@ -40057,6 +40092,91 @@ func (ec *executionContext) fieldContext_Trip_alerts(ctx context.Context, field return fc, nil } +func (ec *executionContext) _ValidationRealtimeResult_url(ctx context.Context, field graphql.CollectedField, obj *model.ValidationRealtimeResult) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ValidationRealtimeResult_url(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Url, 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) fieldContext_ValidationRealtimeResult_url(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ValidationRealtimeResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ValidationRealtimeResult_json(ctx context.Context, field graphql.CollectedField, obj *model.ValidationRealtimeResult) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ValidationRealtimeResult_json(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Json, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(map[string]any) + fc.Result = res + return ec.marshalOMap2map(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ValidationRealtimeResult_json(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ValidationRealtimeResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Map does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _ValidationResult_success(ctx context.Context, field graphql.CollectedField, obj *model.ValidationResult) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ValidationResult_success(ctx, field) if err != nil { @@ -40934,6 +41054,53 @@ func (ec *executionContext) fieldContext_ValidationResult_feed_infos(ctx context return fc, nil } +func (ec *executionContext) _ValidationResult_realtime(ctx context.Context, field graphql.CollectedField, obj *model.ValidationResult) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ValidationResult_realtime(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Realtime, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]model.ValidationRealtimeResult) + fc.Result = res + return ec.marshalOValidationRealtimeResult2ᚕgithubᚗcomᚋinterlineᚑioᚋtransitlandᚑserverᚋmodelᚐValidationRealtimeResultᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ValidationResult_realtime(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ValidationResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "url": + return ec.fieldContext_ValidationRealtimeResult_url(ctx, field) + case "json": + return ec.fieldContext_ValidationRealtimeResult_json(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ValidationRealtimeResult", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _ValidationResultError_filename(ctx context.Context, field graphql.CollectedField, obj *model.ValidationResultError) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ValidationResultError_filename(ctx, field) if err != nil { @@ -51789,6 +51956,38 @@ func (ec *executionContext) _Trip(ctx context.Context, sel ast.SelectionSet, obj return out } +var validationRealtimeResultImplementors = []string{"ValidationRealtimeResult"} + +func (ec *executionContext) _ValidationRealtimeResult(ctx context.Context, sel ast.SelectionSet, obj *model.ValidationRealtimeResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, validationRealtimeResultImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ValidationRealtimeResult") + case "url": + + out.Values[i] = ec._ValidationRealtimeResult_url(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "json": + + out.Values[i] = ec._ValidationRealtimeResult_json(ctx, field, obj) + + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var validationResultImplementors = []string{"ValidationResult"} func (ec *executionContext) _ValidationResult(ctx context.Context, sel ast.SelectionSet, obj *model.ValidationResult) graphql.Marshaler { @@ -51884,6 +52083,10 @@ func (ec *executionContext) _ValidationResult(ctx context.Context, sel ast.Selec if out.Values[i] == graphql.Null { invalids++ } + case "realtime": + + out.Values[i] = ec._ValidationResult_realtime(ctx, field, obj) + default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -54428,6 +54631,10 @@ func (ec *executionContext) marshalNTrip2ᚖgithubᚗcomᚋinterlineᚑioᚋtran return ec._Trip(ctx, sel, v) } +func (ec *executionContext) marshalNValidationRealtimeResult2githubᚗcomᚋinterlineᚑioᚋtransitlandᚑserverᚋmodelᚐValidationRealtimeResult(ctx context.Context, sel ast.SelectionSet, v model.ValidationRealtimeResult) graphql.Marshaler { + return ec._ValidationRealtimeResult(ctx, sel, &v) +} + func (ec *executionContext) marshalNValidationResultError2ᚕᚖgithubᚗcomᚋinterlineᚑioᚋtransitlandᚑserverᚋmodelᚐValidationResultErrorᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.ValidationResultError) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -57267,6 +57474,53 @@ func (ec *executionContext) marshalOUpload2ᚖgithubᚗcomᚋ99designsᚋgqlgen return res } +func (ec *executionContext) marshalOValidationRealtimeResult2ᚕgithubᚗcomᚋinterlineᚑioᚋtransitlandᚑserverᚋmodelᚐValidationRealtimeResultᚄ(ctx context.Context, sel ast.SelectionSet, v []model.ValidationRealtimeResult) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNValidationRealtimeResult2githubᚗcomᚋinterlineᚑioᚋtransitlandᚑserverᚋmodelᚐValidationRealtimeResult(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalOValidationResult2ᚖgithubᚗcomᚋinterlineᚑioᚋtransitlandᚑserverᚋmodelᚐValidationResult(ctx context.Context, sel ast.SelectionSet, v *model.ValidationResult) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/model/models.go b/model/models.go index dd4f280a..84baadbf 100644 --- a/model/models.go +++ b/model/models.go @@ -275,6 +275,12 @@ type ValidationResult struct { Routes []Route `json:"routes"` Stops []Stop `json:"stops"` FeedInfos []FeedInfo `json:"feed_infos"` + Realtime []ValidationRealtimeResult `json:"realtime"` +} + +type ValidationRealtimeResult struct { + Url string `json:"url"` + Json map[string]any `json:"json"` } type ValidationResultError struct { diff --git a/schema/schema.graphqls b/schema/schema.graphqls index 2de59f17..b6c1b73d 100644 --- a/schema/schema.graphqls +++ b/schema/schema.graphqls @@ -732,6 +732,12 @@ type ValidationResult { routes(limit: Int): [Route!]! stops(limit: Int): [Stop!]! feed_infos(limit: Int): [FeedInfo!]! + realtime: [ValidationRealtimeResult!] +} + +type ValidationRealtimeResult { + url: String! + json: Map } type ValidationResultErrorGroup { diff --git a/server/server_cmd.go b/server/server_cmd.go index 7da66967..596fd3ba 100644 --- a/server/server_cmd.go +++ b/server/server_cmd.go @@ -95,6 +95,7 @@ func (cmd *Command) Parse(args []string) error { fl.StringVar(&cmd.AuthConfig.JwtAudience, "jwt-audience", "", "JWT Audience (use with -auth=jwt)") fl.StringVar(&cmd.AuthConfig.JwtIssuer, "jwt-issuer", "", "JWT Issuer (use with -auth=jwt)") fl.StringVar(&cmd.AuthConfig.JwtPublicKeyFile, "jwt-public-key-file", "", "Path to JWT public key file (use with -auth=jwt)") + fl.BoolVar(&cmd.AuthConfig.JwtUseEmailAsId, "jwt-use-email-as-id", false, "JWT use claim email as user id") fl.StringVar(&cmd.AuthConfig.GatekeeperEndpoint, "gatekeeper-endpoint", "", "Gatekeeper endpoint (use with -auth=gatekeeper)") fl.StringVar(&cmd.AuthConfig.GatekeeperRoleSelector, "gatekeeper-selector", "", "Gatekeeper role selector (use with -auth=gatekeeper)") fl.StringVar(&cmd.AuthConfig.GatekeeperExternalIDSelector, "gatekeeper-eid-selector", "", "Gatekeeper External ID selector (use with -auth=gatekeeper)")