From 59ec77e2049bb800d0da45f77cfbb4c42c8f41e0 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Fri, 14 Jan 2022 22:19:25 +0100 Subject: [PATCH] Refactor JWT implementation --- app/app.go | 6 + app/boot_levels.go | 31 +- app/servers.go | 8 +- auth/auth.go | 11 +- auth/commands/commands.go | 12 +- auth/handlers/handle_oauth2.go | 5 +- auth/handlers/routes.go | 4 +- auth/oauth2/access_token.go | 62 ++++ auth/oauth2/client.go | 3 +- auth/oauth2/corteza_token_store.go | 107 ++++--- auth/oauth2/jwt_access.go | 58 ---- auth/oauth2/oauth2.go | 6 +- auth/request/context.go | 18 -- auth/request/corteza_session_store.go | 10 +- automation/rest/router.go | 26 +- compose/rest/router.go | 59 ++-- compose/types/locale.gen.go | 3 +- compose/types/type_labels.gen.go | 16 +- federation/rest/router.go | 29 +- federation/service/node.go | 8 +- federation/service/service.go | 2 +- go.mod | 16 +- go.sum | 17 ++ pkg/api/server/server.go | 8 +- pkg/auth/errors.go | 17 ++ pkg/auth/extra.go | 31 ++ pkg/auth/interfaces.go | 23 +- pkg/auth/jwt.go | 424 +++++++++++++++----------- pkg/auth/middleware.go | 104 ++++--- pkg/corredor/service.go | 4 +- pkg/websocket/session_test.go | 16 +- store/auth_oa2tokens.gen.go | 7 + store/auth_oa2tokens.yaml | 1 + store/rdbms/auth_oa2tokens.gen.go | 8 + system/commands/commands.go | 1 + system/rest/auth.go | 8 +- system/rest/router.go | 62 ++-- system/types/auth_oa2token.go | 3 +- tests/apigw/main_test.go | 4 +- tests/automation/main_test.go | 4 +- tests/compose/main_test.go | 4 +- tests/federation/main_test.go | 4 +- tests/helpers/auth.go | 4 +- tests/reporter/main_test.go | 4 +- tests/system/main_test.go | 4 +- tests/system/static/idp_to_sp.cookie | 2 +- tests/system/static/idp_to_sp.post | 2 +- 47 files changed, 730 insertions(+), 536 deletions(-) create mode 100644 auth/oauth2/access_token.go delete mode 100644 auth/oauth2/jwt_access.go create mode 100644 pkg/auth/extra.go diff --git a/app/app.go b/app/app.go index 6eee6594d..499b050d4 100644 --- a/app/app.go +++ b/app/app.go @@ -5,11 +5,13 @@ import ( "net/http" "github.com/cortezaproject/corteza-server/auth/settings" + "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/pkg/plugin" "github.com/cortezaproject/corteza-server/store" "github.com/go-chi/chi/v5" + "github.com/go-oauth2/oauth2/v4" "github.com/spf13/cobra" "go.uber.org/zap" "google.golang.org/grpc" @@ -62,6 +64,10 @@ type ( // CLI Commands Command *cobra.Command + jwt auth.MiddlewareValidator + + oa2m oauth2.Manager + // Servers HttpServer httpApiServer GrpcServer grpcServer diff --git a/app/boot_levels.go b/app/boot_levels.go index 78c8c8905..03b8c8daf 100644 --- a/app/boot_levels.go +++ b/app/boot_levels.go @@ -9,6 +9,7 @@ import ( authService "github.com/cortezaproject/corteza-server/auth" authHandlers "github.com/cortezaproject/corteza-server/auth/handlers" + "github.com/cortezaproject/corteza-server/auth/oauth2" "github.com/cortezaproject/corteza-server/auth/saml" authSettings "github.com/cortezaproject/corteza-server/auth/settings" autService "github.com/cortezaproject/corteza-server/automation/service" @@ -120,13 +121,6 @@ func (app *CortezaApp) Setup() (err error) { } } - // set base path for links&routes in auth server - authHandlers.BasePath = app.Opt.HTTPServer.BaseUrl - - if err = auth.SetupDefault(app.Opt.Auth.Secret, app.Opt.Auth.Expiry); err != nil { - return - } - http.SetupDefaults( app.Opt.HTTPClient.HttpClientTimeout, app.Opt.HTTPClient.ClientTSLInsecure, @@ -323,6 +317,24 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { return } + { + app.oa2m = oauth2.NewManager( + app.Opt.Auth, + app.Log, + &oauth2.ContextClientStore{}, + &oauth2.CortezaTokenStore{Store: app.Store}, + ) + + // set base path for links&routes in auth server + authHandlers.BasePath = app.Opt.HTTPServer.BaseUrl + + if err = auth.SetupDefault(app.oa2m, app.Opt.Auth.Secret, app.Opt.Auth.Expiry); err != nil { + return + } + + app.jwt = auth.JWT() + } + app.WsServer = websocket.Server(app.Log, app.Opt.Websocket) ctx = actionlog.RequestOriginToContext(ctx, actionlog.RequestOrigin_APP_Init) @@ -402,7 +414,8 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { return } - auth.SetJWTStore(app.Store) + //@todo remove vv + //auth.SetJWTStore(app.Store) corredor.Service().SetUserFinder(sysService.DefaultUser) corredor.Service().SetRoleFinder(sysService.DefaultRole) @@ -498,7 +511,7 @@ func (app *CortezaApp) Activate(ctx context.Context) (err error) { updateSmtpSettings(app.Log, sysService.CurrentSettings) - if app.AuthService, err = authService.New(ctx, app.Log, app.Store, app.Opt.Auth); err != nil { + if app.AuthService, err = authService.New(ctx, app.Log, app.oa2m, app.Store, app.Opt.Auth); err != nil { return fmt.Errorf("failed to init auth service: %w", err) } diff --git a/app/servers.go b/app/servers.go index 9d48aad46..05cd89ce5 100644 --- a/app/servers.go +++ b/app/servers.go @@ -95,13 +95,13 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) { zap.String("baseUrl", fullpathAPI), ) - r.Route("/system", systemRest.MountRoutes) - r.Route("/automation", automationRest.MountRoutes) - r.Route("/compose", composeRest.MountRoutes) + r.Route("/system", systemRest.MountRoutes(app.jwt)) + r.Route("/automation", automationRest.MountRoutes(app.jwt)) + r.Route("/compose", composeRest.MountRoutes(app.jwt)) r.Route("/websocket", app.WsServer.MountRoutes) if app.Opt.Federation.Enabled { - r.Route("/federation", federationRest.MountRoutes) + r.Route("/federation", federationRest.MountRoutes(app.jwt)) } var fullpathDocs = options.CleanBase(ho.BaseUrl, ho.ApiBaseUrl, "docs") diff --git a/auth/auth.go b/auth/auth.go index 84a9f7283..4f0393008 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -51,7 +51,7 @@ type ( var PublicAssets embed.FS // New initializes Auth service that orchestrates session manager, oauth2 manager and http request handlers -func New(ctx context.Context, log *zap.Logger, s store.Storer, opt options.AuthOpt) (svc *service, err error) { +func New(ctx context.Context, log *zap.Logger, oa2m oauth2def.Manager, s store.Storer, opt options.AuthOpt) (svc *service, err error) { var ( tpls templateExecutor defClient *types.AuthClient @@ -73,14 +73,7 @@ func New(ctx context.Context, log *zap.Logger, s store.Storer, opt options.AuthO sesManager := request.NewSessionManager(s, opt, log) - oauth2Manager := oauth2.NewManager( - opt, - log, - &oauth2.ContextClientStore{}, - &oauth2.CortezaTokenStore{Store: s}, - ) - - oauth2Server := oauth2.NewServer(oauth2Manager) + oauth2Server := oauth2.NewServer(oa2m) // Called after oauth2 authorization request is validated // We'll try to get valid user out of the session or redirect user to login page diff --git a/auth/commands/commands.go b/auth/commands/commands.go index f434c8f39..04a38a344 100644 --- a/auth/commands/commands.go +++ b/auth/commands/commands.go @@ -91,9 +91,9 @@ func Command(ctx context.Context, app serviceInitializer, storeInit func(ctx con Run: func(cmd *cobra.Command, args []string) { ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser()) var ( - at []byte - user *types.User - err error + signedToken []byte + user *types.User + err error userStr = args[0] ) @@ -104,15 +104,15 @@ func Command(ctx context.Context, app serviceInitializer, storeInit func(ctx con err = service.DefaultAuth.LoadRoleMemberships(ctx, user) cli.HandleError(err) - at, err = auth.DefaultJwtHandler.Generate(ctx, user, 0) + signedToken, err = auth.JWT().Generate(ctx, user, 0, "api", "profile") cli.HandleError(err) - cmd.Println(string(at)) + cmd.Println(string(signedToken)) }, } testEmails := &cobra.Command{ Use: "test-notifications [recipient]", - Short: "Sends samples of all authentication notification to receipient", + Short: "Sends samples of all authentication notification to recipient", Args: cobra.ExactArgs(1), PreRunE: commandPreRunInitService(app), Run: func(cmd *cobra.Command, args []string) { diff --git a/auth/handlers/handle_oauth2.go b/auth/handlers/handle_oauth2.go index d47b8f191..c2f18ed8f 100644 --- a/auth/handlers/handle_oauth2.go +++ b/auth/handlers/handle_oauth2.go @@ -49,8 +49,7 @@ func (h AuthHandlers) oauth2Authorize(req *request.AuthReq) (err error) { return err } - // add client to context so we can reach it from client store via context.Value() fn - // + // add client to context, now we can reach it from client store via context.Value() fn // this way we work around the limitations we have with the oauth2 lib. ctx = context.WithValue(req.Context(), &oauth2.ContextClientStore{}, client) @@ -60,7 +59,7 @@ func (h AuthHandlers) oauth2Authorize(req *request.AuthReq) (err error) { request.SetOauth2Client(req.Session, client) } - // set to -1 to make sure that wrapping request handler + // set to -1 to make sure wrapping request handler // does not send status code! req.Status = -1 diff --git a/auth/handlers/routes.go b/auth/handlers/routes.go index d49c720c8..2455d1022 100644 --- a/auth/handlers/routes.go +++ b/auth/handlers/routes.go @@ -3,8 +3,8 @@ package handlers import ( "net/http" - "github.com/cortezaproject/corteza-server/auth/request" "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/locale" "github.com/go-chi/chi/v5" "github.com/go-chi/httprate" @@ -36,7 +36,7 @@ func (h *AuthHandlers) MountHttpRoutes(r chi.Router) { r.Use(httprate.LimitByIP(h.Opt.RequestRateLimit, h.Opt.RequestRateWindowLength)) } - r.Use(request.ExtraReqInfoMiddleware) + r.Use(auth.ExtraReqInfoMiddleware) r.Group(func(r chi.Router) { // all routes protected with CSRF: diff --git a/auth/oauth2/access_token.go b/auth/oauth2/access_token.go new file mode 100644 index 000000000..223ce9561 --- /dev/null +++ b/auth/oauth2/access_token.go @@ -0,0 +1,62 @@ +package oauth2 + +//import ( +// "context" +// "strings" +// +// "github.com/cortezaproject/corteza-server/pkg/auth" +// "github.com/cortezaproject/corteza-server/pkg/payload" +// "github.com/cortezaproject/corteza-server/pkg/rand" +// "github.com/go-oauth2/oauth2/v4" +// "github.com/spf13/cast" +//) +// +//// JWTAccessGenerate generate the jwt access token +//type ( +// tokenGenerator interface { +// Generate(ctx context.Context, i auth.Identifiable, clientID uint64, scope ...string) (token []byte, err error) +// } +// +// JWTAccessGenerate struct { +// tm tokenGenerator +// } +//) +// +//// NewJWTAccessGenerate create to generate the jwt access token instance +//// +//// @todo move this to pkg/auth (??) so it can be re-used +//func NewJWTAccessGenerate(tg tokenGenerator) *JWTAccessGenerate { +// return &JWTAccessGenerate{tg} +//} +// +//// Token based on the UUID generated token +//func (a *JWTAccessGenerate) Token(ctx context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (_ string, refresh string, err error) { +// var ( +// user auth.Identifiable +// rawToken []byte +// ) +// +// { +// // extract user ID and roles from a space-delimited list of IDs stored in userID +// userIdWithRoles := strings.Split(data.TokenInfo.GetUserID(), " ") +// if len(userIdWithRoles) == 1 { +// user = auth.Authenticated(cast.ToUint64(userIdWithRoles[0])) +// } else { +// user = auth.Authenticated( +// cast.ToUint64(userIdWithRoles[0]), +// payload.ParseUint64s(userIdWithRoles)..., +// ) +// } +// } +// +// rawToken, err = a.tm.Generate(ctx, user, cast.ToUint64(data.Client.GetID()), data.TokenInfo.GetScope()) +// if err != nil { +// return +// } +// +// if isGenRefresh { +// refresh = string(rand.Bytes(48)) +// } +// +// return string(rawToken), refresh, nil +//} diff --git a/auth/oauth2/client.go b/auth/oauth2/client.go index 5bcf5eec6..a6715fb89 100644 --- a/auth/oauth2/client.go +++ b/auth/oauth2/client.go @@ -19,7 +19,8 @@ var _ oauth2.ClientStore = &ContextClientStore{} // // This requires that client is put in context before oauth2 procedures are executed! func (s ContextClientStore) GetByID(ctx context.Context, id string) (oauth2.ClientInfo, error) { - return &clientInfo{ctx.Value(&ContextClientStore{}).(*types.AuthClient)}, nil + return &clientInfo{&types.AuthClient{}}, nil + //return &clientInfo{ctx.Value(&ContextClientStore{}).(*types.AuthClient)}, nil } type ( diff --git a/auth/oauth2/corteza_token_store.go b/auth/oauth2/corteza_token_store.go index 52873c596..851f9b343 100644 --- a/auth/oauth2/corteza_token_store.go +++ b/auth/oauth2/corteza_token_store.go @@ -7,7 +7,6 @@ import ( "strconv" "time" - "github.com/cortezaproject/corteza-server/auth/request" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/id" @@ -44,23 +43,36 @@ var ( func (c CortezaTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) (err error) { var ( - eti = request.GetExtraReqInfoFromContext(ctx) - oa2t = &types.AuthOa2token{ - ID: nextID(), - CreatedAt: *now(), - RemoteAddr: eti.RemoteAddr, - UserAgent: eti.UserAgent, - } + oa2t *types.AuthOa2token + acc *types.AuthConfirmedClient - acc = &types.AuthConfirmedClient{ - ConfirmedAt: oa2t.CreatedAt, - } + userID uint64 + clientID uint64 + + jwtID = id.Next() ) + if clientID, err = strconv.ParseUint(info.GetClientID(), 10, 64); err != nil { + return fmt.Errorf("could not parse client ID from token info: %w", err) + } + + if userID, _ = auth.ExtractFromSubClaim(info.GetUserID()); userID == 0 { + return fmt.Errorf("could not parse user ID from token info") + } + + // Make oauth2 token and auth confirmation structs from user and client IDs + if oa2t, acc, err = makeAuthStructs(ctx, jwtID, userID, clientID, info, info.GetCodeExpiresIn()); err != nil { + return + } + + // this is oauth2 specific go-code and there is no + // need for it to be moved to MakeAuthStructs fn in auth pkg if code := info.GetCode(); code != "" { oa2t.Code = code - oa2t.ExpiresAt = info.GetCodeCreateAt().Add(info.GetCodeExpiresIn()) } else { + // When creating non-access-code tokens, + // we need to overwrite expiration time + // with custom values for access or refresh token oa2t.Access = info.GetAccess() oa2t.ExpiresAt = info.GetAccessCreateAt().Add(info.GetAccessExpiresIn()) @@ -70,32 +82,28 @@ func (c CortezaTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) (e } } - if oa2t.Data, err = json.Marshal(info); err != nil { - return - } - - if oa2t.ClientID, err = strconv.ParseUint(info.GetClientID(), 10, 64); err != nil { - return fmt.Errorf("could not parse client ID from token info: %w", err) - } - - // copy client id to auth client confirmation - acc.ClientID = oa2t.ClientID - - if info.GetUserID() != "" { - if oa2t.UserID, _ = auth.ExtractFromSubClaim(info.GetUserID()); oa2t.UserID == 0 { - // UserID stores collection of IDs: user's ID and set of all roles user is member of - return fmt.Errorf("could not parse user ID from token info") - } - } - - // copy user id to auth client confirmation - acc.UserID = oa2t.UserID + //if oa2t.ClientID, err = strconv.ParseUint(info.GetClientID(), 10, 64); err != nil { + // return fmt.Errorf("could not parse client ID from token info: %w", err) + //} + //if info.GetUserID() != "" { + // if oa2t.UserID, _ = auth.ExtractFromSubClaim(info.GetUserID()); oa2t.UserID == 0 { + // // UserID stores collection of IDs: user's ID and set of all roles user is member of + // return fmt.Errorf("could not parse user ID from token info") + // } + //} + // + //// copy user id to auth client confirmation + //acc.UserID = oa2t.UserID if err = store.UpsertAuthConfirmedClient(ctx, c.Store, acc); err != nil { return } - return store.CreateAuthOa2token(ctx, c.Store, oa2t) + if err = store.CreateAuthOa2token(ctx, c.Store, oa2t); err != nil { + return + } + + return nil } func (c CortezaTokenStore) RemoveByCode(ctx context.Context, code string) error { @@ -115,8 +123,8 @@ func (c CortezaTokenStore) GetByCode(ctx context.Context, code string) (oauth2.T internal = &oauth2models.Token{} t, err = store.LookupAuthOa2tokenByCode(ctx, c.Store, code) ) - if err != nil { + if err != nil { if errors.IsNotFound(err) { return nil, oauth2errors.ErrInvalidAuthorizeCode } @@ -132,6 +140,7 @@ func (c CortezaTokenStore) GetByAccess(ctx context.Context, access string) (oaut internal = &oauth2models.Token{} t, err = store.LookupAuthOa2tokenByAccess(ctx, c.Store, access) ) + if err != nil { return nil, fmt.Errorf("failed to get access token: %w", err) } @@ -144,6 +153,7 @@ func (c CortezaTokenStore) GetByRefresh(ctx context.Context, refresh string) (oa internal = &oauth2models.Token{} t, err = store.LookupAuthOa2tokenByRefresh(ctx, c.Store, refresh) ) + if err != nil { if errors.IsNotFound(err) { return nil, oauth2errors.ErrInvalidRefreshToken @@ -158,3 +168,32 @@ func (c CortezaTokenStore) GetByRefresh(ctx context.Context, refresh string) (oa return internal, t.Data.Unmarshal(internal) } + +func makeAuthStructs(ctx context.Context, jwtID, userID, clientID uint64, data oauth2.TokenInfo, expiresAt time.Duration) (oa2t *types.AuthOa2token, acc *types.AuthConfirmedClient, err error) { + var ( + eti = auth.GetExtraReqInfoFromContext(ctx) + createdAt = time.Now().Round(time.Second) + ) + + oa2t = &types.AuthOa2token{ + ID: jwtID, + CreatedAt: createdAt, + RemoteAddr: eti.RemoteAddr, + UserAgent: eti.UserAgent, + ClientID: clientID, + UserID: userID, + ExpiresAt: createdAt.Add(expiresAt), + } + + acc = &types.AuthConfirmedClient{ + ClientID: clientID, + UserID: userID, + ConfirmedAt: createdAt, + } + + if oa2t.Data, err = json.Marshal(data); err != nil { + return nil, nil, err + } + + return +} diff --git a/auth/oauth2/jwt_access.go b/auth/oauth2/jwt_access.go deleted file mode 100644 index f2615fbfb..000000000 --- a/auth/oauth2/jwt_access.go +++ /dev/null @@ -1,58 +0,0 @@ -package oauth2 - -import ( - "context" - "strings" - - "github.com/cortezaproject/corteza-server/pkg/auth" - "github.com/cortezaproject/corteza-server/pkg/payload" - "github.com/cortezaproject/corteza-server/pkg/rand" - "github.com/go-oauth2/oauth2/v4" - "github.com/spf13/cast" -) - -// JWTAccessGenerate generate the jwt access token -type ( - JWTAccessGenerate struct { - tm auth.TokenGenerator - } -) - -// NewJWTAccessGenerate create to generate the jwt access token instance -// -// @todo move this to pkg/auth (??) so it can be re-used -func NewJWTAccessGenerate(tg auth.TokenGenerator) *JWTAccessGenerate { - return &JWTAccessGenerate{tg} -} - -// Token based on the UUID generated token -func (a *JWTAccessGenerate) Token(_ context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (_ string, refresh string, err error) { - var ( - user auth.Identifiable - rawToken []byte - ) - - { - // extract user ID and roles from a space-delimited list of IDs stored in userID - userIdWithRoles := strings.Split(data.TokenInfo.GetUserID(), " ") - if len(userIdWithRoles) == 1 { - user = auth.Authenticated(cast.ToUint64(userIdWithRoles[0])) - } else { - user = auth.Authenticated( - cast.ToUint64(userIdWithRoles[0]), - payload.ParseUint64s(userIdWithRoles)..., - ) - } - } - - rawToken, err = a.tm.Encode(user, cast.ToUint64(data.Client.GetID()), data.TokenInfo.GetScope()) - if err != nil { - return - } - - if isGenRefresh { - refresh = string(rand.Bytes(48)) - } - - return string(rawToken), refresh, nil -} diff --git a/auth/oauth2/oauth2.go b/auth/oauth2/oauth2.go index ed499f1db..28d878fff 100644 --- a/auth/oauth2/oauth2.go +++ b/auth/oauth2/oauth2.go @@ -3,7 +3,6 @@ package oauth2 import ( "strings" - "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/go-oauth2/oauth2/v4" @@ -30,9 +29,6 @@ func NewManager(opt options.AuthOpt, log *zap.Logger, cs oauth2.ClientStore, ts // token store manager.MapTokenStorage(ts) - - // generate jwt access token - manager.MapAccessGenerate(NewJWTAccessGenerate(auth.DefaultJwtHandler)) manager.MapClientStorage(cs) manager.SetValidateURIHandler(func(baseURI, redirectURI string) (err error) { @@ -67,7 +63,7 @@ func NewManager(opt options.AuthOpt, log *zap.Logger, cs oauth2.ClientStore, ts return manager } -func NewServer(manager *manage.Manager) *server.Server { +func NewServer(manager oauth2.Manager) *server.Server { srv := server.NewServer(&server.Config{ TokenType: "Bearer", AllowGetAccessRequest: false, diff --git a/auth/request/context.go b/auth/request/context.go index 2c958ad71..ae081a79e 100644 --- a/auth/request/context.go +++ b/auth/request/context.go @@ -134,21 +134,3 @@ func (req *AuthReq) PopKV() map[string]string { req.SetKV(nil) return val } - -func ExtraReqInfoMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ExtraReqInfo{}, ExtraReqInfo{ - RemoteAddr: r.RemoteAddr, - UserAgent: r.UserAgent(), - }))) - }) -} - -func GetExtraReqInfoFromContext(ctx context.Context) ExtraReqInfo { - eti := ctx.Value(ExtraReqInfo{}) - if eti != nil { - return eti.(ExtraReqInfo) - } else { - return ExtraReqInfo{} - } -} diff --git a/auth/request/corteza_session_store.go b/auth/request/corteza_session_store.go index 2518069e3..a5c38ba01 100644 --- a/auth/request/corteza_session_store.go +++ b/auth/request/corteza_session_store.go @@ -5,15 +5,17 @@ import ( "context" "encoding/gob" "fmt" + "net/http" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/pkg/rand" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/types" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" - "net/http" - "strings" - "time" ) // cortezaSessionStore implements the session store and bridge to corteza store @@ -186,7 +188,7 @@ func (s cortezaSessionStore) save(ctx context.Context, ses *sessions.Session) (e cortezaSession.UserID = au.User.ID } - extra := GetExtraReqInfoFromContext(ctx) + extra := auth.GetExtraReqInfoFromContext(ctx) cortezaSession.UserAgent = extra.UserAgent cortezaSession.RemoteAddr = extra.RemoteAddr diff --git a/automation/rest/router.go b/automation/rest/router.go index 56934d572..289667b2a 100644 --- a/automation/rest/router.go +++ b/automation/rest/router.go @@ -7,17 +7,19 @@ import ( "github.com/cortezaproject/corteza-server/pkg/auth" ) -func MountRoutes(r chi.Router) { - // Protect all _private_ routes - r.Group(func(r chi.Router) { - r.Use(auth.MiddlewareValidOnly) +func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { + return func(r chi.Router) { + // Protect all _private_ routes + r.Group(func(r chi.Router) { + r.Use(mv.HttpValidator("api")) - handlers.NewWorkflow(Workflow{}.New()).MountRoutes(r) - handlers.NewTrigger(Trigger{}.New()).MountRoutes(r) - handlers.NewSession(Session{}.New()).MountRoutes(r) - handlers.NewFunction(Function{}.New()).MountRoutes(r) - handlers.NewType(Type{}.New()).MountRoutes(r) - handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) - handlers.NewEventTypes(EventTypes{}.New()).MountRoutes(r) - }) + handlers.NewWorkflow(Workflow{}.New()).MountRoutes(r) + handlers.NewTrigger(Trigger{}.New()).MountRoutes(r) + handlers.NewSession(Session{}.New()).MountRoutes(r) + handlers.NewFunction(Function{}.New()).MountRoutes(r) + handlers.NewType(Type{}.New()).MountRoutes(r) + handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) + handlers.NewEventTypes(EventTypes{}.New()).MountRoutes(r) + }) + } } diff --git a/compose/rest/router.go b/compose/rest/router.go index 1c2596146..68c11cd90 100644 --- a/compose/rest/router.go +++ b/compose/rest/router.go @@ -7,34 +7,37 @@ import ( "github.com/cortezaproject/corteza-server/pkg/auth" ) -func MountRoutes(r chi.Router) { - var ( - namespace = Namespace{}.New() - module = Module{}.New() - record = Record{}.New() - page = Page{}.New() - chart = Chart{}.New() - notification = Notification{}.New() - attachment = Attachment{}.New() - automation = Automation{}.New() - ) +func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { + return func(r chi.Router) { + var ( + namespace = Namespace{}.New() + module = Module{}.New() + record = Record{}.New() + page = Page{}.New() + chart = Chart{}.New() + notification = Notification{}.New() + attachment = Attachment{}.New() + automation = Automation{}.New() + ) - // Initialize handlers & controllers. - r.Group(func(r chi.Router) { - // Use alternative handlers that support file serving - handlers.NewAttachment(attachment).MountRoutes(r) - }) + // Initialize handlers & controllers. + r.Group(func(r chi.Router) { + // Use alternative handlers that support file serving + handlers.NewAttachment(attachment).MountRoutes(r) + }) - // Protect all _private_ routes - r.Group(func(r chi.Router) { - r.Use(auth.MiddlewareValidOnly) - handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) - handlers.NewNamespace(namespace).MountRoutes(r) - handlers.NewPage(page).MountRoutes(r) - handlers.NewAutomation(automation).MountRoutes(r) - handlers.NewModule(module).MountRoutes(r) - handlers.NewRecord(record).MountRoutes(r) - handlers.NewChart(chart).MountRoutes(r) - handlers.NewNotification(notification).MountRoutes(r) - }) + // Protect all _private_ routes + r.Group(func(r chi.Router) { + r.Use(mv.HttpValidator("api")) + + handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) + handlers.NewNamespace(namespace).MountRoutes(r) + handlers.NewPage(page).MountRoutes(r) + handlers.NewAutomation(automation).MountRoutes(r) + handlers.NewModule(module).MountRoutes(r) + handlers.NewRecord(record).MountRoutes(r) + handlers.NewChart(chart).MountRoutes(r) + handlers.NewNotification(notification).MountRoutes(r) + }) + } } diff --git a/compose/types/locale.gen.go b/compose/types/locale.gen.go index 3d33fceb1..c180e9788 100644 --- a/compose/types/locale.gen.go +++ b/compose/types/locale.gen.go @@ -14,9 +14,8 @@ package types import ( "fmt" - "strconv" - "github.com/cortezaproject/corteza-server/pkg/locale" + "strconv" ) type ( diff --git a/compose/types/type_labels.gen.go b/compose/types/type_labels.gen.go index 33c6ab529..22e9ab5c7 100644 --- a/compose/types/type_labels.gen.go +++ b/compose/types/type_labels.gen.go @@ -57,17 +57,17 @@ func (m Module) LabelResourceID() uint64 { } // SetLabel adds new label to label map -func (f *ModuleField) SetLabel(key string, value string) { - if f.Labels == nil { - f.Labels = make(map[string]string) +func (m *ModuleField) SetLabel(key string, value string) { + if m.Labels == nil { + m.Labels = make(map[string]string) } - f.Labels[key] = value + m.Labels[key] = value } // GetLabels adds new label to label map -func (f ModuleField) GetLabels() map[string]string { - return f.Labels +func (m ModuleField) GetLabels() map[string]string { + return m.Labels } // GetLabels adds new label to label map @@ -76,8 +76,8 @@ func (ModuleField) LabelResourceKind() string { } // GetLabels adds new label to label map -func (f ModuleField) LabelResourceID() uint64 { - return f.ID +func (m ModuleField) LabelResourceID() uint64 { + return m.ID } // SetLabel adds new label to label map diff --git a/federation/rest/router.go b/federation/rest/router.go index eaef6362c..9e613e23c 100644 --- a/federation/rest/router.go +++ b/federation/rest/router.go @@ -7,20 +7,23 @@ import ( "github.com/cortezaproject/corteza-server/pkg/auth" ) -func MountRoutes(r chi.Router) { - r.Group(func(r chi.Router) { - handlers.NewNodeHandshake(NodeHandshake{}.New()).MountRoutes(r) - }) +func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { + return func(r chi.Router) { + r.Group(func(r chi.Router) { + handlers.NewNodeHandshake(NodeHandshake{}.New()).MountRoutes(r) + }) - // Protect all _private_ routes - r.Group(func(r chi.Router) { - r.Use(auth.MiddlewareValidOnly) - handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) + // Protect all _private_ routes + r.Group(func(r chi.Router) { + r.Use(mv.HttpValidator("api")) - handlers.NewNode(Node{}.New()).MountRoutes(r) - handlers.NewManageStructure((ManageStructure{}.New())).MountRoutes(r) + handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) - handlers.NewSyncData((SyncData{}.New())).MountRoutes(r) - handlers.NewSyncStructure((SyncStructure{}.New())).MountRoutes(r) - }) + handlers.NewNode(Node{}.New()).MountRoutes(r) + handlers.NewManageStructure((ManageStructure{}.New())).MountRoutes(r) + + handlers.NewSyncData((SyncData{}.New())).MountRoutes(r) + handlers.NewSyncStructure((SyncStructure{}.New())).MountRoutes(r) + }) + } } diff --git a/federation/service/node.go b/federation/service/node.go index fbf76952e..0d6acdf27 100644 --- a/federation/service/node.go +++ b/federation/service/node.go @@ -24,13 +24,17 @@ const ( ) type ( + tokenGenerator interface { + Generate(ctx context.Context, i auth.Identifiable, clientID uint64, scope ...string) (token []byte, err error) + } + node struct { store store.Storer sysUser service.UserService actionlog actionlog.Recorder - tokenEncoder auth.TokenGenerator + tokenEncoder tokenGenerator name string host string @@ -56,7 +60,7 @@ type ( } ) -func Node(s store.Storer, u service.UserService, al actionlog.Recorder, th auth.TokenHandler, options options.FederationOpt, ac nodeAccessController) *node { +func Node(s store.Storer, u service.UserService, al actionlog.Recorder, th tokenGenerator, options options.FederationOpt, ac nodeAccessController) *node { return &node{ store: s, sysUser: u, diff --git a/federation/service/service.go b/federation/service/service.go index 99dca1530..0331a1657 100644 --- a/federation/service/service.go +++ b/federation/service/service.go @@ -86,7 +86,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config) DefaultAccessControl = AccessControl() - DefaultNode = Node(DefaultStore, service.DefaultUser, DefaultActionlog, auth.DefaultJwtHandler, c.Federation, DefaultAccessControl) + DefaultNode = Node(DefaultStore, service.DefaultUser, DefaultActionlog, auth.JWT(), c.Federation, DefaultAccessControl) DefaultNodeSync = NodeSync() DefaultExposedModule = ExposedModule() DefaultSharedModule = SharedModule() diff --git a/go.mod b/go.mod index 715b3d5aa..f5d2626f1 100644 --- a/go.mod +++ b/go.mod @@ -18,18 +18,18 @@ require ( github.com/PuerkitoBio/goquery v1.5.1 // indirect github.com/SentimensRG/ctx v0.0.0-20180729130232-0bfd988c655d github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d - github.com/brianvoe/gofakeit/v6 v6.11.1 - github.com/cortezaproject/corteza-locale v0.0.0-20220103124542-5d327f93f42c + github.com/brianvoe/gofakeit/v6 v6.12.1 + github.com/cortezaproject/corteza-locale v0.0.0-20220111135803-4fb0db6196bc github.com/crewjam/saml v0.4.6 github.com/crusttech/go-oidc v0.0.0-20180918092017-982855dad3e1 github.com/davecgh/go-spew v1.1.1 github.com/deckarep/golang-set v1.7.1 // indirect - github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 github.com/disintegration/imaging v1.6.2 - github.com/dop251/goja v0.0.0-20220102113305-2298ace6d09d + github.com/dop251/goja v0.0.0-20220110113543-261677941f3c github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f - github.com/evanw/esbuild v0.14.10 + github.com/evanw/esbuild v0.14.11 github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect github.com/fsnotify/fsnotify v1.5.1 github.com/gabriel-vasile/mimetype v1.4.0 @@ -56,7 +56,7 @@ require ( github.com/jmoiron/sqlx v1.3.4 github.com/joho/godotenv v1.4.0 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 - github.com/lestrrat-go/jwx v1.2.14 + github.com/lestrrat-go/jwx v1.2.15 github.com/lestrrat-go/strftime v1.0.5 github.com/lib/pq v1.10.4 github.com/markbates/goth v1.68.0 @@ -71,7 +71,7 @@ require ( github.com/prometheus/tsdb v0.7.1 // indirect github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect github.com/sony/sonyflake v1.0.0 - github.com/spf13/afero v1.7.1 + github.com/spf13/afero v1.8.0 github.com/spf13/cast v1.4.1 github.com/spf13/cobra v1.3.0 github.com/steinfletcher/apitest v1.5.11 @@ -79,7 +79,7 @@ require ( github.com/stretchr/testify v1.7.0 github.com/tebeka/strftime v0.1.5 // indirect go.uber.org/atomic v1.9.0 - go.uber.org/zap v1.19.1 + go.uber.org/zap v1.20.0 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 golang.org/x/text v0.3.7 diff --git a/go.sum b/go.sum index 6c420baab..49cb8ebd9 100644 --- a/go.sum +++ b/go.sum @@ -125,6 +125,8 @@ github.com/brianvoe/gofakeit/v6 v6.5.0 h1:zoWqGsuB8TB4MSwUZXtV3OwUSdzi8EHeXO8JfR github.com/brianvoe/gofakeit/v6 v6.5.0/go.mod h1:palrJUk4Fyw38zIFB/uBZqsgzW5VsNllhHKKwAebzew= github.com/brianvoe/gofakeit/v6 v6.11.1 h1:Srilo77ZZfDRwqyJ7sYFBtyhrOdC57NG69s6sVUHwmE= github.com/brianvoe/gofakeit/v6 v6.11.1/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= +github.com/brianvoe/gofakeit/v6 v6.12.1 h1:12JSuDkqX/eUiqnNcwetTrbHMdxdLIBx1pBEZNlCp98= +github.com/brianvoe/gofakeit/v6 v6.12.1/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -165,6 +167,8 @@ github.com/cortezaproject/corteza-locale v0.0.0-20211210121242-931011c1250c h1:z github.com/cortezaproject/corteza-locale v0.0.0-20211210121242-931011c1250c/go.mod h1:wsI1UftEdBqTuEDKBZmx2LfNu/kZun5pRbCAi420JCg= github.com/cortezaproject/corteza-locale v0.0.0-20220103124542-5d327f93f42c h1:VV5Z9ZiULjNoNltJ0ho7Du6svKovDQ92wkxbkdB2gmg= github.com/cortezaproject/corteza-locale v0.0.0-20220103124542-5d327f93f42c/go.mod h1:wsI1UftEdBqTuEDKBZmx2LfNu/kZun5pRbCAi420JCg= +github.com/cortezaproject/corteza-locale v0.0.0-20220111135803-4fb0db6196bc h1:d4xI2vuPSA/U+y1JJQKC9nwk2FSnoYansUquX6IPj5E= +github.com/cortezaproject/corteza-locale v0.0.0-20220111135803-4fb0db6196bc/go.mod h1:wsI1UftEdBqTuEDKBZmx2LfNu/kZun5pRbCAi420JCg= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -205,6 +209,8 @@ github.com/dop251/goja v0.0.0-20210726224656-a55e4cfac4cf h1:eK64KqjIBLpCtzIbzci github.com/dop251/goja v0.0.0-20210726224656-a55e4cfac4cf/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20220102113305-2298ace6d09d h1:RHB3jZIxEzQHPzoGtvn47BMbD7jzTfHAXpVC3v4aVI8= github.com/dop251/goja v0.0.0-20220102113305-2298ace6d09d/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20220110113543-261677941f3c h1:1XnAlcjYBdO7xsa2rhNB/BTztiu4cFKOxE+3brXVtG4= +github.com/dop251/goja v0.0.0-20220110113543-261677941f3c/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b h1:6CBzNasH8+bKeFwr5Bt5JtALHLFN4iQp7sf4ShlP/ik= @@ -228,6 +234,8 @@ github.com/evanw/esbuild v0.12.16 h1:UxvizOzRZk0gnlal2g2MulpCjIiAPtciLr674nOKtcI github.com/evanw/esbuild v0.12.16/go.mod h1:y2AFBAGVelPqPodpdtxWWqe6n2jYf5FrsJbligmRmuw= github.com/evanw/esbuild v0.14.10 h1:+7c1VNndl7uLLxVEeRH4rOUz0Y+nrSw8xfmE9rGtrtw= github.com/evanw/esbuild v0.14.10/go.mod h1:GG+zjdi59yh3ehDn4ZWfPcATxjPDUH53iU4ZJbp7dkY= +github.com/evanw/esbuild v0.14.11 h1:bw50N4v70Dqf/B6Wn+3BM6BVttz4A6tHn8m8Ydj9vxk= +github.com/evanw/esbuild v0.14.11/go.mod h1:GG+zjdi59yh3ehDn4ZWfPcATxjPDUH53iU4ZJbp7dkY= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= @@ -305,6 +313,8 @@ github.com/goccy/go-json v0.7.10 h1:ulhbuNe1JqE68nMRXXTJRrUu0uhouf0VevLINxQq4Ec= github.com/goccy/go-json v0.7.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI= github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.9.0 h1:2flW7bkbrRgU8VuDi0WXDqTmPimjv1thfxkPe8sug+8= +github.com/goccy/go-json v0.9.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -555,6 +565,8 @@ github.com/lestrrat-go/jwx v1.2.11 h1:e9BS5NQ003hxXogNsgf5fEWf01ZJvj4Aj1qy7Dykqm github.com/lestrrat-go/jwx v1.2.11/go.mod h1:25DcLbNWArPA/Ew5CcBmewl32cJKxOk5cbepBsIJFzw= github.com/lestrrat-go/jwx v1.2.14 h1:69OeaiFKCTn8xDmBGzHTgv/GBoO1LJcXw99GfYCDKzg= github.com/lestrrat-go/jwx v1.2.14/go.mod h1:3Q3Re8TaOcVTdpx4Tvz++OWmryDklihTDqrrwQiyS2A= +github.com/lestrrat-go/jwx v1.2.15 h1:58CEGJpf1TS3NJASMfMkTp6stlvPTsqs1xxAu/Yf/uM= +github.com/lestrrat-go/jwx v1.2.15/go.mod h1:DJKaoM8f1OvYVwWoW45gBrUxMlpD4FHjT0UnrW3iX28= github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= @@ -760,6 +772,8 @@ github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY52 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.7.1 h1:F37zV8E8RLstLpZ0RUGK2NGg1X57y6/B0Eg6S8oqdoA= github.com/spf13/afero v1.7.1/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60= +github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= @@ -872,6 +886,7 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -882,6 +897,8 @@ go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.20.0 h1:N4oPlghZwYG55MlU6LXk/Zp00FVNE9X9wrYO8CEs4lc= +go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 29c323f3e..f89103a15 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -85,12 +85,8 @@ func (s server) Serve(ctx context.Context) { r.Use(LogResponse) } - println("using up DefaultJwtHandler", auth.DefaultJwtHandler != nil) - - r.Use( - auth.DefaultJwtHandler.HttpVerifier(), - auth.DefaultJwtHandler.HttpAuthenticator(), - ) + // Verifies JWT in headers, cookies, ... + r.Use(auth.JWT().HttpVerifier()) for _, mountRoutes := range s.endpoints { mountRoutes(r) diff --git a/pkg/auth/errors.go b/pkg/auth/errors.go index f8b8a5ef8..4ec5b8469 100644 --- a/pkg/auth/errors.go +++ b/pkg/auth/errors.go @@ -38,3 +38,20 @@ func ErrUnauthorizedScope() error { errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"), ) } + +func ErrMalformedToken(details string) error { + return errors.New( + errors.KindUnauthorized, + + "malformed token: "+details, + + errors.Meta("type", "malformedToken"), + + // translation namespace & key + errors.Meta(locale.ErrorMetaNamespace{}, "internal"), + errors.Meta(locale.ErrorMetaKey{}, "auth.errors.malformedToken"), + + errors.StackSkip(1), + errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"), + ) +} diff --git a/pkg/auth/extra.go b/pkg/auth/extra.go new file mode 100644 index 000000000..9e67b8c33 --- /dev/null +++ b/pkg/auth/extra.go @@ -0,0 +1,31 @@ +package auth + +import ( + "context" + "net/http" +) + +type ( + ExtraReqInfo struct { + RemoteAddr string + UserAgent string + } +) + +func ExtraReqInfoMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ExtraReqInfo{}, ExtraReqInfo{ + RemoteAddr: r.RemoteAddr, + UserAgent: r.UserAgent(), + }))) + }) +} + +func GetExtraReqInfoFromContext(ctx context.Context) ExtraReqInfo { + eti := ctx.Value(ExtraReqInfo{}) + if eti != nil { + return eti.(ExtraReqInfo) + } else { + return ExtraReqInfo{} + } +} diff --git a/pkg/auth/interfaces.go b/pkg/auth/interfaces.go index 50c4dc1c6..b94438979 100644 --- a/pkg/auth/interfaces.go +++ b/pkg/auth/interfaces.go @@ -1,10 +1,5 @@ package auth -import ( - "context" - "net/http" -) - type ( Identifiable interface { Identity() uint64 @@ -13,16 +8,16 @@ type ( String() string } - TokenGenerator interface { - Encode(i Identifiable, clientID uint64, scope ...string) (token []byte, err error) - Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (token []byte, err error) - } + //TokenGenerator interface { + // Encode(i Identifiable, clientID uint64, scope ...string) (token []byte, err error) + // Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (token []byte, err error) + //} - TokenHandler interface { - TokenGenerator - HttpVerifier() func(http.Handler) http.Handler - HttpAuthenticator() func(http.Handler) http.Handler - } + //TokenHandler interface { + // TokenGenerator + // HttpVerifier() func(http.Handler) http.Handler + // HttpAuthenticator() func(http.Handler) http.Handler + //} Signer interface { Sign(userID uint64, pp ...interface{}) string diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index bd804ff82..179e845f0 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -2,67 +2,96 @@ package auth import ( "context" - "encoding/json" "fmt" "net/http" + "strconv" "strings" "time" - "github.com/cortezaproject/corteza-server/pkg/api" - "github.com/cortezaproject/corteza-server/pkg/id" + "github.com/cortezaproject/corteza-server/pkg/errors" + "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/cortezaproject/corteza-server/system/types" "github.com/go-chi/jwtauth" + "github.com/go-oauth2/oauth2/v4" "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwk" "github.com/lestrrat-go/jwx/jwt" "github.com/spf13/cast" + "go.uber.org/zap" ) type ( - tokenManager struct { + signer interface { + Sign(accessToken string, identity Identifiable, clientID uint64, scope ...string) (signed []byte, err error) + } + + MiddlewareValidator interface { + HttpValidator(scope ...string) func(http.Handler) http.Handler + } + + oauth2manager interface { + LoadAccessToken(ctx context.Context, access string) (ti oauth2.TokenInfo, err error) + GenerateAccessToken(ctx context.Context, gt oauth2.GrantType, tgr *oauth2.TokenGenerateRequest) (oauth2.TokenInfo, error) + } + + jwtManager struct { // Expiration time in minutes expiry time.Duration signAlgo jwa.SignatureAlgorithm signKey jwk.Key + + log *zap.Logger + + oa2m oauth2manager + + issuerClaim string } + // @todo remove tokenStore interface { - LookupUserByID(ctx context.Context, id uint64) (*types.User, error) - LookupAuthOa2tokenByAccess(ctx context.Context, access string) (*types.AuthOa2token, error) - SearchRoleMembers(ctx context.Context, f types.RoleMemberFilter) (types.RoleMemberSet, types.RoleMemberFilter, error) - CreateAuthOa2token(ctx context.Context, rr ...*types.AuthOa2token) error - DeleteAuthOA2TokenByUserID(ctx context.Context, _userID uint64) error - UpsertAuthConfirmedClient(ctx context.Context, rr ...*types.AuthConfirmedClient) error } - ExtraReqInfo struct { - RemoteAddr string - UserAgent string - } + // @todo remove + //tokenLookup interface { + // LookupAuthOa2tokenByID(ctx context.Context, id uint64) (*types.AuthOa2token, error) + //} + // + //tokenStoreWithLookup interface { + // tokenStore + // tokenLookup + //} ) var ( - DefaultJwtHandler TokenHandler - DefaultJwtStore tokenStore + defaultJWTManager *jwtManager + //DefaultJwtStore tokenStoreWithLookup ) -func SetupDefault(secret string, expiry time.Duration) (err error) { +// JWT returns d +func JWT() *jwtManager { + return defaultJWTManager +} + +func SetupDefault(oa2m oauth2manager, secret string, expiry time.Duration) (err error) { // Use JWT secret for hmac signer for now DefaultSigner = HmacSigner(secret) - DefaultJwtHandler, err = TokenManager(secret, expiry) + defaultJWTManager, err = NewJWTManager(oa2m, jwa.HS512, secret, expiry) return } -// TokenManager returns token management facility +// NewJWTManager initializes and returns new instance of JWT manager // @todo should be extended to accept different kinds of algorythms, private-keys etc. -func TokenManager(secret string, expiry time.Duration) (tm *tokenManager, err error) { - tm = &tokenManager{ - expiry: expiry, - signAlgo: jwa.HS512, +func NewJWTManager(oa2m oauth2manager, algo jwa.SignatureAlgorithm, secret string, expiry time.Duration) (tm *jwtManager, err error) { + tm = &jwtManager{ + expiry: expiry, + signAlgo: algo, + issuerClaim: "cortezaproject.org", + log: logger.Default(), + oa2m: oa2m, } if len(secret) == 0 { @@ -74,72 +103,40 @@ func TokenManager(secret string, expiry time.Duration) (tm *tokenManager, err er } return - - // - //var ( - // // tuukn = jwt.New() - // // signed []byte - // //) - // // - // //if err = tuukn.Set(jwt.ExpirationKey, expiry); err != nil { - // // return - // //} - // // - // signed, err = jwt.Sign(tuukn, jwa.HS512, []byte(secret)) - // - // tkn = &tokenManager{ - // expiry: expiry, - // tokenAuth: jwtauth.New(jwt.SigningMethodHS512.Alg(), []byte(secret), nil), - // secret: []byte(secret), - // }, nil - // - //return tkn, nil } -// SetJWTStore set store for JWT -// @todo find better way to initiate store, -// it mainly used for generating and storing accessToken for impersonate and corredor, Ref: j.Generate() -func SetJWTStore(store tokenStore) { - DefaultJwtStore = store -} - -// Authenticate the token from the given string and return parsed token or error -func (tm *tokenManager) Authenticate(token string) (pToken jwt.Token, err error) { - if pToken, err = jwt.Parse([]byte(token), jwt.WithVerify(tm.signAlgo, tm.signKey)); err != nil { - return - } - - if err = jwt.Validate(pToken); err != nil { - return - } - - return -} - -//// Encode identity into a -//func (tm *tokenManager) Encode(identity Identifiable, scope ...string) ([]byte, error) { -// var ( -// // when possible, extend this with the client -// clientID uint64 = 0 -// ) +//// @todo remove +////// SetJWTStore set store for JWT +////// @todo find better way to initiate store, +////// it mainly used for generating and storing accessToken for impersonate and corredor, Ref: j.Generate() +////func SetJWTStore(store tokenStoreWithLookup) { +//// DefaultJwtStore = store +////} // -// if len(scope) == 0 { -// // for backward compatibility we default -// // unset scope to profile & api -// scope = []string{"profile", "api"} +//// Authenticate the token from the given string and return parsed token or error +//func (m *jwtManager) Authenticate(s string) (pToken jwt.Token, err error) { +// if pToken, err = jwt.Parse([]byte(s), jwt.WithVerify(m.signAlgo, m.signKey)); err != nil { +// return // } // -// return tm.Encode(identity, clientID, scope...) +// if err = jwt.Validate(pToken); err != nil { +// return +// } +// +// return //} -// Encode give identity, clientID & scope into JWT access token (that can be use for API requests) +// Sign takes security information and returns signed JWT // -// @todo this follows implementation in auth/oauth2/jwt_access.go -// and should be refactored accordingly (move both into the same location/pkg => here) -func (tm *tokenManager) Encode(identity Identifiable, clientID uint64, scope ...string) (_ []byte, err error) { +// Access token is expected to be issued by OAuth2 token manager +// without it, we can only do static (JWT itself) validation +//f +// Identity holds user ID and all roles that go into this security context +// Client ID represents the auth client that was used +func (m *jwtManager) Sign(accessToken string, identity Identifiable, clientID uint64, scope ...string) (signed []byte, err error) { var ( + roles string token = jwt.New() - roles = "" ) if len(scope) == 0 { @@ -149,12 +146,12 @@ func (tm *tokenManager) Encode(identity Identifiable, clientID uint64, scope ... } for _, r := range identity.Roles() { - roles += fmt.Sprintf(" %d", r) + roles += strconv.FormatUint(r, 10) } - // previous implementation had special a "salt" claim that ensured JWT uniquness - // we're using more standard approach with JWT ID now. - if err = token.Set(jwt.JwtIDKey, fmt.Sprintf("%d", id.Next())); err != nil { + // this is the key part + // here we put access token to the JWT ID claim + if err = token.Set(jwt.JwtIDKey, accessToken); err != nil { return } @@ -162,11 +159,19 @@ func (tm *tokenManager) Encode(identity Identifiable, clientID uint64, scope ... return } - if err = token.Set(jwt.ExpirationKey, time.Now().Add(tm.expiry).Unix()); err != nil { + if err = token.Set(jwt.ExpirationKey, time.Now().Add(m.expiry).Unix()); err != nil { return } - if err = token.Set(jwt.AudienceKey, fmt.Sprintf("%d", clientID)); err != nil { + if err = token.Set(jwt.IssuerKey, m.issuerClaim); err != nil { + return + } + + if err = token.Set(jwt.IssuedAtKey, time.Now().Unix()); err != nil { + return + } + + if err = token.Set("clientID", strconv.FormatUint(clientID, 10)); err != nil { return } @@ -178,57 +183,87 @@ func (tm *tokenManager) Encode(identity Identifiable, clientID uint64, scope ... return } - return jwt.Sign(token, tm.signAlgo, tm.signKey) + if signed, err = jwt.Sign(token, m.signAlgo, m.signKey); err != nil { + return + } - //claims := jwt.MapClaims{ - // "sub": identity.String(), - // "exp": time.Now().Add(tm.expiry).Unix(), - // "aud": fmt.Sprintf("%d", clientID), - // "scope": strings.Join(scope, " "), - // "roles": strings.TrimSpace(roles), - //} - // - //newToken := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) - //newToken.Header["salt"] = string(rand.Bytes(32)) - //access, _ := newToken.SignedString(tm.secret) - //return access + return signed, nil } -// HttpVerifier returns a HTTP handler that verifies JWT and stores it into context -func (tm *tokenManager) HttpVerifier() func(http.Handler) http.Handler { - ////jwt.WithHTTPClient() - //return func(next http.Handler) http.Handler { - // return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // token, err := jwt.ParseRequest(req) - // if err != nil { - // - // } - // - // next.ServeHTTP(w, req) - // }) - //} +func (m *jwtManager) Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (signed []byte, err error) { + var ( + ti oauth2.TokenInfo + ) - return jwtauth.Verifier(jwtauth.New(tm.signAlgo.String(), tm.signKey, nil)) + ti, err = m.oa2m.GenerateAccessToken(ctx, oauth2.Implicit, &oauth2.TokenGenerateRequest{ + ClientID: strconv.FormatUint(clientID, 10), + UserID: i.String(), + Scope: strings.Join(scope, " "), + Refresh: "cli?", + AccessTokenExp: m.expiry, + }) + + if err != nil { + return + } + + return m.Sign(ti.GetAccess(), i, 0, scope...) } -// HttpAuthenticator converts JWT claims into identity and stores it into context -func (tm *tokenManager) HttpAuthenticator() func(http.Handler) http.Handler { +func ValidateContext(ctx context.Context, oa2m oauth2manager, scope ...string) (err error) { + var ( + token jwt.Token + ) + + if token, _, err = jwtauth.FromContext(ctx); err != nil { + return ErrUnauthorized() + } + + return Validate(ctx, token, oa2m, scope...) +} + +func Validate(ctx context.Context, token jwt.Token, oa2m oauth2manager, scope ...string) (err error) { + if !CheckJwtScope(token, scope...) { + return ErrUnauthorizedScope() + } + + // Extract the JWT id from the token (string) and convert it to uint64 + // to be compatible with the lookup function + if len(token.JwtID()) < 10 { + return ErrMalformedToken("missing or malformed JWT ID") + } + + // @todo we could use a simple caching mechanism here + // 1. if lookup is successful, add a JWT ID to the list + // 2. add short exp time (that should not last longer than token's exp time) + // 3. check against the list first; if JWT ID is not present there check in storage + // + if _, err = oa2m.LoadAccessToken(ctx, token.JwtID()); err != nil { + return ErrUnauthorized() + } + + return nil +} + +// HttpVerifier http middleware handler will verify a JWT string from a http request. +func (m *jwtManager) HttpVerifier() func(http.Handler) http.Handler { + return jwtauth.Verifier(jwtauth.New(m.signAlgo.String(), m.signKey, nil)) +} + +func (m *jwtManager) HttpValidator(scope ...string) func(http.Handler) http.Handler { + if len(scope) == 0 { + // ensure that scope is not empty + scope = []string{"api"} + } + return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - tkn, _, err := jwtauth.FromContext(ctx) - - // When token is present, expect no errors and valid claims! - if tkn != nil { - if err != nil { - // But if token is present, there shouldn't be an error - api.Send(w, r, err) - return - } - - ctx = SetIdentityToContext(ctx, IdentityFromToken(tkn)) - r = r.WithContext(ctx) + if err := ValidateContext(r.Context(), m.oa2m, scope...); err != nil { + errors.ProperlyServeHTTP(w, r, err, false) + return + } else { + token, _, _ := jwtauth.FromContext(r.Context()) + r = r.WithContext(SetIdentityToContext(r.Context(), IdentityFromToken(token))) } next.ServeHTTP(w, r) @@ -236,62 +271,87 @@ func (tm *tokenManager) HttpAuthenticator() func(http.Handler) http.Handler { } } -// Generates JWT and stores alongside with client-confirmation entry, -func (tm *tokenManager) Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (token []byte, err error) { - var ( - eti = GetExtraReqInfoFromContext(ctx) - oa2t = &types.AuthOa2token{ - ID: id.Next(), - CreatedAt: time.Now().Round(time.Second), - RemoteAddr: eti.RemoteAddr, - UserAgent: eti.UserAgent, - ClientID: clientID, - } +//// HttpAuthenticator converts JWT claims into identity and stores it into context +//func (m *jwtManager) HttpAuthenticator(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// ctx := r.Context() +// +// tkn, _, err := jwtauth.FromContext(ctx) +// +// // Requests w/o token should not yield an error +// // there are parts of the system that can be access without it +// // and/or handle such situation internally +// if err != nil && !errors.Is(err, jwtauth.ErrNoTokenFound) { +// api.Send(w, r, err) +// return +// } +// +// // If token is present extract identity +// if tkn != nil { +// ctx = SetIdentityToContext(ctx, IdentityFromToken(tkn)) +// r = r.WithContext(ctx) +// +// // @todo verify JWT ID (access-token!! +// tkn.JwtID() +// +// } +// +// next.ServeHTTP(w, r) +// }) +//} - acc = &types.AuthConfirmedClient{ - ConfirmedAt: oa2t.CreatedAt, - ClientID: clientID, - } - ) +// +//// Generate makes a new token and stores it in the database +//func (tm *tokenManager) Generate(ctx context.Context, i Identifiable, clientID uint64, scope ...string) (token []byte, err error) { +// var ( +// // eti = GetExtraReqInfoFromContext(ctx) +// // oa2t = &types.AuthOa2token{ +// // ID: id.Next(), +// // CreatedAt: time.Now().Round(time.Second), +// // RemoteAddr: eti.RemoteAddr, +// // UserAgent: eti.UserAgent, +// // ClientID: clientID, +// // } +// // +// // acc = &types.AuthConfirmedClient{ +// // ConfirmedAt: oa2t.CreatedAt, +// // ClientID: clientID, +// // } +// oa2t *types.AuthOa2token +// acc *types.AuthConfirmedClient +// +// jwtID = id.Next() +// ) +// +// if oa2t, acc, err = MakeAuthStructs(ctx, jwtID, i.Identity(), clientID, nil, tm.expiry); err != nil { +// return +// } +// +// if token, err = tm.make(jwtID, i, clientID, scope...); err != nil { +// return nil, err +// } +// +// oa2t.Access = string(token) +// +// // use the same expiration as on token +// //oa2t.ExpiresAt = oa2t.CreatedAt.Add(tm.expiry) +// +// //if oa2t.Data, err = json.Marshal(oa2t); err != nil { +// // return +// //} +// +// //if oa2t.UserID, _ = ExtractFromSubClaim(i.String()); oa2t.UserID == 0 { +// // // UserID stores collection of IDs: user's ID and set of all roles' user is member of +// // return nil, fmt.Errorf("could not parse user ID from token") +// //} +// // +// //// copy user id to auth client confirmation +// //acc.UserID = oa2t.UserID +// +// return token, StoreAuthToken(ctx, DefaultJwtStore, oa2t, acc) +//} - if token, err = tm.Encode(i, clientID, scope...); err != nil { - return - } - - oa2t.Access = string(token) - - // use the same expiration as on token - oa2t.ExpiresAt = oa2t.CreatedAt.Add(tm.expiry) - - if oa2t.Data, err = json.Marshal(oa2t); err != nil { - return - } - - if oa2t.UserID, _ = ExtractFromSubClaim(i.String()); oa2t.UserID == 0 { - // UserID stores collection of IDs: user's ID and set of all roles' user is member of - return nil, fmt.Errorf("could not parse user ID from token") - } - - // copy user id to auth client confirmation - acc.UserID = oa2t.UserID - - if err = DefaultJwtStore.UpsertAuthConfirmedClient(ctx, acc); err != nil { - return - } - - return token, DefaultJwtStore.CreateAuthOa2token(ctx, oa2t) -} - -func GetExtraReqInfoFromContext(ctx context.Context) ExtraReqInfo { - eti := ctx.Value(ExtraReqInfo{}) - if eti != nil { - return eti.(ExtraReqInfo) - } else { - return ExtraReqInfo{} - } -} - -// ClaimsToIdentity decodes sub & roles claims into identity +// IdentityFromToken decodes sub & roles claims into identity func IdentityFromToken(token jwt.Token) *identity { var ( roles, _ = token.Get("roles") diff --git a/pkg/auth/middleware.go b/pkg/auth/middleware.go index 441508656..f8e03e00c 100644 --- a/pkg/auth/middleware.go +++ b/pkg/auth/middleware.go @@ -1,53 +1,57 @@ package auth -import ( - "net/http" +//import ( +// "net/http" +//) +// +//func MiddlewareValidOnly(next http.Handler) http.Handler { +// return AccessTokenCheck("api")(next) +//} - "github.com/cortezaproject/corteza-server/pkg/errors" - "github.com/davecgh/go-spew/spew" - "github.com/go-chi/jwtauth" -) - -func MiddlewareValidOnly(next http.Handler) http.Handler { - return AccessTokenCheck("api")(next) -} - -func AccessTokenCheck(scope ...string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var ctx = r.Context() - - token, _, err := jwtauth.FromContext(ctx) - spew.Dump(token, err) - - if err != nil { - errors.ProperlyServeHTTP(w, r, ErrUnauthorized(), false) - return - } - - if !CheckJwtScope(token, scope...) { - errors.ProperlyServeHTTP(w, r, ErrUnauthorizedScope(), false) - } - - // @todo we need to check if token is in store!! - // @todo we need to check if token is in store!! - // @todo we need to check if token is in store!! - // @todo we need to check if token is in store!! - // @todo we need to check if token is in store!! - // @todo we need to check if token is in store!! - // @todo we need to check if token is in store!! - // @todo we need to check if token is in store!! - // @todo we need to check if token is in store!! - // @todo we need to check if token is in store!! - // - //// verify JWT from store - //_, err = DefaultJwtStore.LookupAuthOa2tokenByAccess(ctx, tkn.Raw) - //if err != nil { - // errors.ProperlyServeHTTP(w, r, ErrUnauthorized(), false) - // return - //} - - next.ServeHTTP(w, r) - }) - } -} +//func AccessTokenCheck(s store.AuthOa2tokens, scope ...string) func(http.Handler) http.Handler { +// return func(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// if err := validateContextToken(r.Context(), s, scope); err != nil { +// errors.ProperlyServeHTTP(w, r, err, false) +// return +// } +// +// next.ServeHTTP(w, r) +// }) +// } +//} +// +//func validateContextToken(ctx context.Context, s store.AuthOa2tokens, scope []string) (err error) { +// var ( +// token jwt.Token +// ) +// +// if token, _, err = jwtauth.FromContext(ctx); err != nil { +// return ErrUnauthorized() +// } +// +// if !CheckJwtScope(token, scope...) { +// return ErrUnauthorizedScope() +// } +// +// // Extract the JWT id from the token (string) and convert it to uint64 +// // to be compatible with the lookup function +// if len(token.JwtID()) < 10 { +// return ErrMalformedToken("missing or malformed JWT ID") +// } +// +// // check if token exists in our DB +// // there is no need to check for anything beyond existence +// // because +// // +// // @todo we could use a simple caching mechanism here +// // 1. if lookup is successful, add a JWT ID to the list +// // 2. add short exp time (that should not last onger than token's exp time) +// // 3. check against the list first; if JWT ID is not present there check in storage +// // +// if _, err = store.LookupAuthOa2tokenByAccess(ctx, s, token.JwtID()); err != nil { +// return ErrUnauthorized() +// } +// +// return nil +//} diff --git a/pkg/corredor/service.go b/pkg/corredor/service.go index f5564253c..a41bf7a28 100644 --- a/pkg/corredor/service.go +++ b/pkg/corredor/service.go @@ -104,7 +104,7 @@ type ( } authTokenMaker interface { - auth.TokenGenerator + Generate(ctx context.Context, i auth.Identifiable, clientID uint64, scope ...string) (token []byte, err error) } ) @@ -168,7 +168,7 @@ func NewService(logger *zap.Logger, opt options.CorredorOpt) *service { iteratorProviders: make(map[string]IteratorResourceFinder), - authTokenMaker: auth.DefaultJwtHandler, + authTokenMaker: auth.JWT(), eventRegistry: eventbus.Service(), denyExec: make(map[string]map[uint64]bool), diff --git a/pkg/websocket/session_test.go b/pkg/websocket/session_test.go index 9c9cca3ad..8a0d475f8 100644 --- a/pkg/websocket/session_test.go +++ b/pkg/websocket/session_test.go @@ -7,15 +7,15 @@ import ( "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" + "github.com/lestrrat-go/jwx/jwa" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func TestSession_procRawMessage(t *testing.T) { var ( - req = require.New(t) - s = session{server: Server(nil, options.WebsocketOpt{})} - jwtHandler, err = auth.TokenManager("secret", time.Minute) + req = require.New(t) + s = session{server: Server(nil, options.WebsocketOpt{})} userID uint64 = 123 token []byte @@ -28,6 +28,9 @@ func TestSession_procRawMessage(t *testing.T) { } ) + jwtManager, err := auth.NewJWTManager(nil, jwa.HS512, "secret", time.Minute) + req.NoError(err) + if testing.Verbose() { s.logger = logger.MakeDebugLogger() } else { @@ -36,7 +39,7 @@ func TestSession_procRawMessage(t *testing.T) { req.NoError(err) - token, err = jwtHandler.Encode(auth.Authenticated(userID, 456, 789), 0, "api") + token, err = jwtManager.Sign("access-token", auth.Authenticated(userID, 456, 789), 0, "api") req.NoError(err) req.EqualError(s.procRawMessage([]byte("{}")), "unauthenticated session") @@ -53,7 +56,7 @@ func TestSession_procRawMessage(t *testing.T) { req.Equal(userID, s.identity.Identity()) // Repeat with the same user - token, err = jwtHandler.Encode(auth.Authenticated(userID, 456, 789), 0, "api") + token, err = jwtManager.Sign("access-token", auth.Authenticated(userID, 456, 789), 0, "api") req.NoError(err) req.NoError(s.procRawMessage(mockResponse(token))) @@ -61,9 +64,10 @@ func TestSession_procRawMessage(t *testing.T) { req.Equal(userID, s.identity.Identity()) // Try to authenticate on an existing authenticated session as a different user - token, err = jwtHandler.Encode(auth.Authenticated(userID+1, 456, 789), 0, "api") + token, err = jwtManager.Sign("access-token", auth.Authenticated(userID+1, 456, 789), 0, "api") req.NoError(err) req.EqualError(s.procRawMessage(mockResponse(token)), "unauthorized: identity does not match") + t.Error("are we actually checking if access token exists?") } diff --git a/store/auth_oa2tokens.gen.go b/store/auth_oa2tokens.gen.go index e72e5ec9d..c474d6b89 100644 --- a/store/auth_oa2tokens.gen.go +++ b/store/auth_oa2tokens.gen.go @@ -10,12 +10,14 @@ package store import ( "context" + "github.com/cortezaproject/corteza-server/system/types" ) type ( AuthOa2tokens interface { SearchAuthOa2tokens(ctx context.Context, f types.AuthOa2tokenFilter) (types.AuthOa2tokenSet, types.AuthOa2tokenFilter, error) + LookupAuthOa2tokenByID(ctx context.Context, id uint64) (*types.AuthOa2token, error) LookupAuthOa2tokenByCode(ctx context.Context, code string) (*types.AuthOa2token, error) LookupAuthOa2tokenByAccess(ctx context.Context, access string) (*types.AuthOa2token, error) LookupAuthOa2tokenByRefresh(ctx context.Context, refresh string) (*types.AuthOa2token, error) @@ -54,6 +56,11 @@ func SearchAuthOa2tokens(ctx context.Context, s AuthOa2tokens, f types.AuthOa2to return s.SearchAuthOa2tokens(ctx, f) } +// LookupAuthOa2tokenByID +func LookupAuthOa2tokenByID(ctx context.Context, s AuthOa2tokens, id uint64) (*types.AuthOa2token, error) { + return s.LookupAuthOa2tokenByID(ctx, id) +} + // LookupAuthOa2tokenByCode func LookupAuthOa2tokenByCode(ctx context.Context, s AuthOa2tokens, code string) (*types.AuthOa2token, error) { return s.LookupAuthOa2tokenByCode(ctx, code) diff --git a/store/auth_oa2tokens.yaml b/store/auth_oa2tokens.yaml index e370a806f..58a4ea28b 100644 --- a/store/auth_oa2tokens.yaml +++ b/store/auth_oa2tokens.yaml @@ -20,6 +20,7 @@ rdbms: customFilterConverter: true lookups: + - fields: [ ID ] - fields: [ Code ] uniqueConstraintCheck: true - fields: [ Access ] diff --git a/store/rdbms/auth_oa2tokens.gen.go b/store/rdbms/auth_oa2tokens.gen.go index d2b4fe048..74a12141b 100644 --- a/store/rdbms/auth_oa2tokens.gen.go +++ b/store/rdbms/auth_oa2tokens.gen.go @@ -11,6 +11,7 @@ package rdbms import ( "context" "database/sql" + "github.com/Masterminds/squirrel" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/store" @@ -85,6 +86,13 @@ func (s Store) QueryAuthOa2tokens( return set, nil } +// LookupAuthOa2tokenByID +func (s Store) LookupAuthOa2tokenByID(ctx context.Context, id uint64) (*types.AuthOa2token, error) { + return s.execLookupAuthOa2token(ctx, squirrel.Eq{ + s.preprocessColumn("tkn.id", ""): store.PreprocessValue(id, ""), + }) +} + // LookupAuthOa2tokenByCode func (s Store) LookupAuthOa2tokenByCode(ctx context.Context, code string) (*types.AuthOa2token, error) { return s.execLookupAuthOa2token(ctx, squirrel.Eq{ diff --git a/system/commands/commands.go b/system/commands/commands.go index fb7a35c80..d849be1dc 100644 --- a/system/commands/commands.go +++ b/system/commands/commands.go @@ -2,6 +2,7 @@ package commands import ( "context" + "github.com/cortezaproject/corteza-server/pkg/cli" "github.com/spf13/cobra" ) diff --git a/system/rest/auth.go b/system/rest/auth.go index 85df9def6..22d247aaf 100644 --- a/system/rest/auth.go +++ b/system/rest/auth.go @@ -14,8 +14,12 @@ import ( var _ = errors.Wrap type ( + tokenGenerator interface { + Generate(ctx context.Context, i auth.Identifiable, clientID uint64, scope ...string) (token []byte, err error) + } + Auth struct { - tokenHandler auth.TokenGenerator + tokenHandler tokenGenerator settings *types.AppSettings authSvc authUserService } @@ -47,7 +51,7 @@ type ( func (Auth) New() *Auth { return &Auth{ - tokenHandler: auth.DefaultJwtHandler, + tokenHandler: auth.JWT(), settings: service.CurrentSettings, authSvc: service.DefaultAuth, } diff --git a/system/rest/router.go b/system/rest/router.go index 020276f11..4a2bccf52 100644 --- a/system/rest/router.go +++ b/system/rest/router.go @@ -8,39 +8,41 @@ import ( "github.com/cortezaproject/corteza-server/system/service" ) -func MountRoutes(r chi.Router) { - r.Group(func(r chi.Router) { - handlers.NewLocale(Locale{}.New()).MountRoutes(r) +func MountRoutes(mv auth.MiddlewareValidator) func(r chi.Router) { + return func(r chi.Router) { + r.Group(func(r chi.Router) { + handlers.NewLocale(Locale{}.New()).MountRoutes(r) - handlers.NewAttachment(Attachment{}.New()).MountRoutes(r) - handlers.NewAuth((Auth{}).New()).MountRoutes(r) + handlers.NewAttachment(Attachment{}.New()).MountRoutes(r) + handlers.NewAuth((Auth{}).New()).MountRoutes(r) - // A special case that, we do not add this through standard request, handlers & controllers - // combo but directly -- we need access to r.Body - r.Handle(service.SinkBaseURL+"*", &Sink{ - svc: service.DefaultSink, - sign: auth.DefaultSigner, + // A special case that, we do not add this through standard request, handlers & controllers + // combo but directly -- we need access to r.Body + r.Handle(service.SinkBaseURL+"*", &Sink{ + svc: service.DefaultSink, + sign: auth.DefaultSigner, + }) }) - }) - // Protect all _private_ routes - r.Group(func(r chi.Router) { - r.Use(auth.MiddlewareValidOnly) + // Protect all _private_ routes + r.Group(func(r chi.Router) { + r.Use(mv.HttpValidator("api")) - handlers.NewAuthClient(AuthClient{}.New()).MountRoutes(r) - handlers.NewAutomation(Automation{}.New()).MountRoutes(r) - handlers.NewUser(User{}.New()).MountRoutes(r) - handlers.NewRole(Role{}.New()).MountRoutes(r) - handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) - handlers.NewApplication(Application{}.New()).MountRoutes(r) - handlers.NewTemplate(Template{}.New()).MountRoutes(r) - handlers.NewReport(Report{}.New()).MountRoutes(r) - handlers.NewSettings(Settings{}.New()).MountRoutes(r) - handlers.NewStats(Stats{}.New()).MountRoutes(r) - handlers.NewReminder(Reminder{}.New()).MountRoutes(r) - handlers.NewActionlog(Actionlog{}.New()).MountRoutes(r) - handlers.NewQueues(Queue{}.New()).MountRoutes(r) - handlers.NewApigwRoute(ApigwRoute{}.New()).MountRoutes(r) - handlers.NewApigwFilter(ApigwFilter{}.New()).MountRoutes(r) - }) + handlers.NewAuthClient(AuthClient{}.New()).MountRoutes(r) + handlers.NewAutomation(Automation{}.New()).MountRoutes(r) + handlers.NewUser(User{}.New()).MountRoutes(r) + handlers.NewRole(Role{}.New()).MountRoutes(r) + handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) + handlers.NewApplication(Application{}.New()).MountRoutes(r) + handlers.NewTemplate(Template{}.New()).MountRoutes(r) + handlers.NewReport(Report{}.New()).MountRoutes(r) + handlers.NewSettings(Settings{}.New()).MountRoutes(r) + handlers.NewStats(Stats{}.New()).MountRoutes(r) + handlers.NewReminder(Reminder{}.New()).MountRoutes(r) + handlers.NewActionlog(Actionlog{}.New()).MountRoutes(r) + handlers.NewQueues(Queue{}.New()).MountRoutes(r) + handlers.NewApigwRoute(ApigwRoute{}.New()).MountRoutes(r) + handlers.NewApigwFilter(ApigwFilter{}.New()).MountRoutes(r) + }) + } } diff --git a/system/types/auth_oa2token.go b/system/types/auth_oa2token.go index be2393ce7..286f067ca 100644 --- a/system/types/auth_oa2token.go +++ b/system/types/auth_oa2token.go @@ -1,8 +1,9 @@ package types import ( - sqlxTypes "github.com/jmoiron/sqlx/types" "time" + + sqlxTypes "github.com/jmoiron/sqlx/types" ) type ( diff --git a/tests/apigw/main_test.go b/tests/apigw/main_test.go index 484eaae94..9510acacc 100644 --- a/tests/apigw/main_test.go +++ b/tests/apigw/main_test.go @@ -79,7 +79,7 @@ func InitTestApp() { helpers.BindAuthMiddleware(r) // Sys routes for route management tests - rest.MountRoutes(r) + r.Group(rest.MountRoutes(auth.JWT())) // API gw routes apigw.Setup(options.Apigw(), service.DefaultLogger, service.DefaultStore) @@ -111,7 +111,7 @@ func newHelper(t *testing.T) helper { helpers.UpdateRBAC(h.roleID) var err error - h.token, err = auth.DefaultJwtHandler.Generate(context.Background(), h.cUser, 0) + h.token, err = auth.JWT().Generate(context.Background(), h.cUser, 0) if err != nil { panic(err) } diff --git a/tests/automation/main_test.go b/tests/automation/main_test.go index c6b6c962f..7374c54ef 100644 --- a/tests/automation/main_test.go +++ b/tests/automation/main_test.go @@ -84,7 +84,7 @@ func InitTestApp() { r = chi.NewRouter() r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - rest.MountRoutes(r) + r.Group(rest.MountRoutes(auth.JWT())) } } @@ -107,7 +107,7 @@ func newHelper(t *testing.T) helper { helpers.UpdateRBAC(h.roleID) var err error - h.token, err = auth.DefaultJwtHandler.Generate(context.Background(), h.cUser, 0) + h.token, err = auth.JWT().Generate(context.Background(), h.cUser, 0) if err != nil { panic(err) } diff --git a/tests/compose/main_test.go b/tests/compose/main_test.go index c801f7126..19328a7bb 100644 --- a/tests/compose/main_test.go +++ b/tests/compose/main_test.go @@ -95,7 +95,7 @@ func InitTestApp() { r = chi.NewRouter() r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - rest.MountRoutes(r) + r.Group(rest.MountRoutes(auth.JWT())) } } @@ -124,7 +124,7 @@ func newHelper(t *testing.T) helper { func (h *helper) identityToHelper(u *sysTypes.User) { var err error h.cUser = u - h.token, err = auth.DefaultJwtHandler.Generate(context.Background(), u, 0) + h.token, err = auth.JWT().Generate(context.Background(), u, 0) if err != nil { panic(err) } diff --git a/tests/federation/main_test.go b/tests/federation/main_test.go index 5101915b3..eb5038330 100644 --- a/tests/federation/main_test.go +++ b/tests/federation/main_test.go @@ -64,7 +64,7 @@ func InitTestApp() { r = chi.NewRouter() r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - rest.MountRoutes(r) + r.Group(rest.MountRoutes(auth.JWT())) } } @@ -87,7 +87,7 @@ func newHelper(t *testing.T) helper { helpers.UpdateRBAC(h.roleID) var err error - h.token, err = auth.DefaultJwtHandler.Generate(context.Background(), h.cUser, 0) + h.token, err = auth.JWT().Generate(context.Background(), h.cUser, 0) if err != nil { panic(err) } diff --git a/tests/helpers/auth.go b/tests/helpers/auth.go index c0d6346e3..cf784cf2c 100644 --- a/tests/helpers/auth.go +++ b/tests/helpers/auth.go @@ -11,8 +11,8 @@ import ( func BindAuthMiddleware(r chi.Router) { r.Use( - auth.DefaultJwtHandler.HttpVerifier(), - auth.DefaultJwtHandler.HttpAuthenticator(), + auth.JWT().HttpVerifier(), + auth.JWT().HttpValidator("api"), ) } diff --git a/tests/reporter/main_test.go b/tests/reporter/main_test.go index 66e70cb07..4be9feaf7 100644 --- a/tests/reporter/main_test.go +++ b/tests/reporter/main_test.go @@ -102,7 +102,7 @@ func InitTestApp() { r = chi.NewRouter() r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - rest.MountRoutes(r) + r.Group(rest.MountRoutes(auth.JWT())) } } @@ -125,7 +125,7 @@ func newHelper(t *testing.T) helper { helpers.UpdateRBAC(h.roleID) var err error - h.token, err = auth.DefaultJwtHandler.Generate(context.Background(), h.cUser, 0) + h.token, err = auth.JWT().Generate(context.Background(), h.cUser, 0) if err != nil { panic(err) } diff --git a/tests/system/main_test.go b/tests/system/main_test.go index 52ebe9399..e1299c2d7 100644 --- a/tests/system/main_test.go +++ b/tests/system/main_test.go @@ -116,7 +116,7 @@ func InitTestApp() { r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - rest.MountRoutes(r) + r.Group(rest.MountRoutes(auth.JWT())) hh.MountHttpRoutes(r) } } @@ -142,7 +142,7 @@ func newHelper(t *testing.T) helper { h.mockPermissionsWithAccess() var err error - h.token, err = auth.DefaultJwtHandler.Generate(context.Background(), h.cUser, 0) + h.token, err = auth.JWT().Generate(context.Background(), h.cUser, 0) if err != nil { panic(err) } diff --git a/tests/system/static/idp_to_sp.cookie b/tests/system/static/idp_to_sp.cookie index 2a70246e1..c59ec16fe 100644 --- a/tests/system/static/idp_to_sp.cookie +++ b/tests/system/static/idp_to_sp.cookie @@ -1 +1 @@ -eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODQiLCJleHAiOjE2MjEyNDMwODIsImlhdCI6MTYyMTI0Mjk5MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg0IiwibmJmIjoxNjIxMjQyOTkyLCJzdWIiOiJ0Q3U1UFY2RWd4Y3ZVQWE5ZTU3dUoyZy1iVGtxbk5reXlISGFPdTE1eUVmWmpnV0t0MDJBdFhHZSIsImlkIjoiaWQtN2JmZmIyNmQ2YmVlNGI1YmRmODZmOGRjNGRhMmNmYzExN2Q4OWQwZiIsInVyaSI6Ii9hdXRoL2V4dGVybmFsL3NhbWwvaW5pdCIsInNhbWwtYXV0aG4tcmVxdWVzdCI6dHJ1ZX0.CU_jrc5gx6JhafzbekO-7VXLJU6jzDd-R4QyrtQIZN3jyqIZtYB466KiTFZnyYeEWEjK7GW18eHuzZFHmDpcQ9weOtvu9u0Z7UUDm3YQoG-6XgUeQKTV2i1uPzq1ZlT8iiMBUsn0kdKL2F18U4jl4Fss0_Ysdc3OqoEJ73xcu0P721ZSsg-vEwyooe1WMSosunN_HEmWOU2aC61uQwNFSRk5_JotdUEytko1Jzn9JeOnLllf8izr7z7JWEnBqN8845IV_zqiScrAptpAZRocAeMAbFPFPmj5_lL2SzKC4GF4lkoOIZ5vWRBVdfzIxvD21rVZK5rRlSdYUmT0txFoQw \ No newline at end of file +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODQiLCJpYXQiOjE2MjEyNDI5OTIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4NCIsIm5iZiI6MTYyMTI0Mjk5Miwic3ViIjoidEN1NVBWNkVneGN2VUFhOWU1N3VKMmctYlRrcW5Oa3l5SEhhT3UxNXlFZlpqZ1dLdDAyQXRYR2UiLCJpZCI6ImlkLTdiZmZiMjZkNmJlZTRiNWJkZjg2ZjhkYzRkYTJjZmMxMTdkODlkMGYiLCJ1cmkiOiIvYXV0aC9leHRlcm5hbC9zYW1sL2luaXQiLCJzYW1sLWF1dGhuLXJlcXVlc3QiOnRydWV9.fBI_TorEbXYMtoNRGApQs5_89Q9IZjV-1dwkOeF5ZC9xQ6p3Mbo3r0x4CgKzYS2n8i4mMIEUDI_C4bY5jqyVEfmrwtv4qGbhYCJjnvSu1vAncJGQNfbcWCmSW0RMiiJZzfj3whHTzmK_mLgOch07iwxKGBOyNscdZfxfJp_sDMuHePwiggGssWglCC_KWXNkGh3TPad-_mo4kc_9qUf4onyISms6uyZpbJW-BqGP-iYiTnbEGdbF-24bmbTpVBU8Arv3jQjaHq8teT9XI4vFgWHfEp497LD7snYNOn3-9S05JGWKA74wrgZwFcRBaIhfVMtOqy7YMHBJZ8NNbVH8Tg diff --git a/tests/system/static/idp_to_sp.post b/tests/system/static/idp_to_sp.post index 421ff50ab..d98616e85 100644 --- a/tests/system/static/idp_to_sp.post +++ b/tests/system/static/idp_to_sp.post @@ -1 +1 @@ -PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHNhbWwycDpSZXNwb25zZSBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo4MDg0L2F1dGgvZXh0ZXJuYWwvc2FtbC9jYWxsYmFjayIgSUQ9Il9mMTA1MjczOGU0NTg2ZDNhZDNhMDFmMWJmYzI1YjNjYiIgSW5SZXNwb25zZVRvPSJpZC03YmZmYjI2ZDZiZWU0YjViZGY4NmY4ZGM0ZGEyY2ZjMTE3ZDg5ZDBmIiBJc3N1ZUluc3RhbnQ9IjIwMjEtMDUtMTdUMDk6MTc6MDguMDI0WiIgVmVyc2lvbj0iMi4wIiB4bWxuczpzYW1sMnA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+PHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+aHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDwvc2FtbDI6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz48ZHM6UmVmZXJlbmNlIFVSST0iI19mMTA1MjczOGU0NTg2ZDNhZDNhMDFmMWJmYzI1YjNjYiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPlkyUTlCQUtET0R4YTFUUjB4NzMzcm5meFFPcmloZ2czOVE0VWNPa2lGUzA9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmNRRDZYY21yRS9naXlIaVNhT0VMY0s2WTFMQXkvQnN6dmFaOElYS1o1OEdjTVdUM2pPT3FZRnlUcW1pZHhaY0x2SDMyUTVZN3JKYjYwa0N2KzhWalBpUDd1QTlJeE0xMy9QRXN2WTBLYVpYWHBjclZiWHlQOFdxTWVQNHpIcUUvUW91VDh4STR2Ukg0K0FVOGpMUDdLTSt4UitZZi83YjNldC9yK0phcnJrR2tPTDJOS2kxZUdDL2VnUzhtb1VZck9NZGNJdlVIaFVFbjdrNlgzTkl4Z0lhcjA4eGFwNmNrU01PdG5FeG12U2lhdVQzZVVCaWZmVytzb3JKOWpqckN4QTlESUcyd2gwamxUTVdkZ0t3V2Q0WFdzWG5oMnh1aWg1Y0ZsMFRoNklFS3hpcjN1eHA1NFVpVFNjVVBTN2w1V2NmV0haaVdZSGZocmhkekdSclYwdz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlERWpDQ0FmcWdBd0lCQWdJVkFNRUNRMXRqZ2hhZm01T3hXRGg5aHdaZnh0aFdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1CWXhGREFTCkJnTlZCQU1NQzNOaGJXeDBaWE4wTG1sa01CNFhEVEU0TURneU5ESXhNVFF3T1ZvWERUTTRNRGd5TkRJeE1UUXdPVm93RmpFVU1CSUcKQTFVRUF3d0xjMkZ0YkhSbGMzUXVhV1F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQzBaNFFYMU5GSwpzNzF1ZmJRd29Rb1c3cWtOQUpSSUFOR0E0aU0wVGhZZ2h1bDNwQytGd3JHdjM3YVR4V1hmQTFVRzluaktiYkRyZWlEQVpLbmdDZ3lqCnhqMHVKNGxBcmdrcjRBT0VqajV6WEE4MXVHSEFSZlVCY3R2UWNzWnBCSXhET3ZVVUltQWwrM05xTGdNR0YyZmt0eE1HN2tYM0dFVk4KYzFrbGJOM2RmWXNhdzVkVXJ3MjVEaGVMOW5wN0cvKzI4R3dIUHZMYjRhcHRPaU9OYkNhVnZoOVVNSEVBOUY3YzB6ZkYvY0w1Zk9wZApWYTU0d1RJMHUxMkNzRkt0NzhoNmxFR0c1alVzL3FYOWNsWm5jSk03RUZrTjNpbVBQeSswSEM4bnNwWGlIL01aVzhvMmNxV1JrcnczCk16QlpXM09qazVuUWo0MFY2TlViamI3a2ZlanpBZ01CQUFHalZ6QlZNQjBHQTFVZERnUVdCQlFUNlk5SjNUdy9oT0djOFBOVjdKRUUKNGsyWk5UQTBCZ05WSFJFRUxUQXJnZ3R6WVcxc2RHVnpkQzVwWklZY2FIUjBjSE02THk5ellXMXNkR1Z6ZEM1cFpDOXpZVzFzTDJsawpjREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBU2szZ3VLZlRrVmhFYUlWdnhFUE5SMnczdld0M2Z3bXdKQ2NjVzk4WFhMV2dOYnUzCllhTWIyUlNuN1RoNHAzaCttZnlrMmRvbjZhdTdVeXpjMUpkMzlSTnY4MFRHNWlRb3hmQ2dwaHkxRlltbWRhU2ZPOHd2RHRIVFROaUwKQXJBeE9ZdHpmWWJ6YjVRck5OSC9nUUVOOFJKYUVmL2cvMUdUdzl4LzEwM2RTTUswUlh0bCtmUnMybmJsRDFKSktTUTNBZGh4Sy93ZQpQM2FVUHRMeFZWSjl3TU9RT2ZjeTAybCtoSE1iNnVBanNQT3BPVktxaTNNOFhtY1VaT3B4NHN3dGdHZGVvU3BlUnlydE12UndkY2NpCk5CcDlVWm9tZTQ0cVpBWUgxaXFycG1tanNmSTlwSkl0c2dXdTNrWFBqaFNmajFBSkdSMWw5Skd2SnJIa2kxaUhUQT09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWwycDpTdGF0dXMgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxzYW1sMnA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1sMnA6U3RhdHVzPjxzYW1sMjpFbmNyeXB0ZWRBc3NlcnRpb24geG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjx4ZW5jOkVuY3J5cHRlZERhdGEgSWQ9Il9lNDYyMzUzNGM4M2I1ZDExZjQzYzU2YzIzNDE3YWNmMCIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCIgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIj48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIi8+PGRzOktleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleSBJZD0iXzA2ZWVkYWNhYzIzYjI2NzcxYjMzZGFlNjllNjE0NDJiIiBSZWNpcGllbnQ9Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4NC9hdXRoL2V4dGVybmFsL3NhbWwvbWV0YWRhdGEiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlESVRDQ0FnbWdBd0lCQWdJVUIzNFhOb0lndDNrNHRMYkM0MEdEbkx1dllBa3dEUVlKS29aSWh2Y05BUUVMQlFBd0lERWVNQndHCkExVUVBd3dWYlhselpYSjJhV05sTG1WNFlXMXdiR1V1WTI5dE1CNFhEVEl4TURReU1qRTNNekl6T1ZvWERUSXlNRFF5TWpFM016SXoKT1Zvd0lERWVNQndHQTFVRUF3d1ZiWGx6WlhKMmFXTmxMbVY0WVcxd2JHVXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBcDlqRWxVc2pNM1dlVUhBOUNqR1FMb1FKbGhOT3FrdjJvS1pCdlRwcjhiQVFGNDB2UDlxR2l3clpxUmUwCnNlMXhDVG1sVk0vSEdyY3RDMEk2YUw3K1dUYW1ORXZ3UGltb1dzazFWN3pZWTdjRlcrSkVtTC9FMzc5Uis5Z2lRTEJ4SXZ2ZzdoU2QKN2ljbHVjd0c1VjZXUGVuUWl5bmVFZWdUc0VFZ0txYWdJQWdTVjl6WWpOUXJRZXM3M1dIQmo4VkNaaFRzVkVuVk8rL1ZtQ0phRXd3bwp2WldQMGxvTGViUjI5L0k0Z3IvTG1mUWZySUErajExNm81enA0NHZsMXlXOTh4cTVzbmtnYjRZQmZtNnBUSERsMnJuNHBNM2F2TCtzCksxdXN6N0hRd21Lclc2Y3BMdEtWYytTYmpmM015TFV1SHhSbWtQb1kySjU2dUoxT1dDaG9Zd0lEQVFBQm8xTXdVVEFkQmdOVkhRNEUKRmdRVVRqZy9GRUthVWpwcXptM0t6Q2crV01Vc20yb3dId1lEVlIwakJCZ3dGb0FVVGpnL0ZFS2FVanBxem0zS3pDZytXTVVzbTJvdwpEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFMWHRJV3ltSWdaRE1LSGxWek1XcS9JZC9qUDJhCkkwaHB2WmdTMkx2N1VEZHlGMWJqMU1EVE90RGtFUm9UVmgvWGJoYW81bHMrR1ZoeHZ1ZGpuWjhpcmR0bFFSSk5XVGdqNThaaXphZ08KM1N0UTJkdXdaUHl2dDlqSGdWSjhKbTl3OEpWZ3NOUDROb3RsNUdqN2RNR2h4bm00YUEyb0hTY0pvQzVndTJ1a0dBQXhnVXBqTkE3ago5Qy83aVZMemlqRnpoenRxN1hUbWJuSm11RlBSNTBycUhKY1RUWU1WRGM4UGI3M2NtakRaRHo5VTkxdis3ajVvRExWLzhraHZMc0h4Cmk4NS9HQTErajd6Nzd3V3pySXd3TmpZNFlmYmhGMEhKWlhRelhmM0VIR3h0U0MwMWhKeWZCanlYaDVIOCtOYnlBWUY4K2Q4eSt1RnUKK0dIaExTK1lhZz09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PHhlbmM6Q2lwaGVyRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjx4ZW5jOkNpcGhlclZhbHVlPldEVTBEdDFtMmxuZjEwUEg4NWkxZmxSSGpJTEhDZWdIb3djQm1tS0pWSkRxeXNGT2FXa25veUxHd2MrSHYyVGZJMjZjdEdQTjI3TGw3eTNRd09Ia2VDYWF0THJVUm5UbEdOejFGMkJabXBlY3h3VnJITGhZRDMvWVFmaWduRkNKRThZUGtkalExZ3NOcWY0VFJWaWZrMDBlUHRBN21mQnlGdVlrTVloVDQzSVF0K0lCMVR5K2I1YUh0Sk8vVytHbTVDZG9Ob2NhUlhUQVJrOWhoZ2owUDgrZlBHS2REL3FmYVJHOWsrV05CcnVVeTd4MVpwRDhWL01sRysrcS9lK3hmM09EU1FkK25weVpLdVBhRGJEaklrUG13Sk0xMmVJYmRoVjd2clpLM0F6MG9VQVNURXN4UVluL0NzZC9yczdtZlY0S05uSVljY1RuMEtDUk96N2hBdz09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kczpLZXlJbmZvPjx4ZW5jOkNpcGhlckRhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIj48eGVuYzpDaXBoZXJWYWx1ZT4vUEF6c1M4cEo3Mk9sRlJ1Vi9Qczk3TDZuZnYwN2dUL01HdkFQVFFYOVg4UHRjSnVncm05aVVaUlQzbm13UzhDaUpNRHhPTnpqVGgwREpPYWk2eFV3MjljVForcVRlQkZWNEZlYTA4MVlrRUloNGwrS3pIOHpvUjNSeUo3TkFXenhFVjEvaWVUeUFIdlk0V2pMM0ZQOXdKeFdhbmI2VEJ1UFkvMXdhU05ONVlYTGlhTHlpWWZXTFVPdXVsRjZqakx4d0JpRTdleEFPaWhmcUUrcWhLYUJQTVR2ajAvSkNKUUYyQU5aekN4SHNwMHdoK2Z3aHJ6VEJEV1BZRHFDQ1ZjK3duNXhEVTEyeSt5WVdpdk1HUkVkNTIrVTZHQ1BINTJVQ1pFV1h5QnR2VHk5MENkdFN0eDErZEpyNCtqbzBLRUpOS1VNSzl6R1ZiM01RUVVkWEJDZnl4NnRvTmJtTnhYUFF3b3ZnOUdWTGUrcjExRjc0aCtBNFBnTWE0MVozckRFeHowWHNkWHhiazFCU3poZjg2WlkrQVl4TnJXUzM0WWpnWVM0VHdZRkcrRXJVbkMySDdTZkM2TElkZFFiNTJHeGhkb3NYSVNJZnJKS011bjN4Rm1kRXhxcjU5S0VPSUtmV0J6OGpUNGdUQXk5azRMZ3lUTzh2cFd2d3ZFREx6NUczUGlxSVhHOXlBcEMwa2trblNPNUtHSENTS0RRb2VIUlM0MXRyOUdKeEFQYjZFbFBDVTc3aDludlVuVit0bVBHWklBcWY3cGF0S2gxUHpIbzRLQitPb0t6TXhiMnNHbkEra09jVXVKblJOdG5NYjlGVG9ScUdDZmYyQzBhazBXSkdTcnBvVmJqeG0vais3Yi85RENOcU1JMHo5NktLQjlVckNSU0doamhjK0c0Rm5MODVMVUpuYVZzNTBodXhEK1JUSC9tYkZ4YVpLaDJNM0pjWGZ6bFRpbzB0VDZXUWVTRG9CMUFRK2NGZmN1dlpSN1gwd2hJeEhRT0hYeWhFajArSndBUWlFWTlkd2hEMGV3U2RGWkxmcHFyR0ZMSFVRYW9DK1JaR1gvOXpobDhLcW1ySmpMbDZxb1JWU01KTWtNaU50VTBSempibVFUYkJTTU1XTkxVN3BFajgxVEdTajVoOUxVYkVwV21Oc2JWbTFoVmJ2SkkyVW0wR3A1ekZkTm5qR3JBd1RERjh4YnhYSU80RFJZMkxqSVJsZGc0b01lSnNyQkFMaVFvMjA0MnNyOGlaNDkvZ2p3azdyU1Q2VzQ0NEZndjhmSHlwSWRadS95djJub0tzZ3BRSUJneUhZaDlUQklRQUNhanAzTWNiV1ZYNDk2VzNlR2NPa0J4ODh3TjhaYTR2TTgvTVZPY1hmaFE2TFI5cXNRNXNaei9rMUcvSXJLeTdsV0JYSFk3ZXhyUzY2Wi9ENHM3ZjR6VmhXcmR2RjY0QVV4cjVWMUtGUElzQmtUM21zSExYeFNUZFlqcGprclc4YjRWTzNjdmpRMTJDbDZ1R1NDMWM4aTg5SW9ZMnJ2bkJDTnB5Q2owclM5RkZoeTRYNHVKV3YvK3BJR3owcWlOUmpuQlgyUWlsQ0llVjlOMEFNYzI3bzNCYXZqdnplM2l5d0RXUmlQcS9PYmgzbzF3T0FuRkUrbVZpNmtxNGdxQjYxMndSWEg5bURyZ0NFa1BLUFpKMUxYY0hGbHRNTEdwbDlaT2U2VzE3eVRkWUdSMi9mQmNSVFgrZHVmSHpHem9ZcTNEcGJaaVNDVFZQU1dvQWtFdDUvcDBtZktNbVQ5Z1k2REpCMEJLU3pLRldWM0FYUEVFb1FZUHlxMEdxdk5DUHAxTGQ5V3M1ZGxXbjA5TExWUjdJb0lRUnhmeE5JU0RZS1lLOFdyS3VyWHdxYkJQYlp6LzFlbTNSQi9TMjZnRVpLM0N4WnVrS0dybWZSdW9wdUxqL3Y4UjIySVZ5a1h2bE1jTkxSS3Vxa2FWZGhFTnNXc0JubEpiUVozc2IvK2dWNHBwTzRobGlKU0ZHcHhEQm9xOHJvcERqSVN0SnN5OXJnUmJoZXYvMVo1ZkFtMktYSWtHdUFvNlBNbTdpZFdoUXJseDFjRWJ6eEY0aUxjM1BqMzVaazExMHpJYWdPOC9scW1RclhuZHBsT3VjZFpFSW5hZ0FLRVVNVHpneVArbE85UVc0RHRhcXlGaDJ6cG5nekdNR096a2xQNWsyK3ZLM1gwcTR0QThYbFJsY01xMjNRK2pacjl5cmhIa1lvMEZCNG4zNFRld09YWEVaOVNLMDlHYnh6ZUVjUTBKU1VyZFRkTitORnhFRlMydkNDb2dwbGJvZGl2ZHpRR2ZDY2pPbXYyN3h5YnRFRm5uWnc5QkRTSWJNYXYxRU8rOWg2WXRyQ2ZHVUVzRVFhdkpvc1BKa05XVjB4Vmk1eUJTOVdoeElXOGhlazJaanZYOXB1eFNFMG5tT2FsRmNPMzRZY0tQZmluVXVaak1iUGczR2V1TkpwY0tVRjU1RjJ0cEdYa0lSclVZaFRmMlBrZG0yb3owVEprRkNIdStERGRWNzlub2M2cWJicmpGemJEZktydms0U2xQRTZNQThMUFBTL2Q1RHFpUHNMUy96OVRZZ1VMZE93SmtDbzFaYUdmOVZscm9oNTZtdmh6QklXVHQ3d0dVYVhLUGpJTlk0YlFpc05iUGhEeGJKOURlT2dPbFNYYnhjVVRyb1I0WjZtYThWanNPYU1aYUU5OHVxdElVRjkzMjJLUWtid2YvUys0dWNVc2xCb3Uzd3dFcElNWHVST1R3Mk9sNEZLNEl1STRRTWJ1M2Q1WXVHdm1HOUpGblJaN00rdnkzTlFJdzBoS1RRUjBrNkE0MnBkY3pUSDdWY2JMMmJxL1BXY2JPanp2UlJrMnduRW5WOEFZblAzbFRrbnBtTjlaWTN5dnVMS0NBTU9FdU9XQno3d1JWNEtUTmtHdkRqMXBCRHp2VmE0N1UvMHJyZ1Zkc3FnRFE1cnkwdUZXMDZsZDZoeklMOVlleVV1TGtldVJzdmZQV0x1Wi9Qbk01dGZQZ1h5QldSOERPeTh1NmZuZk5mYmxkV2kxNENNZERxWk9RRk5aQXJvYnpQdmNHTGlFUWp6a2pJS2orTWxvaW4wempoS1hDVGtxOEhEV1hSU0txcG9lQXBraERxTSt1bDVBM0lNa0pSZWx1SUxMK2UxdFJwaURQMDlYZndJSnU2elBMcjhIRUQ1SHZNTHpHM1BYWjFGMDhJWHNwdU1zWm1HL255L2JqMDUxVEY1NmhEcTdBblJPMEd6NFJnYks0WW96eVVwRGxWdWo2RzJkalYycHlCcHM4ZjJ5R2ZxMkNpK1oxaVpsbkFaL1JUeEtJQ0hST2hBa0FSMHd4cERsOC93aTlzTUs0SjJQb203VzZ1VWh3VVdEVFVLYUp3U1JRNXJEaE5VekRlS2ViUDUrMmV3NTlRRTlZdkIzaHFIb1ZJdGNBVlhneHhYMDZMdVJpdVl0V0tIMmQ5S244bnpzUldIamdqVTFnRjIwUXhwYm95VUN6czJwVStNQysxK3J1N3l3UTVEUnRPTUpUZlUvN3ZsMDYySmZhU1JwVHA0VkVyVWtwSEsxRXJQOWVkS3pibzUxQnE4WFZQTE1sUWlOZ0NzT3ZQcFlBU1BrSmsxanJ3dFkybkdzR0RoM3NNaG1taWZqbE11WFJEd09MMWM4WExhUkxmTFZHMFg2UUtiZHA0ZmttSERoMm1nYkViS2lUbWFEVXlWeHlObjgrb0s1Vm9oQzllWEhUSFVUSFlyWlA0UXFYczNjU2NrYU9BRVlJY2YxemQrMjR5SkVyRFEzSjJvbDRzMTNyMXpDWW9TclhNQ2draVJDY3ZSZWVVdGM0dHJWRGpYL1lNT2g0V05jS1ZTV1FrR2VXRzVnTWU0Vmc4MFQwK1FFZnhiOXZQU0V0TUFPcDNyWFh1OEhyWHFDTHN1N005Rk9jcEdMVElYL1hGM3kzeTJQcW92ekxwYmVmdUlZNjUwem9XVG1tMmxDV1N4OFM3WWhqd1FYRjdPNVhudEZteGRNWG4zdjZZM1BtdTZFMDZpaXp6WS9ScVJqckJ1UDVMKzRGTytRYVUyK25NY3JuVFVycmJuUWJtbSt3UlpaUityVXFYR1VBK0M1bmUwRGtBMzNPeHBrZlFkUzNlNXd0VjVDNDZxMzIxSStROThEdEIwTGN5a1h6amFLWlpuWmRjeWVQaTdLWlJUaFhqOEQ0cEZQWlZwWDFnUFhYUDU4Z29BdnlzWjMvZ1BaWS9Qa211b0NCcGFlQUp0MVlqemJQSzc5MkxvSHd3YWlNM0dRUWtzOXU1SDI2QjZmeUh5K0RYMVgyb2hzVGU5d2ZNalpqdUs0SElReFZsVkp2d242WUtpYVh4d2g5VjN1UE9jR0ZoUHBZZ3V3SUlObWd0Q2NRN3pSZWtMMzhBMVg4d3ZRbVM0VVNZcE0xNVJPTmZsU3dHQWh2VUtVc2wrTDE0a3VrS3liRGYxbHlwVDJpVmYyd3NGSkQ5SEZPb09FdkVXL2R3eXlaa2dLY3BlZHpWTDRuZUNrVjFrYlpQaVJWNHVxVjFwN2pDN2VlM1dUNmpBanhLZnpYTkxtcGRtdFFsOGVSWXhvR0FqRTB4dmx0MkU0ZFNkVXZtcmxpMTIrWi93NFNYREY1d0Q3SW5KSEorRVZ0a3Exdk1seXdFZjdOK2lYRElFc1FaTUdWVlB6SW1PZDhncklWYnhsaFZSanJmdFVtVmd3Z2FRM3BYUlBaaWtXeEpTZFcxaVZ5anFvZGQzaldIcHVreUFCNUJvNldBOUFYcFlwcnJMRFBmMHdlSXRGWjNVczlMYmMxbDZ0NXRhWkdaQXdEMWl1dDc4anBNbTMzNzhMRkZlR3JBeHQ3cWo0Ulp5cWhZa0xUbEhBWDlKRVFrQU1Famc3Vm5OaXRCRCtneGViY3FaQklkcW1VbFVXT2Z2OTZmc0JobmFCODlzcDB4Zmt6ejAvL0xTeHl0MklPeCtrVjdzd0l4eE50ZlBjUHg2Vzk1ckwxelRKcGNqcVQrYVBsZy9XSTJ3VXQ2MDVMbFIyTUE0end1ZkN3Q21SR0lxNjBLQTVMSEpQajNQR0grSGgvalpOZ0F2UHR5VGQ5dUthd1B0WEloNFh2UEZBb09ZRTFxdjFYK0JnSlF4c0pEcndESDhSVlN3Y1psUk4xQzNsc3d3QVpQS2xoRWFOOElvNnVYNVFrcnNvaDVOV2dua2FOMWcvM0k1MDY5MzVLTk56akZqWE9UamV3NUlLUmtNUThWMk1BTElmYzFNZjN4N3hYL2ZlcitCMjUveU0vYXVTMVZYd1Q4bFJHQVVZS0dKd0Q5Y0lCZW01a0lVNENEcE4yM0lnYU1pUUYrblR4R3dPZHNjcFFEcHdNMXF3SGZBQ08vMHc5VHVyYVdtSFM3d2NET21GQlVjcFdpeUhjZW1rang3NDdyVkppZGFOMTlWd01kRUlxQUdMSkdqZUN1ZHUyOCtzd0lERUxTc2crajdtNGcwTnNXU0NhYTU3enB5WUFBV2hNbyt6V211VG0zd1VRcXJuZEQ4ZmNkdVhmWVgxeWFlM3B3U3hkRks0WitUeHNlN1NtWHl6TndhZWpjMCthTEt1ZnlWc1VmeWlSaTI3VHF5TjM2dEFYN3ZqTkpuY2cyUTZ6dzl2ZmpqWHhRdDB5a0JWOUdtR3E3NURNS2p4bGdBYnduSUhlOW5LaHdvdXhTcWh2cU5vcVdDVUNyZGw4NUdSU0tUdmx3RndFWXJOZHRoVXhyUnA5YjZUZVZzU2tMeUs3WE1aUUFkaW1EUGppTGxNa3J4d1JDaFYyWVY1ZEEzT0NFQThUV1NIamJDWEY4VnlDYVZrZWZhWDBsdWhwTUhpbnppSys0Rm5CV0x5OUtWZ0p4NjlmR3FyL0liMjd5REU0V0FyeklKeGFIMkNJTjdQK1hXSU8yMmJNWkRLMmI2MnIrZi9RckFCNVY0UFh6OGsxd1piUzR6UnJ0OE5jN29Bd3NwTzR6R1Vyb2lkaS91dFJ6bU9ESmtISmM0b2FzU2lZRHVYMWJjdnJ2cWN6MWF4ZUN4SkgreUFtVVc4SnUvTU5pS29kN0x1RG9rMEFsa1Q2ZzMxMldmd3dmSmd0Ly9rQzJTMisyblE4UEh1TEk0b1R4OUl2aHhxdzEydGpCbHJqcnc1dlEwRE81d1M1VmZQTGNrTU9JRjl2VlNxdjNISUk0YmpDM3FqVHZUWmpoTkVtNU9vZk92aUZVdFYvWHNHdUQ1TW1RcVpJalgrSmVZdm5RQVgyOWg0NVVyWXYvK3B2NjVsT1o0VkgvaWxyR3IwNUpGMFdHT2tvdnhGNkYvVzF3ekxHbEdXSnhscjQ2NXJYUS9qQ1ZyUWJBWUFoQlE2eFVHOXczRk1FNU1CQjF6OEJGVnBOLzkzVkZBeFVsSXpxNENOaFNPbHQzVUxmemhkWHRZSzM0VEx2eEFBL09SRXpqaEt4UEJENVE5Z2JmTjhsci9OQ0tqSWFLaU82dFpteERZeVdsYTY3RjRZdUFyYWllRkFPWjU1a0I0c2kvdXBGcEo2bGJsZHh1d2tmbVliRWJSMTBQdElUNU9kbEZrZ1RCQlRsUlVRL3ZmUVhNN3M4T1I1NkNGL2JTWUVuRmVYbU1yWUluZXNRVU44VVRJd1lHTktRU090L1I2NGRLaXI2MTd3andpby9MSW81RTJTbGQ2VG9GQ2MzNFQxRGNxZk9vVTh1UHBLTEh0VWFxL0dDS0EvVklEekZVL0dJQ09MR3plejdFeEpvZGFWbTFUcUtNdjNjOE13VE1BcnN3NndwOXZpVXNUdTVUUnU4eTlMKzlGRURjVXlmV3V2SmZEK2RNa09LdktpRlJyTno3N25jbHlWNlJhQS9CVTF2b3NPMXNIMy9nRHJsUlZpSWo0ampHcE9vN3lOTS91d0poeTFxRElaQkVuTVNMWmhOWW80dkhmNjN5VFhzdlZNRWpjSDUvc0ZRTldPa0xiU25IS25xVUFzMVNqUzd2YjNPQy91a2I2UWh1NGtQV0JER2tndnVpR3RiMXdRalpqbEFPc2p3Slp5VVdGRGIvYUtjTnlqWTNTanJUWG9Ya0VJVFhQcWNBU21zc01yK05DZlhUc2x5QjRKWXVJUHcrWEQ4U3lMYy9BM2NqNWkrR0tQbjBXUWFLM2RleEVjS21YckhmREtUaHVCWHgzVXAxM1VYQlg2cXMzODBQbUxoZEI2K1ZFL29xcHpPWWZuSXhobXZXNnR2UC9DdCs2VlJVSjhVMHFsOVY5SUJFbmt5cG42dnV2OHVMeHhFRmVrU3FoK001UUZxeU53VnlZMllFdzNEK2xJamY1cERHRWdNNkhJRGFQd3pObFF5N1VDMkJYa0Q5cjZZZ25QQ2dKL2JuV29PbExzTjA1VFlGdWhpblllaXRjN2JGSDFBUk42RjRuZHJDaHo1UVRrdGtnOHBjcnhBRllRSlprblZiRG9MVEZPV3RSK3ByWS9aL3czYU9oNko3RjdRcjIxa1VoZnZhWjRpU3RMbjFtdVBSeHBLN05aakdvOFlrR2RMM3hjeUJEUUQ3T2N5QnRXRWREK0kzVGV6MCsyV21Wbk90SEpHcmN2ZVcwSnA3b2h4WklzSktMWFUzL0R5Z1Y4ZkEwTGtqampZL2QxWnU5V25RaUpqSWc5YUVjUG53VWFWWlVUdjFnTlRCaVFUbjcyTzBpMEpvVE5xWWZldEdXbFZMNmVvR3NFVkpXdUhuaVltZk1oc3BOUEsveDZjVG9DenhnU0tjQ0lMY2QyOFJxVWI3QmF2NTZUSFRtcXN2WVAvZ2dzUzN5eDZHWTU4RjliSkJKc1RtWWRBWlpvK2ZEbHBjZDhXNzBCZVhIS2pIVTRDbWVsTHZPS1hlZGtaMlB4cEpMWmxHNVZmc00vVHRzayt2cUlpSjVjYU1rcGdJZUZpUnpJWnNTckVVTWg4OVJabk5Gemx2UCswZCtNa29aUkhOR0R4UUx6T0ZTaVFGQzJ6WnpjNXhGakRZS3ZHVitpaWtIMC83c1U3ZVVHZHpXdTB5bi9PWnIzbWtzRVgrNk0zMmxoOEg3aVhvNFFoVVJ0a3o3Rk9TMHREWWh5Qy9sNCtRdmgxbFJrNDF4UERHRkxhT2d6MGM1MTdPNENQWmlDQmVRQUUreGN1SXNZSXFBNmFhd3lNQjdsVUtaL2w3UGdvMlV1eGdUN0QxYklSS0dsMXNFTTduUzdvRCtoS2p4VEx6dXdra2RhdHlMMFd1SVlQSFpNZDdlNnhBaHlzbG8rbCs2c0JOalZZN09mS2g5UzVRaWpjZDhWUU1FSHMvUmljZUVmV2lOMlhLelk0UzhROVg2dnNCTW1vUHQ0aGE5dk8vU2lpZTFpMVUyTit1SFUwNGhuUmNsMnJoVW9INnpKTDBSUFBMMXRLZE1HNHVxVzJUbTk5aDRFaTM4OHo3c1RFWFB5endlaTZLYzdpeUQxNHhPcXZGSXJEcHFWeTRLNzRVSmdBaGFnVUQ4UnUvVXdBdXF0aHl1MFdQREFodStGOHZ2QUNxOVNmaVgzd1d0NG5vQ0RlcDNuNXhwUm8zMDZ4Y3AxYURuR0dnSVZ1amdER2U2LzVZMXUvUnQxV2cvdnRaTU15cHRXODdkZDFGejc1Y21uUG5obXBMaTNxM3RKRlBoU2tiSG1rNElVaDVyQi8vVU8rbkNQMnViNmIvcXQ4UnZ0aWlFa0d5N2Z5TjNFYU1MNDlDVXBoMU1RWUVGMk9saWFtTVoyU3UyWVlXemp6Si9pV2ZoaTJnM0FnNzcvU0lZcjNyc0ZLenVLSDUrMGY3WTZ5dFYzbVFYRlFtMjhVdE9XMnJtSC9sQlptU0Y3amRnTUhTNmxhWGd1OXhIaUpWY1UxeFNWT0tWWFo0Z1VtZkNiaFlCMHBRRHhGZWpJWEw5MW55bFhwWi9lNnJ5aitKN3pOK2JMZXBFcjRqWlpPTk0zUktkTWN0UmVzdXdLV2lEa084QjJIb2VEVFJCeHJwQ1AvT0hqVTRWR1ZsUFhVU2lQQk1kWDIyT3BTbDZCdUlBY2l5UnJVK0JBbklVcWY5c2pnOUF4UHNlSTlNK2pKZmQwc1l6TUJGazhzSGx4UlUrWXVmcERzTmlrY3V6WTB1ZHBMRlh3SjZhNTN6OW5nNDU4RGJ1VGhyWjhkOVpLYndaaFd4OGJXYU9pYkx3VFQvVDZONkRWR2NUb0hUUjZ1TXh1eVRFeGV3ci9oZStmRmpvbklVVWZQM3M2MlROblJUd29wa0plSXFMQnNqYjJ6TXdXRFRQbngwV21zbFplalRpODAvenhJN01VZGtVVWxjWGU5UjNrdnQ4MHIxU0RGamdXTExRMmEvYnFGRlBsd2hzOWlyTHdzYzV3dUtWTVZPMFlEbWFUazFmMHg3ZFJpYTdyR21CcWhsM1VJVlErb0hOU2NPQjFabW5Td3VreEpkNFJYUUx0bHZNWU1UejZnblVud2tPb3Y5cXhUL0ZhTk9EYzhLRVhWalpuTTlIajhpU1lQaDROSXVFZmxlV2JlSjBYNnB3cmlQWjU4RU1JbXduTTdldVdIeVE1MU5nZlpQWkcrRlFhdklnZ3o5Syt2VGZEVEhsRFFBa3VQblc4Z0ZSdmJDeVFDM2MrVkZoYjRKUWdibTh4dE0vaWVnd0VaYU5xbVc0UnBYTWdFUDNhb1YyQ3FGamdUbzZGdGhjZ2YxbUIxMUVISmMxaXRoOWFMbkUvK01DdXhLRXJQTmZRM1ErTnNDUW5PaisydjZsVHhiSENhTTJzMml5dUk0N3AvM0pXd1hIUk9qZEE0N29HNzdzYURrZmdISWo3aDYvdExXbjUrc0QxWFM3NTYyTGpJS1JEYWVYUHZVbHUyenh4NUo1Yzl0YWZ0TExCMnkyN29OSlVsL2JkVFBBOXgzelV1SDE0c3ZMUGVzMEI4dEg1akJWaEhjVkw2bUlWNlFvT0JTMWVWVHBCYTIxRlQxM29Ka0FURnovenZDQStMSFExZFNzTDJ3Ulo1TjdtTjZOY0NTa3ZsZUxpMmJLS2VXMWFXK2o4Qmd0S0hBRzFnZ1FVdzNIUDVTOHJVNkZDQkd1QkJzR0N6Vjg1QUZQOFVONDExc3QzRVNWazgzU2ZYL0k5aTE2eGthRTY0bXk2T2ZoOHVBQ3U0OHlFdVd4ZTVWSWJNTk82Vm93U2JEZEVEN1RINlhSMjlqOUpiL0ZXQktSMjBkSC9WL2Z3bU56aHRiQUhGeGRLWDhScU5zamNtNVVqeXRlcXJtM2N0cnBHSGFRbXpUdVVlUnJTaFVQcjFkbUZLK3hqejgyVENPU010b2UrT1IxY2Z2cFBwejUvZC84cGRaWWtGSStJSkllVlFHS1BMNHFHNmhwNU44MEd2dVN2d2ZLMUMrNVdnc1ZTODdUZ1phUDEwcm5RNnVVT0xpM1cvN3pISlZtbnl2R3FmRE1uWkg3S0NxWTNid1JtalFMQUFwZjREU3dwNnRBZ0U4T2ZONUpmNEFoU2c2R1JSNUNWMFBQLzZoRnkrT3RpVFdYUy9HQVhxNlYwZ1g4ais3cGUrcXcxV1lDQnZnaktCVGhRcWNYWDBQTks5bjJBZG5qNXltRGtJMmdxZE50SXY0RzMyZzF4Rzh0MkZGbXBKdjVVM3hQMTVWRHVQT3dLMERoeno3b0lPNXpHTGdzY1A1RGduNFlua2laZ1FYVzFQdlFqTS9BUzNrODhGV0U2RzlEN3dpbkEwUzljV1ZOdmVob3pteHQwQUVTOXFUOFFRSHlabHFXQjBYMmduUExrY3Z6MG1CK0xqRUYxdlJiMnNya3czZUtRQXZqVHVwMU9rZWtOamFsaWVnMnQ2ZFVnbzRvblhlWE5WY2RVRUQ1TWx1VUEzaVFxYXlOY3ZJbThFQ0ZqckRGTzFwcjUyemlrY2UvVWdwNDd1SnRVWk9UMzZmZFdMNm42eVp3MUZ2cVlzdERmUk5mNmRGdURnQy9UYkM2M3loWVZSWFRuck5CZlVEVmZZNmVKbnI0d2pHSGZNd0xVSlp4a1NXblhsWGZPckdVclo0TzRibkI0R29Bb1RZTmdVeXoxaFFNVHkzT1hCcngyVi9kMDNWdk5Nb0dMMXBySFc3U1NyU2tOWjFOUTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWwyOkVuY3J5cHRlZEFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4= \ No newline at end of file +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHNhbWwycDpSZXNwb25zZSBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo4MDg0L2F1dGgvZXh0ZXJuYWwvc2FtbC9jYWxsYmFjayIgSUQ9Il9mMTA1MjczOGU0NTg2ZDNhZDNhMDFmMWJmYzI1YjNjYiIgSW5SZXNwb25zZVRvPSJpZC03YmZmYjI2ZDZiZWU0YjViZGY4NmY4ZGM0ZGEyY2ZjMTE3ZDg5ZDBmIiBJc3N1ZUluc3RhbnQ9IjIwMjEtMDUtMTdUMDk6MTc6MDguMDI0WiIgVmVyc2lvbj0iMi4wIiB4bWxuczpzYW1sMnA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+PHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+aHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDwvc2FtbDI6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz48ZHM6UmVmZXJlbmNlIFVSST0iI19mMTA1MjczOGU0NTg2ZDNhZDNhMDFmMWJmYzI1YjNjYiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPlkyUTlCQUtET0R4YTFUUjB4NzMzcm5meFFPcmloZ2czOVE0VWNPa2lGUzA9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmNRRDZYY21yRS9naXlIaVNhT0VMY0s2WTFMQXkvQnN6dmFaOElYS1o1OEdjTVdUM2pPT3FZRnlUcW1pZHhaY0x2SDMyUTVZN3JKYjYwa0N2KzhWalBpUDd1QTlJeE0xMy9QRXN2WTBLYVpYWHBjclZiWHlQOFdxTWVQNHpIcUUvUW91VDh4STR2Ukg0K0FVOGpMUDdLTSt4UitZZi83YjNldC9yK0phcnJrR2tPTDJOS2kxZUdDL2VnUzhtb1VZck9NZGNJdlVIaFVFbjdrNlgzTkl4Z0lhcjA4eGFwNmNrU01PdG5FeG12U2lhdVQzZVVCaWZmVytzb3JKOWpqckN4QTlESUcyd2gwamxUTVdkZ0t3V2Q0WFdzWG5oMnh1aWg1Y0ZsMFRoNklFS3hpcjN1eHA1NFVpVFNjVVBTN2w1V2NmV0haaVdZSGZocmhkekdSclYwdz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlERWpDQ0FmcWdBd0lCQWdJVkFNRUNRMXRqZ2hhZm01T3hXRGg5aHdaZnh0aFdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1CWXhGREFTCkJnTlZCQU1NQzNOaGJXeDBaWE4wTG1sa01CNFhEVEU0TURneU5ESXhNVFF3T1ZvWERUTTRNRGd5TkRJeE1UUXdPVm93RmpFVU1CSUcKQTFVRUF3d0xjMkZ0YkhSbGMzUXVhV1F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQzBaNFFYMU5GSwpzNzF1ZmJRd29Rb1c3cWtOQUpSSUFOR0E0aU0wVGhZZ2h1bDNwQytGd3JHdjM3YVR4V1hmQTFVRzluaktiYkRyZWlEQVpLbmdDZ3lqCnhqMHVKNGxBcmdrcjRBT0VqajV6WEE4MXVHSEFSZlVCY3R2UWNzWnBCSXhET3ZVVUltQWwrM05xTGdNR0YyZmt0eE1HN2tYM0dFVk4KYzFrbGJOM2RmWXNhdzVkVXJ3MjVEaGVMOW5wN0cvKzI4R3dIUHZMYjRhcHRPaU9OYkNhVnZoOVVNSEVBOUY3YzB6ZkYvY0w1Zk9wZApWYTU0d1RJMHUxMkNzRkt0NzhoNmxFR0c1alVzL3FYOWNsWm5jSk03RUZrTjNpbVBQeSswSEM4bnNwWGlIL01aVzhvMmNxV1JrcnczCk16QlpXM09qazVuUWo0MFY2TlViamI3a2ZlanpBZ01CQUFHalZ6QlZNQjBHQTFVZERnUVdCQlFUNlk5SjNUdy9oT0djOFBOVjdKRUUKNGsyWk5UQTBCZ05WSFJFRUxUQXJnZ3R6WVcxc2RHVnpkQzVwWklZY2FIUjBjSE02THk5ellXMXNkR1Z6ZEM1cFpDOXpZVzFzTDJsawpjREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBU2szZ3VLZlRrVmhFYUlWdnhFUE5SMnczdld0M2Z3bXdKQ2NjVzk4WFhMV2dOYnUzCllhTWIyUlNuN1RoNHAzaCttZnlrMmRvbjZhdTdVeXpjMUpkMzlSTnY4MFRHNWlRb3hmQ2dwaHkxRlltbWRhU2ZPOHd2RHRIVFROaUwKQXJBeE9ZdHpmWWJ6YjVRck5OSC9nUUVOOFJKYUVmL2cvMUdUdzl4LzEwM2RTTUswUlh0bCtmUnMybmJsRDFKSktTUTNBZGh4Sy93ZQpQM2FVUHRMeFZWSjl3TU9RT2ZjeTAybCtoSE1iNnVBanNQT3BPVktxaTNNOFhtY1VaT3B4NHN3dGdHZGVvU3BlUnlydE12UndkY2NpCk5CcDlVWm9tZTQ0cVpBWUgxaXFycG1tanNmSTlwSkl0c2dXdTNrWFBqaFNmajFBSkdSMWw5Skd2SnJIa2kxaUhUQT09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWwycDpTdGF0dXMgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxzYW1sMnA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1sMnA6U3RhdHVzPjxzYW1sMjpFbmNyeXB0ZWRBc3NlcnRpb24geG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjx4ZW5jOkVuY3J5cHRlZERhdGEgSWQ9Il9lNDYyMzUzNGM4M2I1ZDExZjQzYzU2YzIzNDE3YWNmMCIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCIgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIj48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIi8+PGRzOktleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleSBJZD0iXzA2ZWVkYWNhYzIzYjI2NzcxYjMzZGFlNjllNjE0NDJiIiBSZWNpcGllbnQ9Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4NC9hdXRoL2V4dGVybmFsL3NhbWwvbWV0YWRhdGEiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlESVRDQ0FnbWdBd0lCQWdJVUIzNFhOb0lndDNrNHRMYkM0MEdEbkx1dllBa3dEUVlKS29aSWh2Y05BUUVMQlFBd0lERWVNQndHCkExVUVBd3dWYlhselpYSjJhV05sTG1WNFlXMXdiR1V1WTI5dE1CNFhEVEl4TURReU1qRTNNekl6T1ZvWERUSXlNRFF5TWpFM016SXoKT1Zvd0lERWVNQndHQTFVRUF3d1ZiWGx6WlhKMmFXTmxMbVY0WVcxd2JHVXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBcDlqRWxVc2pNM1dlVUhBOUNqR1FMb1FKbGhOT3FrdjJvS1pCdlRwcjhiQVFGNDB2UDlxR2l3clpxUmUwCnNlMXhDVG1sVk0vSEdyY3RDMEk2YUw3K1dUYW1ORXZ3UGltb1dzazFWN3pZWTdjRlcrSkVtTC9FMzc5Uis5Z2lRTEJ4SXZ2ZzdoU2QKN2ljbHVjd0c1VjZXUGVuUWl5bmVFZWdUc0VFZ0txYWdJQWdTVjl6WWpOUXJRZXM3M1dIQmo4VkNaaFRzVkVuVk8rL1ZtQ0phRXd3bwp2WldQMGxvTGViUjI5L0k0Z3IvTG1mUWZySUErajExNm81enA0NHZsMXlXOTh4cTVzbmtnYjRZQmZtNnBUSERsMnJuNHBNM2F2TCtzCksxdXN6N0hRd21Lclc2Y3BMdEtWYytTYmpmM015TFV1SHhSbWtQb1kySjU2dUoxT1dDaG9Zd0lEQVFBQm8xTXdVVEFkQmdOVkhRNEUKRmdRVVRqZy9GRUthVWpwcXptM0t6Q2crV01Vc20yb3dId1lEVlIwakJCZ3dGb0FVVGpnL0ZFS2FVanBxem0zS3pDZytXTVVzbTJvdwpEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFMWHRJV3ltSWdaRE1LSGxWek1XcS9JZC9qUDJhCkkwaHB2WmdTMkx2N1VEZHlGMWJqMU1EVE90RGtFUm9UVmgvWGJoYW81bHMrR1ZoeHZ1ZGpuWjhpcmR0bFFSSk5XVGdqNThaaXphZ08KM1N0UTJkdXdaUHl2dDlqSGdWSjhKbTl3OEpWZ3NOUDROb3RsNUdqN2RNR2h4bm00YUEyb0hTY0pvQzVndTJ1a0dBQXhnVXBqTkE3ago5Qy83aVZMemlqRnpoenRxN1hUbWJuSm11RlBSNTBycUhKY1RUWU1WRGM4UGI3M2NtakRaRHo5VTkxdis3ajVvRExWLzhraHZMc0h4Cmk4NS9HQTErajd6Nzd3V3pySXd3TmpZNFlmYmhGMEhKWlhRelhmM0VIR3h0U0MwMWhKeWZCanlYaDVIOCtOYnlBWUY4K2Q4eSt1RnUKK0dIaExTK1lhZz09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PHhlbmM6Q2lwaGVyRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjx4ZW5jOkNpcGhlclZhbHVlPldEVTBEdDFtMmxuZjEwUEg4NWkxZmxSSGpJTEhDZWdIb3djQm1tS0pWSkRxeXNGT2FXa25veUxHd2MrSHYyVGZJMjZjdEdQTjI3TGw3eTNRd09Ia2VDYWF0THJVUm5UbEdOejFGMkJabXBlY3h3VnJITGhZRDMvWVFmaWduRkNKRThZUGtkalExZ3NOcWY0VFJWaWZrMDBlUHRBN21mQnlGdVlrTVloVDQzSVF0K0lCMVR5K2I1YUh0Sk8vVytHbTVDZG9Ob2NhUlhUQVJrOWhoZ2owUDgrZlBHS2REL3FmYVJHOWsrV05CcnVVeTd4MVpwRDhWL01sRysrcS9lK3hmM09EU1FkK25weVpLdVBhRGJEaklrUG13Sk0xMmVJYmRoVjd2clpLM0F6MG9VQVNURXN4UVluL0NzZC9yczdtZlY0S05uSVljY1RuMEtDUk96N2hBdz09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kczpLZXlJbmZvPjx4ZW5jOkNpcGhlckRhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIj48eGVuYzpDaXBoZXJWYWx1ZT4vUEF6c1M4cEo3Mk9sRlJ1Vi9Qczk3TDZuZnYwN2dUL01HdkFQVFFYOVg4UHRjSnVncm05aVVaUlQzbm13UzhDaUpNRHhPTnpqVGgwREpPYWk2eFV3MjljVForcVRlQkZWNEZlYTA4MVlrRUloNGwrS3pIOHpvUjNSeUo3TkFXenhFVjEvaWVUeUFIdlk0V2pMM0ZQOXdKeFdhbmI2VEJ1UFkvMXdhU05ONVlYTGlhTHlpWWZXTFVPdXVsRjZqakx4d0JpRTdleEFPaWhmcUUrcWhLYUJQTVR2ajAvSkNKUUYyQU5aekN4SHNwMHdoK2Z3aHJ6VEJEV1BZRHFDQ1ZjK3duNXhEVTEyeSt5WVdpdk1HUkVkNTIrVTZHQ1BINTJVQ1pFV1h5QnR2VHk5MENkdFN0eDErZEpyNCtqbzBLRUpOS1VNSzl6R1ZiM01RUVVkWEJDZnl4NnRvTmJtTnhYUFF3b3ZnOUdWTGUrcjExRjc0aCtBNFBnTWE0MVozckRFeHowWHNkWHhiazFCU3poZjg2WlkrQVl4TnJXUzM0WWpnWVM0VHdZRkcrRXJVbkMySDdTZkM2TElkZFFiNTJHeGhkb3NYSVNJZnJKS011bjN4Rm1kRXhxcjU5S0VPSUtmV0J6OGpUNGdUQXk5azRMZ3lUTzh2cFd2d3ZFREx6NUczUGlxSVhHOXlBcEMwa2trblNPNUtHSENTS0RRb2VIUlM0MXRyOUdKeEFQYjZFbFBDVTc3aDludlVuVit0bVBHWklBcWY3cGF0S2gxUHpIbzRLQitPb0t6TXhiMnNHbkEra09jVXVKblJOdG5NYjlGVG9ScUdDZmYyQzBhazBXSkdTcnBvVmJqeG0vais3Yi85RENOcU1JMHo5NktLQjlVckNSU0doamhjK0c0Rm5MODVMVUpuYVZzNTBodXhEK1JUSC9tYkZ4YVpLaDJNM0pjWGZ6bFRpbzB0VDZXUWVTRG9CMUFRK2NGZmN1dlpSN1gwd2hJeEhRT0hYeWhFajArSndBUWlFWTlkd2hEMGV3U2RGWkxmcHFyR0ZMSFVRYW9DK1JaR1gvOXpobDhLcW1ySmpMbDZxb1JWU01KTWtNaU50VTBSempibVFUYkJTTU1XTkxVN3BFajgxVEdTajVoOUxVYkVwV21Oc2JWbTFoVmJ2SkkyVW0wR3A1ekZkTm5qR3JBd1RERjh4YnhYSU80RFJZMkxqSVJsZGc0b01lSnNyQkFMaVFvMjA0MnNyOGlaNDkvZ2p3azdyU1Q2VzQ0NEZndjhmSHlwSWRadS95djJub0tzZ3BRSUJneUhZaDlUQklRQUNhanAzTWNiV1ZYNDk2VzNlR2NPa0J4ODh3TjhaYTR2TTgvTVZPY1hmaFE2TFI5cXNRNXNaei9rMUcvSXJLeTdsV0JYSFk3ZXhyUzY2Wi9ENHM3ZjR6VmhXcmR2RjY0QVV4cjVWMUtGUElzQmtUM21zSExYeFNUZFlqcGprclc4YjRWTzNjdmpRMTJDbDZ1R1NDMWM4aTg5SW9ZMnJ2bkJDTnB5Q2owclM5RkZoeTRYNHVKV3YvK3BJR3owcWlOUmpuQlgyUWlsQ0llVjlOMEFNYzI3bzNCYXZqdnplM2l5d0RXUmlQcS9PYmgzbzF3T0FuRkUrbVZpNmtxNGdxQjYxMndSWEg5bURyZ0NFa1BLUFpKMUxYY0hGbHRNTEdwbDlaT2U2VzE3eVRkWUdSMi9mQmNSVFgrZHVmSHpHem9ZcTNEcGJaaVNDVFZQU1dvQWtFdDUvcDBtZktNbVQ5Z1k2REpCMEJLU3pLRldWM0FYUEVFb1FZUHlxMEdxdk5DUHAxTGQ5V3M1ZGxXbjA5TExWUjdJb0lRUnhmeE5JU0RZS1lLOFdyS3VyWHdxYkJQYlp6LzFlbTNSQi9TMjZnRVpLM0N4WnVrS0dybWZSdW9wdUxqL3Y4UjIySVZ5a1h2bE1jTkxSS3Vxa2FWZGhFTnNXc0JubEpiUVozc2IvK2dWNHBwTzRobGlKU0ZHcHhEQm9xOHJvcERqSVN0SnN5OXJnUmJoZXYvMVo1ZkFtMktYSWtHdUFvNlBNbTdpZFdoUXJseDFjRWJ6eEY0aUxjM1BqMzVaazExMHpJYWdPOC9scW1RclhuZHBsT3VjZFpFSW5hZ0FLRVVNVHpneVArbE85UVc0RHRhcXlGaDJ6cG5nekdNR096a2xQNWsyK3ZLM1gwcTR0QThYbFJsY01xMjNRK2pacjl5cmhIa1lvMEZCNG4zNFRld09YWEVaOVNLMDlHYnh6ZUVjUTBKU1VyZFRkTitORnhFRlMydkNDb2dwbGJvZGl2ZHpRR2ZDY2pPbXYyN3h5YnRFRm5uWnc5QkRTSWJNYXYxRU8rOWg2WXRyQ2ZHVUVzRVFhdkpvc1BKa05XVjB4Vmk1eUJTOVdoeElXOGhlazJaanZYOXB1eFNFMG5tT2FsRmNPMzRZY0tQZmluVXVaak1iUGczR2V1TkpwY0tVRjU1RjJ0cEdYa0lSclVZaFRmMlBrZG0yb3owVEprRkNIdStERGRWNzlub2M2cWJicmpGemJEZktydms0U2xQRTZNQThMUFBTL2Q1RHFpUHNMUy96OVRZZ1VMZE93SmtDbzFaYUdmOVZscm9oNTZtdmh6QklXVHQ3d0dVYVhLUGpJTlk0YlFpc05iUGhEeGJKOURlT2dPbFNYYnhjVVRyb1I0WjZtYThWanNPYU1aYUU5OHVxdElVRjkzMjJLUWtid2YvUys0dWNVc2xCb3Uzd3dFcElNWHVST1R3Mk9sNEZLNEl1STRRTWJ1M2Q1WXVHdm1HOUpGblJaN00rdnkzTlFJdzBoS1RRUjBrNkE0MnBkY3pUSDdWY2JMMmJxL1BXY2JPanp2UlJrMnduRW5WOEFZblAzbFRrbnBtTjlaWTN5dnVMS0NBTU9FdU9XQno3d1JWNEtUTmtHdkRqMXBCRHp2VmE0N1UvMHJyZ1Zkc3FnRFE1cnkwdUZXMDZsZDZoeklMOVlleVV1TGtldVJzdmZQV0x1Wi9Qbk01dGZQZ1h5QldSOERPeTh1NmZuZk5mYmxkV2kxNENNZERxWk9RRk5aQXJvYnpQdmNHTGlFUWp6a2pJS2orTWxvaW4wempoS1hDVGtxOEhEV1hSU0txcG9lQXBraERxTSt1bDVBM0lNa0pSZWx1SUxMK2UxdFJwaURQMDlYZndJSnU2elBMcjhIRUQ1SHZNTHpHM1BYWjFGMDhJWHNwdU1zWm1HL255L2JqMDUxVEY1NmhEcTdBblJPMEd6NFJnYks0WW96eVVwRGxWdWo2RzJkalYycHlCcHM4ZjJ5R2ZxMkNpK1oxaVpsbkFaL1JUeEtJQ0hST2hBa0FSMHd4cERsOC93aTlzTUs0SjJQb203VzZ1VWh3VVdEVFVLYUp3U1JRNXJEaE5VekRlS2ViUDUrMmV3NTlRRTlZdkIzaHFIb1ZJdGNBVlhneHhYMDZMdVJpdVl0V0tIMmQ5S244bnpzUldIamdqVTFnRjIwUXhwYm95VUN6czJwVStNQysxK3J1N3l3UTVEUnRPTUpUZlUvN3ZsMDYySmZhU1JwVHA0VkVyVWtwSEsxRXJQOWVkS3pibzUxQnE4WFZQTE1sUWlOZ0NzT3ZQcFlBU1BrSmsxanJ3dFkybkdzR0RoM3NNaG1taWZqbE11WFJEd09MMWM4WExhUkxmTFZHMFg2UUtiZHA0ZmttSERoMm1nYkViS2lUbWFEVXlWeHlObjgrb0s1Vm9oQzllWEhUSFVUSFlyWlA0UXFYczNjU2NrYU9BRVlJY2YxemQrMjR5SkVyRFEzSjJvbDRzMTNyMXpDWW9TclhNQ2draVJDY3ZSZWVVdGM0dHJWRGpYL1lNT2g0V05jS1ZTV1FrR2VXRzVnTWU0Vmc4MFQwK1FFZnhiOXZQU0V0TUFPcDNyWFh1OEhyWHFDTHN1N005Rk9jcEdMVElYL1hGM3kzeTJQcW92ekxwYmVmdUlZNjUwem9XVG1tMmxDV1N4OFM3WWhqd1FYRjdPNVhudEZteGRNWG4zdjZZM1BtdTZFMDZpaXp6WS9ScVJqckJ1UDVMKzRGTytRYVUyK25NY3JuVFVycmJuUWJtbSt3UlpaUityVXFYR1VBK0M1bmUwRGtBMzNPeHBrZlFkUzNlNXd0VjVDNDZxMzIxSStROThEdEIwTGN5a1h6amFLWlpuWmRjeWVQaTdLWlJUaFhqOEQ0cEZQWlZwWDFnUFhYUDU4Z29BdnlzWjMvZ1BaWS9Qa211b0NCcGFlQUp0MVlqemJQSzc5MkxvSHd3YWlNM0dRUWtzOXU1SDI2QjZmeUh5K0RYMVgyb2hzVGU5d2ZNalpqdUs0SElReFZsVkp2d242WUtpYVh4d2g5VjN1UE9jR0ZoUHBZZ3V3SUlObWd0Q2NRN3pSZWtMMzhBMVg4d3ZRbVM0VVNZcE0xNVJPTmZsU3dHQWh2VUtVc2wrTDE0a3VrS3liRGYxbHlwVDJpVmYyd3NGSkQ5SEZPb09FdkVXL2R3eXlaa2dLY3BlZHpWTDRuZUNrVjFrYlpQaVJWNHVxVjFwN2pDN2VlM1dUNmpBanhLZnpYTkxtcGRtdFFsOGVSWXhvR0FqRTB4dmx0MkU0ZFNkVXZtcmxpMTIrWi93NFNYREY1d0Q3SW5KSEorRVZ0a3Exdk1seXdFZjdOK2lYRElFc1FaTUdWVlB6SW1PZDhncklWYnhsaFZSanJmdFVtVmd3Z2FRM3BYUlBaaWtXeEpTZFcxaVZ5anFvZGQzaldIcHVreUFCNUJvNldBOUFYcFlwcnJMRFBmMHdlSXRGWjNVczlMYmMxbDZ0NXRhWkdaQXdEMWl1dDc4anBNbTMzNzhMRkZlR3JBeHQ3cWo0Ulp5cWhZa0xUbEhBWDlKRVFrQU1Famc3Vm5OaXRCRCtneGViY3FaQklkcW1VbFVXT2Z2OTZmc0JobmFCODlzcDB4Zmt6ejAvL0xTeHl0MklPeCtrVjdzd0l4eE50ZlBjUHg2Vzk1ckwxelRKcGNqcVQrYVBsZy9XSTJ3VXQ2MDVMbFIyTUE0end1ZkN3Q21SR0lxNjBLQTVMSEpQajNQR0grSGgvalpOZ0F2UHR5VGQ5dUthd1B0WEloNFh2UEZBb09ZRTFxdjFYK0JnSlF4c0pEcndESDhSVlN3Y1psUk4xQzNsc3d3QVpQS2xoRWFOOElvNnVYNVFrcnNvaDVOV2dua2FOMWcvM0k1MDY5MzVLTk56akZqWE9UamV3NUlLUmtNUThWMk1BTElmYzFNZjN4N3hYL2ZlcitCMjUveU0vYXVTMVZYd1Q4bFJHQVVZS0dKd0Q5Y0lCZW01a0lVNENEcE4yM0lnYU1pUUYrblR4R3dPZHNjcFFEcHdNMXF3SGZBQ08vMHc5VHVyYVdtSFM3d2NET21GQlVjcFdpeUhjZW1rang3NDdyVkppZGFOMTlWd01kRUlxQUdMSkdqZUN1ZHUyOCtzd0lERUxTc2crajdtNGcwTnNXU0NhYTU3enB5WUFBV2hNbyt6V211VG0zd1VRcXJuZEQ4ZmNkdVhmWVgxeWFlM3B3U3hkRks0WitUeHNlN1NtWHl6TndhZWpjMCthTEt1ZnlWc1VmeWlSaTI3VHF5TjM2dEFYN3ZqTkpuY2cyUTZ6dzl2ZmpqWHhRdDB5a0JWOUdtR3E3NURNS2p4bGdBYnduSUhlOW5LaHdvdXhTcWh2cU5vcVdDVUNyZGw4NUdSU0tUdmx3RndFWXJOZHRoVXhyUnA5YjZUZVZzU2tMeUs3WE1aUUFkaW1EUGppTGxNa3J4d1JDaFYyWVY1ZEEzT0NFQThUV1NIamJDWEY4VnlDYVZrZWZhWDBsdWhwTUhpbnppSys0Rm5CV0x5OUtWZ0p4NjlmR3FyL0liMjd5REU0V0FyeklKeGFIMkNJTjdQK1hXSU8yMmJNWkRLMmI2MnIrZi9RckFCNVY0UFh6OGsxd1piUzR6UnJ0OE5jN29Bd3NwTzR6R1Vyb2lkaS91dFJ6bU9ESmtISmM0b2FzU2lZRHVYMWJjdnJ2cWN6MWF4ZUN4SkgreUFtVVc4SnUvTU5pS29kN0x1RG9rMEFsa1Q2ZzMxMldmd3dmSmd0Ly9rQzJTMisyblE4UEh1TEk0b1R4OUl2aHhxdzEydGpCbHJqcnc1dlEwRE81d1M1VmZQTGNrTU9JRjl2VlNxdjNISUk0YmpDM3FqVHZUWmpoTkVtNU9vZk92aUZVdFYvWHNHdUQ1TW1RcVpJalgrSmVZdm5RQVgyOWg0NVVyWXYvK3B2NjVsT1o0VkgvaWxyR3IwNUpGMFdHT2tvdnhGNkYvVzF3ekxHbEdXSnhscjQ2NXJYUS9qQ1ZyUWJBWUFoQlE2eFVHOXczRk1FNU1CQjF6OEJGVnBOLzkzVkZBeFVsSXpxNENOaFNPbHQzVUxmemhkWHRZSzM0VEx2eEFBL09SRXpqaEt4UEJENVE5Z2JmTjhsci9OQ0tqSWFLaU82dFpteERZeVdsYTY3RjRZdUFyYWllRkFPWjU1a0I0c2kvdXBGcEo2bGJsZHh1d2tmbVliRWJSMTBQdElUNU9kbEZrZ1RCQlRsUlVRL3ZmUVhNN3M4T1I1NkNGL2JTWUVuRmVYbU1yWUluZXNRVU44VVRJd1lHTktRU090L1I2NGRLaXI2MTd3andpby9MSW81RTJTbGQ2VG9GQ2MzNFQxRGNxZk9vVTh1UHBLTEh0VWFxL0dDS0EvVklEekZVL0dJQ09MR3plejdFeEpvZGFWbTFUcUtNdjNjOE13VE1BcnN3NndwOXZpVXNUdTVUUnU4eTlMKzlGRURjVXlmV3V2SmZEK2RNa09LdktpRlJyTno3N25jbHlWNlJhQS9CVTF2b3NPMXNIMy9nRHJsUlZpSWo0ampHcE9vN3lOTS91d0poeTFxRElaQkVuTVNMWmhOWW80dkhmNjN5VFhzdlZNRWpjSDUvc0ZRTldPa0xiU25IS25xVUFzMVNqUzd2YjNPQy91a2I2UWh1NGtQV0JER2tndnVpR3RiMXdRalpqbEFPc2p3Slp5VVdGRGIvYUtjTnlqWTNTanJUWG9Ya0VJVFhQcWNBU21zc01yK05DZlhUc2x5QjRKWXVJUHcrWEQ4U3lMYy9BM2NqNWkrR0tQbjBXUWFLM2RleEVjS21YckhmREtUaHVCWHgzVXAxM1VYQlg2cXMzODBQbUxoZEI2K1ZFL29xcHpPWWZuSXhobXZXNnR2UC9DdCs2VlJVSjhVMHFsOVY5SUJFbmt5cG42dnV2OHVMeHhFRmVrU3FoK001UUZxeU53VnlZMllFdzNEK2xJamY1cERHRWdNNkhJRGFQd3pObFF5N1VDMkJYa0Q5cjZZZ25QQ2dKL2JuV29PbExzTjA1VFlGdWhpblllaXRjN2JGSDFBUk42RjRuZHJDaHo1UVRrdGtnOHBjcnhBRllRSlprblZiRG9MVEZPV3RSK3ByWS9aL3czYU9oNko3RjdRcjIxa1VoZnZhWjRpU3RMbjFtdVBSeHBLN05aakdvOFlrR2RMM3hjeUJEUUQ3T2N5QnRXRWREK0kzVGV6MCsyV21Wbk90SEpHcmN2ZVcwSnA3b2h4WklzSktMWFUzL0R5Z1Y4ZkEwTGtqampZL2QxWnU5V25RaUpqSWc5YUVjUG53VWFWWlVUdjFnTlRCaVFUbjcyTzBpMEpvVE5xWWZldEdXbFZMNmVvR3NFVkpXdUhuaVltZk1oc3BOUEsveDZjVG9DenhnU0tjQ0lMY2QyOFJxVWI3QmF2NTZUSFRtcXN2WVAvZ2dzUzN5eDZHWTU4RjliSkJKc1RtWWRBWlpvK2ZEbHBjZDhXNzBCZVhIS2pIVTRDbWVsTHZPS1hlZGtaMlB4cEpMWmxHNVZmc00vVHRzayt2cUlpSjVjYU1rcGdJZUZpUnpJWnNTckVVTWg4OVJabk5Gemx2UCswZCtNa29aUkhOR0R4UUx6T0ZTaVFGQzJ6WnpjNXhGakRZS3ZHVitpaWtIMC83c1U3ZVVHZHpXdTB5bi9PWnIzbWtzRVgrNk0zMmxoOEg3aVhvNFFoVVJ0a3o3Rk9TMHREWWh5Qy9sNCtRdmgxbFJrNDF4UERHRkxhT2d6MGM1MTdPNENQWmlDQmVRQUUreGN1SXNZSXFBNmFhd3lNQjdsVUtaL2w3UGdvMlV1eGdUN0QxYklSS0dsMXNFTTduUzdvRCtoS2p4VEx6dXdra2RhdHlMMFd1SVlQSFpNZDdlNnhBaHlzbG8rbCs2c0JOalZZN09mS2g5UzVRaWpjZDhWUU1FSHMvUmljZUVmV2lOMlhLelk0UzhROVg2dnNCTW1vUHQ0aGE5dk8vU2lpZTFpMVUyTit1SFUwNGhuUmNsMnJoVW9INnpKTDBSUFBMMXRLZE1HNHVxVzJUbTk5aDRFaTM4OHo3c1RFWFB5endlaTZLYzdpeUQxNHhPcXZGSXJEcHFWeTRLNzRVSmdBaGFnVUQ4UnUvVXdBdXF0aHl1MFdQREFodStGOHZ2QUNxOVNmaVgzd1d0NG5vQ0RlcDNuNXhwUm8zMDZ4Y3AxYURuR0dnSVZ1amdER2U2LzVZMXUvUnQxV2cvdnRaTU15cHRXODdkZDFGejc1Y21uUG5obXBMaTNxM3RKRlBoU2tiSG1rNElVaDVyQi8vVU8rbkNQMnViNmIvcXQ4UnZ0aWlFa0d5N2Z5TjNFYU1MNDlDVXBoMU1RWUVGMk9saWFtTVoyU3UyWVlXemp6Si9pV2ZoaTJnM0FnNzcvU0lZcjNyc0ZLenVLSDUrMGY3WTZ5dFYzbVFYRlFtMjhVdE9XMnJtSC9sQlptU0Y3amRnTUhTNmxhWGd1OXhIaUpWY1UxeFNWT0tWWFo0Z1VtZkNiaFlCMHBRRHhGZWpJWEw5MW55bFhwWi9lNnJ5aitKN3pOK2JMZXBFcjRqWlpPTk0zUktkTWN0UmVzdXdLV2lEa084QjJIb2VEVFJCeHJwQ1AvT0hqVTRWR1ZsUFhVU2lQQk1kWDIyT3BTbDZCdUlBY2l5UnJVK0JBbklVcWY5c2pnOUF4UHNlSTlNK2pKZmQwc1l6TUJGazhzSGx4UlUrWXVmcERzTmlrY3V6WTB1ZHBMRlh3SjZhNTN6OW5nNDU4RGJ1VGhyWjhkOVpLYndaaFd4OGJXYU9pYkx3VFQvVDZONkRWR2NUb0hUUjZ1TXh1eVRFeGV3ci9oZStmRmpvbklVVWZQM3M2MlROblJUd29wa0plSXFMQnNqYjJ6TXdXRFRQbngwV21zbFplalRpODAvenhJN01VZGtVVWxjWGU5UjNrdnQ4MHIxU0RGamdXTExRMmEvYnFGRlBsd2hzOWlyTHdzYzV3dUtWTVZPMFlEbWFUazFmMHg3ZFJpYTdyR21CcWhsM1VJVlErb0hOU2NPQjFabW5Td3VreEpkNFJYUUx0bHZNWU1UejZnblVud2tPb3Y5cXhUL0ZhTk9EYzhLRVhWalpuTTlIajhpU1lQaDROSXVFZmxlV2JlSjBYNnB3cmlQWjU4RU1JbXduTTdldVdIeVE1MU5nZlpQWkcrRlFhdklnZ3o5Syt2VGZEVEhsRFFBa3VQblc4Z0ZSdmJDeVFDM2MrVkZoYjRKUWdibTh4dE0vaWVnd0VaYU5xbVc0UnBYTWdFUDNhb1YyQ3FGamdUbzZGdGhjZ2YxbUIxMUVISmMxaXRoOWFMbkUvK01DdXhLRXJQTmZRM1ErTnNDUW5PaisydjZsVHhiSENhTTJzMml5dUk0N3AvM0pXd1hIUk9qZEE0N29HNzdzYURrZmdISWo3aDYvdExXbjUrc0QxWFM3NTYyTGpJS1JEYWVYUHZVbHUyenh4NUo1Yzl0YWZ0TExCMnkyN29OSlVsL2JkVFBBOXgzelV1SDE0c3ZMUGVzMEI4dEg1akJWaEhjVkw2bUlWNlFvT0JTMWVWVHBCYTIxRlQxM29Ka0FURnovenZDQStMSFExZFNzTDJ3Ulo1TjdtTjZOY0NTa3ZsZUxpMmJLS2VXMWFXK2o4Qmd0S0hBRzFnZ1FVdzNIUDVTOHJVNkZDQkd1QkJzR0N6Vjg1QUZQOFVONDExc3QzRVNWazgzU2ZYL0k5aTE2eGthRTY0bXk2T2ZoOHVBQ3U0OHlFdVd4ZTVWSWJNTk82Vm93U2JEZEVEN1RINlhSMjlqOUpiL0ZXQktSMjBkSC9WL2Z3bU56aHRiQUhGeGRLWDhScU5zamNtNVVqeXRlcXJtM2N0cnBHSGFRbXpUdVVlUnJTaFVQcjFkbUZLK3hqejgyVENPU010b2UrT1IxY2Z2cFBwejUvZC84cGRaWWtGSStJSkllVlFHS1BMNHFHNmhwNU44MEd2dVN2d2ZLMUMrNVdnc1ZTODdUZ1phUDEwcm5RNnVVT0xpM1cvN3pISlZtbnl2R3FmRE1uWkg3S0NxWTNid1JtalFMQUFwZjREU3dwNnRBZ0U4T2ZONUpmNEFoU2c2R1JSNUNWMFBQLzZoRnkrT3RpVFdYUy9HQVhxNlYwZ1g4ais3cGUrcXcxV1lDQnZnaktCVGhRcWNYWDBQTks5bjJBZG5qNXltRGtJMmdxZE50SXY0RzMyZzF4Rzh0MkZGbXBKdjVVM3hQMTVWRHVQT3dLMERoeno3b0lPNXpHTGdzY1A1RGduNFlua2laZ1FYVzFQdlFqTS9BUzNrODhGV0U2RzlEN3dpbkEwUzljV1ZOdmVob3pteHQwQUVTOXFUOFFRSHlabHFXQjBYMmduUExrY3Z6MG1CK0xqRUYxdlJiMnNya3czZUtRQXZqVHVwMU9rZWtOamFsaWVnMnQ2ZFVnbzRvblhlWE5WY2RVRUQ1TWx1VUEzaVFxYXlOY3ZJbThFQ0ZqckRGTzFwcjUyemlrY2UvVWdwNDd1SnRVWk9UMzZmZFdMNm42eVp3MUZ2cVlzdERmUk5mNmRGdURnQy9UYkM2M3loWVZSWFRuck5CZlVEVmZZNmVKbnI0d2pHSGZNd0xVSlp4a1NXblhsWGZPckdVclo0TzRibkI0R29Bb1RZTmdVeXoxaFFNVHkzT1hCcngyVi9kMDNWdk5Nb0dMMXBySFc3U1NyU2tOWjFOUTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWwyOkVuY3J5cHRlZEFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4=