Template files added

main
Tom Peltonen 2023-01-10 22:24:51 +11:00
parent 41132baf23
commit e77ddb5d5e
9 changed files with 1034 additions and 0 deletions

View File

@ -0,0 +1,274 @@
package authn51
import (
"errors"
"log"
"os"
"fmt"
"time"
"context"
"net/http"
"strings"
"gopkg.in/yaml.v2"
"github.com/dgrijalva/jwt-go"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/getkin/kin-openapi/openapi3filter"
)
type Authn51 struct {
Environment string
Providers []string
AuthnFunc openapi3filter.AuthenticationFunc
Bearer struct {
Secret string
DefaultUser string
}
Basic struct {
DefaultUser string
Password string
}
Apikey struct {
ApiValue string
}
}
type Configuration struct {
Environment string `yaml:"environment"`
Authn51 struct {
Bearer struct {
Secret string `yaml:"secret"`
DefaultUser string `yaml:"default_user"`
} `yaml:"bearer"`
Basic struct {
DefaultUser string `yaml:"default_user"`
Password string `yaml:"password"`
} `yaml:"basic"`
Apikey struct {
ApiValue string `yaml:"api_value"`
} `yaml:"api_key"`
} `yaml:"authn51"`
}
type AuthenticationToken struct {
Environment string `yaml:"environment"`
Token string `yaml:"token"`
Claims struct {
UserId string `yaml:"user_id"`
Exp string `yaml:"exp"`
} `yaml:"claims"`
}
func NewAuthn51(configPath string) (*Authn51, error) {
var authn51 Authn51
var config Configuration
file, err := os.Open(configPath)
if err != nil {
return nil, err
}
defer file.Close()
d := yaml.NewDecoder(file)
if err := d.Decode(&config); err != nil {
return nil, err
}
authn51.Bearer.DefaultUser = config.Authn51.Bearer.DefaultUser
authn51.Bearer.Secret = config.Authn51.Bearer.Secret
authn51.AuthnFunc = authn51.AuthenticationFunc
if (authn51.Bearer.Secret == "%ACCESS_SECRET%") {
authn51.Bearer.Secret = os.Getenv("%ACCESS_SECRET%")
}
if (authn51.Bearer.Secret == "") {
log.Printf("No secret supplied. Generating random value")
authn51.Bearer.Secret = uuid.New().String()
}
return &authn51, nil
}
func (self *Authn51) CreateToken(userId string) (AuthenticationToken, error) {
var err error
var authToken AuthenticationToken
exp := time.Now().Add(time.Minute * 20)
atClaims := jwt.MapClaims{}
atClaims["authorized"] = true
atClaims["organization"] = "any"
atClaims["user_id"] = userId
atClaims["exp"] = exp.Unix()
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
token, err := at.SignedString([]byte(self.Bearer.Secret))
if err != nil {
return authToken, err
}
authToken.Token = token
authToken.Claims.UserId = userId
authToken.Claims.Exp = exp.String()
return authToken, nil
}
func (self *Authn51) extractToken(r *http.Request) string {
bearToken := r.Header.Get("Authorization")
strArr := strings.Split(bearToken, " ")
if len(strArr) == 2 {
return strArr[1]
}
return ""
}
func (self *Authn51) verifyToken(r *http.Request) (*jwt.Token, error) {
tokenString := self.extractToken(r)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
//Make sure that the token method conform to "SigningMethodHMAC"
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(self.Bearer.Secret), nil
})
if err != nil {
return nil, err
}
return token, nil
}
func (self *Authn51) tokenValid(r *http.Request) error {
token, err := self.verifyToken(r)
if err != nil {
return err
}
if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
return err
}
return nil
}
type AccessDetails struct {
Organization string
UserId string
}
func (self *Authn51) ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) {
token, err := self.verifyToken(r)
if err != nil {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if ok && token.Valid {
organization, ok := claims["organization"].(string)
if !ok {
return nil, fmt.Errorf("Extract error with missing '%s'", "organization")
}
userId, ok := claims["user_id"].(string)
if !ok {
return nil, fmt.Errorf("Extract error with missing '%s'", "user_id")
}
return &AccessDetails{
Organization: organization,
UserId: userId,
}, nil
}
return nil, fmt.Errorf("Token extract error: %s", "unknown")
}
func (self *Authn51) AuthenticationFunc(ctx context.Context, ai *openapi3filter.AuthenticationInput) error {
schemeN := ai.SecuritySchemeName
if (schemeN == "") {
log.Print("Scheme name is blank")
}
req := ai.RequestValidationInput.Request
if (ai.SecurityScheme.Scheme == "basic") {
userName, password, ok := req.BasicAuth()
if (ok){
if (userName == "joe" && password == "secret") {
return nil
}
}
err := errors.New("Basic authentication failed!")
msg := fmt.Errorf("security requirement '%q' failed", schemeN)
return &echo.HTTPError{
Code: http.StatusUnauthorized,
Message: msg,
Internal: err,
}
}
if (ai.SecurityScheme.Scheme == "bearer") {
ad, err := self.ExtractTokenMetadata(req)
if (err != nil || ad == nil) {
log.Print("Bearer authentication failed extraction!")
err := errors.New("Bearer authentication failed!")
msg := fmt.Errorf("security requirement '%q' failed", schemeN)
return &echo.HTTPError{
Code: http.StatusUnauthorized,
Message: msg,
Internal: err,
}
}
if (ad.UserId != "") {
return nil
}
log.Print("Bearer authentication failed user id!")
err = errors.New("Bearer authentication failed!")
msg := fmt.Errorf("security requirement %q failed", schemeN)
return &echo.HTTPError{
Code: http.StatusUnauthorized,
Message: msg,
Internal: err,
}
}
if (ai.SecurityScheme.Type == "apiKey") {
auth := req.Header.Get("X-ApiKey")
if auth == "secret" {
return nil
}
err := errors.New("API key authentication failed!")
msg := fmt.Errorf("security requirement '%q' failed", schemeN)
return &echo.HTTPError{
Code: http.StatusUnauthorized,
Message: msg,
Internal: err,
}
}
log.Printf("security requirement '%q' failed", schemeN)
err := errors.New("Authentication failed!")
msg := fmt.Errorf("security requirement '%q' failed", schemeN)
return &echo.HTTPError{
Code: http.StatusUnauthorized,
Message: msg,
Internal: err,
}
}

View File

@ -0,0 +1,185 @@
package api
import (
"flag"
"fmt"
"os"
"log"
"time"
"gopkg.in/yaml.v2"
)
type SoftwareConfiguration struct {
configFile string
// Execution environment
// If not psecified or recognised then assume product
// for safey. Vales are:
// - production
// - staging
// - testing
// - development
Environment string `yaml:"environment"`
Server struct {
// IP Address to bind the HTTP Server to
Host string `yaml:"host"`
// TCP Port to bind the HTTP Server to
Port int `yaml:"port"`
// Service base path
BasePath string `yaml:"base_path"`
// Timeout vaues
Timeout struct {
// Server is the general server timeout to use
// for graceful shutdowns
Server time.Duration `yaml:"server"`
// Write is the amount of time to wait until an HTTP server
// write opperation is cancelled
Write time.Duration `yaml:"write"`
// Read is the amount of time to wait until an HTTP server
// read operation is cancelled
Read time.Duration `yaml:"read"`
// Read is the amount of time to wait
// until an IDLE HTTP session is closed
Idle time.Duration `yaml:"idle"`
} `yaml:"timeout"`
} `yaml:"server"`
Folders struct {
// Data folder
Data string `yaml:"data"`
// Static HTLM, CSS folder
Static string `yaml:"static"`
// Configuration folder
Config string `yaml:"config"`
} `yaml:"folders"`
DataSource struct {
// Load products on start up
Load bool `yaml:"load"`
// Overwrte existing product data
Overwrite bool `yaml:"overwrite"`
} `yaml:"dataSource"`
Api struct {
// Deafult page size for lists
PageSize int `yaml:"page_size"`
} `yaml:"api"`
Hosts struct {
// Product protocol, host name and port, e.g.http://localhost:8075
Product string `yaml:"product"`
// Portfolio, account protocol, host name and port, e.g.http://localhost:8076
Protfolio string `yaml:"portfolio"`
// Quote protocol, host name and port, e.g.http://localhost:8077
Quote string `yaml:"quote"`
// Order managemet protocol, host name and port, e.g.http://localhost:8077
Order string `yaml:"order"`
// Search protocol, host name and port, e.g.http://localhost:8075
Search string `yaml:"search"`
} `yaml:"hosts"`
Support struct {
// Message verbose level, 1=minimum message, 5=All messages
Level int `yaml:"level"`
} `yaml:"support"`
}
func NewSoftwareConfiguration() *SoftwareConfiguration {
var sc = SoftwareConfiguration{}
sc.Environment = "production"
sc.Server.BasePath = "/"
sc.DataSource.Load = true
sc.DataSource.Overwrite = false
sc.Folders.Static = "./static"
sc.Folders.Data = "./data"
sc.Folders.Config = "./config"
sc.Support.Level= 1
sc.Api.PageSize = 20
return &sc
}
func (self SoftwareConfiguration) GetConfigurationFile() string {
return self.configFile
}
func LoadConfig(configPath string) (*SoftwareConfiguration, error) {
config := &SoftwareConfiguration{}
file, err := os.Open(configPath)
if err != nil {
return nil, err
}
defer file.Close()
d := yaml.NewDecoder(file)
if err := d.Decode(&config); err != nil {
return nil, err
}
config.configFile = configPath
if (config.Environment == "") {
config.Environment = "production"
}
// Perform some validation
if (config.Environment != "production" &&
config.Environment != "staging" &&
config.Environment != "testing" &&
config.Environment != "development") {
log.Printf("Environment value '%s' not valid. Reverting to defauult", config.Environment)
config.Environment = "production"
}
if (config.Api.PageSize < 1) {
config.Api.PageSize = 20
}
return config, nil
}
func ParseFlags(configPath string) (*SoftwareConfiguration, error) {
flag.StringVar(&configPath, "config", configPath, "path to config file")
var port = flag.Int("port", 0, "Port for HTTP server micro service")
var dataFolder = flag.String("data", "", "Data folder")
var staticFolder = flag.String("static", "", "Static folder")
flag.Parse()
fileStat, err := os.Stat(configPath)
if err != nil {
return nil, err
}
if fileStat.IsDir() {
return nil, fmt.Errorf("'%s' is a directory, not a normal file", configPath)
}
config, err := LoadConfig(configPath)
if err != nil {
return nil, err
}
if (*port > 0) {
config.Server.Port = *port
}
if (*dataFolder != "") {
config.Folders.Data = *dataFolder
}
if (*staticFolder != "") {
config.Folders.Static = *staticFolder
}
return config, nil
}

View File

@ -0,0 +1,27 @@
package errorh
// A common error payload returned
// when the response code is not 2xx
type ErrorModel struct {
// Error description, that shuld be less technical
// and more user orientated where possible
Message string `json:"message,omitempty"`
// Information on how the error or issue may
// be resolved.
Resolution string `json:"resolution,omitempty"`
// Status identifier that can be used to identify the
// cause of the error.
//
// It s not the HTTP status code (eg 4xx ot 5xx)
Status string `json:"status,omitempty"`
// Technical information for the error.
//
// This must not contain sensitive information
Technical string `json:"technical,omitempty"`
}

View File

@ -0,0 +1,93 @@
package errorh
import (
"errors"
"github.com/labstack/echo/v4"
"net/http"
"fmt"
)
type (
httpErrorHandler struct {
statusCodes map[error]int
}
)
func NewHttpErrorHandler() *httpErrorHandler {
return &httpErrorHandler{}
}
func (self *httpErrorHandler) getStatusCode(err error) int {
for key, value := range self.statusCodes {
if errors.Is(err, key) {
return value
}
}
return http.StatusInternalServerError
}
func unwrapRecursive(err error) error {
var originalErr = err
for originalErr != nil {
var internalErr = errors.Unwrap(originalErr)
if internalErr == nil {
break
}
originalErr = internalErr
}
return originalErr
}
func (self *httpErrorHandler) Handler(err error, c echo.Context) {
technical := ""
he, ok := err.(*echo.HTTPError)
if ok {
if he.Internal != nil {
if herr, ok := he.Internal.(*echo.HTTPError); ok {
he = herr
}
technical = fmt.Sprintf("%v", he.Internal)
}
} else {
he = &echo.HTTPError{
Code: self.getStatusCode(err),
Message: unwrapRecursive(err).Error(),
}
}
code := he.Code
stdError := ErrorModel{
Message: fmt.Sprintf("%v", he.Message),
Resolution: "",
Status: fmt.Sprintf("DM00%d", code),
Technical: technical,
}
if (code == 401) {
stdError.Resolution = "Please provide authentication details. These can be API key, Basic Auth, Bearer Auth."
}
if (code == 400) {
if (stdError.Message == "no matching operation was found") {
stdError.Resolution = "Please check the Operation name and resource and correct to a valid name. Reference the Open API specification for valid names"
}
}
// Send response
if !c.Response().Committed {
if c.Request().Method == http.MethodHead {
err = c.NoContent(he.Code)
} else {
err = c.JSON(code, stdError)
}
if err != nil {
c.Echo().Logger.Error(err)
}
}
}

View File

@ -0,0 +1,20 @@
package api
import (
"net/http"
"github.com/labstack/echo/v4"
"{{moduleName}}/api/gen"
)
func (self *BianApiService) {{servicesign}} {
self.Lock.Lock()
defer self.Lock.Unlock()
message := "{{funcName}} service API not implemented"
status := "DM00501"
return sendGeneralError(ctx, http.StatusNotImplemented, message, status, "Implement the API code", "")
}

View File

@ -0,0 +1,98 @@
package api
import (
"time"
"net/http"
"github.com/labstack/echo/v4"
"{{moduleName}}/api/gen"
)
// Service domain health
// (GET /health)
func (self *BianApiService) GetHealth(ctx echo.Context) error {
self.Lock.Lock()
defer self.Lock.Unlock()
description := "API for XXX"
serviceName := ""
health := gen.HealthModel{
Description: &description,
ServiceId: &serviceName,
Status: gen.PASS,
}
health.Links = []gen.LinksModel{}
hmc := self.newHealthModelChecks()
asAt := time.Now()
{
var items1 []gen.HealthcheckModel
status1 := "PASS"
cid1 := "local-file-store"
cty1 := "datastore"
item1 := gen.HealthcheckModel{
AsAt: &asAt,
Status: &status1,
ComponentId: &cid1,
ComponentType: &cty1,
}
items1 = append(items1,item1)
status2 := "PASS"
cid2 := "process-server"
cty2 := "system"
item2 := gen.HealthcheckModel{
AsAt: &asAt,
Status: &status2,
ComponentId: &cid2,
ComponentType: &cty2,
}
items1 = append(items1,item2)
hmc.Set("dependencies", items1)
}
{
var items []gen.HealthcheckModel
status1 := "PASS"
cid1 := "process-server"
cty1 := "system"
observedValue := float32(asAt.Sub(self.StartTime).Seconds())
observedUnit := "s"
item := gen.HealthcheckModel{
AsAt: &asAt,
Status: &status1,
ComponentId: &cid1,
ComponentType: &cty1,
ObservedValue: &observedValue,
ObservedUnit: &observedUnit,
}
items = append(items,item)
hmc.Set("uptime", items)
}
health.Checks = hmc
return ctx.JSON(http.StatusOK, health)
}
func (self *BianApiService) newHealthModelChecks() gen.HealthModel_Checks {
obj := gen.HealthModel_Checks{}
return obj
}

View File

@ -0,0 +1,175 @@
package api
import (
"log"
"sync"
"time"
"net/http"
"github.com/labstack/echo/v4"
"{{moduleName}}/api/gen"
"{{moduleName}}/api/authn51"
)
type BianApiService struct {
Lock sync.Mutex
StartTime time.Time
Configuration SoftwareConfiguration
Authn *authn51.Authn51
DataFolder string
StatusCode int
}
func NewMicroService(serverConfig SoftwareConfiguration) (*BianApiService, error) {
var microservice = BianApiService{
StartTime: time.Now(),
DataFolder: serverConfig.Folders.Data,
StatusCode: 200,
}
microservice.Configuration = serverConfig
aut, err := authn51.NewAuthn51(serverConfig.GetConfigurationFile())
if (err != nil) {
log.Printf("Error creating Authn51: %v", err)
}
microservice.Authn = aut
if (microservice.Configuration.Support.Level > 2) {
log.Printf("Base path: %s", microservice.Configuration.Server.BasePath)
}
// Prime the security
if (!microservice.IsProductionEnvironment()) {
token, _ := microservice.Authn.CreateToken(microservice.Authn.Bearer.DefaultUser)
log.Printf("token: %s", token)
}
return &microservice, nil
}
func sendGeneralErrorBrief(ctx echo.Context, code int, message string) error {
return sendGeneralError(ctx, code, message, "", "", "")
}
func sendGeneralError(ctx echo.Context, code int, message string, status string, resolution string, technical string) error {
generalError := gen.ErrorModel{
Message: &message,
Status: &status,
Resolution: &resolution,
Technical: &technical,
}
return ctx.JSON(code, generalError)
}
func setLinkBrief(href string, rel string) gen.LinksModel {
mt := "application/json"
op := gen.GET
return setLink(href, rel, mt, op, "")
}
func setLink(href string, rel string, mediaType string, operation gen.LinksModelOperation, caption string) gen.LinksModel {
linkHref := href
linkRel := rel
mt := mediaType
if (mt == "") {
mt = "application/json"
}
op := operation
if (op == "") {
op = gen.GET
}
cap := caption
var link = gen.LinksModel {
Href: linkHref,
MediaType: &mt,
Rel: linkRel,
Operation: &op,
Caption: &cap,
}
return link
}
func (microservice *BianApiService) IsProductionEnvironment() bool {
if (microservice.Configuration.Environment == "development" || microservice.Configuration.Environment == "testing") {
return false
} else {
return true
}
}
func (self *BianApiService) RegisterHandlers(e *echo.Echo) {
gen.RegisterHandlersWithBaseURL(e, self, self.Configuration.Server.BasePath)
if (!self.IsProductionEnvironment()) {
if (self.Configuration.Support.Level > 2) {
log.Print("Enabling non production routes for debugging")
}
e.Static((self.Configuration.Server.BasePath+ "/"), self.Configuration.Folders.Static)
e.GET((self.Configuration.Server.BasePath+ "/authn/new-token"), self.NewToken)
e.GET((self.Configuration.Server.BasePath+ "/authn/refresh-token"), self.RefreshToken)
}
if (self.Configuration.Support.Level > 2) {
log.Print("Echo routes are:")
for _, route := range e.Routes() {
log.Printf(" Route: %s %s %s", route.Method, route.Path, route.Name)
}
}
}
func (self *BianApiService) BrowseSkipper(path string) bool {
if (!self.IsProductionEnvironment()) {
return browseSkipper(self.Configuration.Server.BasePath, path)
} else {
return false
}
}
func (self *BianApiService) NewToken(ctx echo.Context) error {
if (self.IsProductionEnvironment()) {
return ctx.JSON(http.StatusForbidden, "")
} else {
token, _ := self.Authn.CreateToken(self.Authn.Bearer.DefaultUser)
log.Printf("New token: %s", token)
return ctx.JSON(http.StatusOK, token)
}
}
func (self *BianApiService) RefreshToken(ctx echo.Context) error {
if (self.IsProductionEnvironment()) {
return ctx.JSON(http.StatusForbidden, "")
} else {
ad, err := self.Authn.ExtractTokenMetadata(ctx.Request())
if (err != nil) {
return ctx.JSON(http.StatusBadRequest, "")
} else {
token, _ := self.Authn.CreateToken(ad.UserId)
log.Printf("Refresh token: %s \n %s", ad.UserId, token )
return ctx.JSON(http.StatusOK, token)
}
}
}

30
src/api/utility.go 100644
View File

@ -0,0 +1,30 @@
package api
import (
"strings"
)
func browseSkipper(baseUrl string, path string) bool {
if strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".json") {
return true
}
if strings.HasSuffix(path, ".ico") {
return true
}
if path == (baseUrl + "/") {
return true
}
if strings.HasPrefix(path, (baseUrl+"/authn/")) {
return true
}
if strings.HasPrefix(path, (baseUrl+"/assets/")) {
return true
}
return false
}

132
src/servermain.go 100644
View File

@ -0,0 +1,132 @@
package main
import (
"fmt"
"os"
"time"
"os/signal"
"log"
"net/http"
"context"
"github.com/getkin/kin-openapi/openapi3"
"github.com/deepmap/oapi-codegen/pkg/middleware"
"github.com/labstack/echo/v4"
echomiddleware "github.com/labstack/echo/v4/middleware"
"{{moduleName}}/api"
"{{moduleName}}/api/gen"
"{{moduleName}}/api/errorh"
)
func Run(e *echo.Echo, addressPort string, timeOut time.Duration, msgLevel int) {
var runChan = make(chan os.Signal, 1)
// Set up a context to allow for graceful server shutdowns in the event
// of an OS interrupt (defers the cancel just in case)
ctx, cancel := context.WithTimeout(
context.Background(),
timeOut,
)
defer cancel()
// Handle ctrl+c/ctrl+x interrupt
signal.Notify(runChan, os.Interrupt)
if (msgLevel > 0) {
log.Printf("Server is starting on %s\n", addressPort)
}
go func() {
if err := e.Start(addressPort); err != nil {
if err == http.ErrServerClosed {
// Normal interrupt operation, ignore
} else {
log.Fatalf("Server failed to start due to err: %v", err)
}
}
}()
interrupt := <-runChan
if (msgLevel > 0) {
log.Printf("Server is shutting down due to %+v\n", interrupt)
}
if err := e.Server.Shutdown(ctx); err != nil {
if ("interrupt" != fmt.Sprintf("%v", interrupt)) {
log.Printf("Server was unable to gracefully shutdown due to err: '%+v'", err)
}
}
}
func main() {
configPath := "../config/{{appShort}}.yaml"
softwareConfig, err := api.ParseFlags(configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading configuration,flags\n")
fmt.Fprintf(os.Stderr, "Error detail: %v\n", err)
os.Exit(1)
}
if (softwareConfig.Support.Level > 2) {
log.Printf("Message verbosity level: %d", softwareConfig.Support.Level)
}
swagger, err := gen.GetSwagger()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading Open API spec\n: %s", err)
os.Exit(1)
}
// Create an instance of our handler which satisfies the generated interface
var microService, _ = api.NewMicroService(*softwareConfig)
// Adjust servers to fit configuration
swagger.Servers = nil
server := openapi3.Server{
URL: microService.Configuration.Server.BasePath,
}
swagger.AddServer(&server)
e := echo.New()
// Log all requests
e.Use(echomiddleware.Logger())
// Custom error handling
e.HTTPErrorHandler = errorh.NewHttpErrorHandler().Handler
var options middleware.Options
if (!microService.IsProductionEnvironment()) {
options.Skipper = func(c echo.Context) bool {
return microService.BrowseSkipper(c.Request().URL.Path)
}
}
options.Options.AuthenticationFunc = microService.Authn.AuthnFunc
// Use our validation middleware to check all requests against the
// OpenAPI schema.
e.Use(middleware.OapiRequestValidatorWithOptions(swagger, &options))
e.Use(echomiddleware.GzipWithConfig(echomiddleware.GzipConfig{
Level: 3,
}))
microService.RegisterHandlers(e)
if (microService.Configuration.Support.Level < 1) {
e.HideBanner = true
}
e.Server.ReadTimeout = microService.Configuration.Server.Timeout.Read * time.Second
e.Server.WriteTimeout = microService.Configuration.Server.Timeout.Write * time.Second
e.Server.IdleTimeout = microService.Configuration.Server.Timeout.Idle * time.Second
addressPort := fmt.Sprintf("%s:%d", microService.Configuration.Server.Host, microService.Configuration.Server.Port)
timeOut := microService.Configuration.Server.Timeout.Server * time.Second
// And we serve HTTP until the world ends.
Run(e, addressPort, timeOut, microService.Configuration.Support.Level)
}