Template files added
parent
41132baf23
commit
e77ddb5d5e
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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", "")
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 µservice, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue