Skip to content

Commit

Permalink
Merge branch 'dev' into feature/WEOS-1342
Browse files Browse the repository at this point in the history
# Conflicts:
#	model/content_entity_test.go
  • Loading branch information
RandyDeo committed Mar 2, 2022
2 parents aaf46b3 + f49bc18 commit 790ed0c
Show file tree
Hide file tree
Showing 37 changed files with 4,244 additions and 82 deletions.
1 change: 1 addition & 0 deletions .github/workflows/dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ env:
SLACK_ICON: https://github.com/wepala.png?size=48
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: copyright 2022 Wepala
OAUTH_TEST_KEY: ${{ secrets.OAUTH_TEST_KEY }}

jobs:
build-api:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ env:
SLACK_ICON: https://github.com/wepala.png?size=48
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: copyright 2022 Wepala
OAUTH_TEST_KEY: ${{ secrets.OAUTH_TEST_KEY }}

jobs:
build-api:
Expand Down
6 changes: 3 additions & 3 deletions api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ components:
type: string
nullable: true
enum:
- null
- unpublished
- published
- "null"
- one
- two
image:
type: string
format: byte
Expand Down
64 changes: 64 additions & 0 deletions conf/tasks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
openapi: 3.0.3
info:
title: Tasks API
description: Tasks API
version: 1.0.0
servers:
- url: 'http://localhost:8681'
x-weos-config:
database:
driver: sqlite3
database: e2e.db
components:
securitySchemes:
Auth0:
type: openIdConnect
openIdConnectUrl: https://samples.auth0.com/.well-known/openid-configuration
schemas:
Task:
type: object
properties:
title:
type: string
security:
- Auth0: ["email","name"]
paths:
/tasks:
get:
description: Get a list of tasks
operationId: getTasks
security:
- Auth0: []
responses:
200:
description: List of tasks
content:
application/json:
schema:
type: object
properties:
total:
type: integer
page:
type: integer
items:
$ref: "#/components/schemas/Task"

post:
description: Create task
operationId: createTask
requestBody:
description: Task
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Task"
application/x-www-form-urlencoded:
schema:
$ref: "#/components/schemas/Task"
responses:
201:
description: Created task


1 change: 1 addition & 0 deletions context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const SORTS ContextKey = "_sorts"
const PAYLOAD ContextKey = "_payload"
const SEQUENCE_NO string = "sequence_no"
const RESPONSE_PREFIX string = "_httpstatus"
const AUTHORIZATION string = "Authorization"

//Path initializers are run per path and can be used to configure routes that are not defined in the open api spec
const METHODS_FOUND ContextKey = "_methods_found"
Expand Down
87 changes: 74 additions & 13 deletions controllers/rest/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type RESTAPI struct {
eventStores map[string]model.EventRepository
commandDispatchers map[string]model.CommandDispatcher
projections map[string]projections.Projection
globalInitializers []GlobalInitializer
operationInitializers []OperationInitializer
registeredInitializers map[reflect.Value]int
prePathInitializers []PathInitializer
Expand Down Expand Up @@ -121,6 +122,20 @@ func (p *RESTAPI) RegisterEventStore(name string, repository model.EventReposito
p.eventStores[name] = repository
}

//RegisterGlobalInitializer add global initializer if it's not already there
func (p *RESTAPI) RegisterGlobalInitializer(initializer GlobalInitializer) {
if p.registeredInitializers == nil {
p.registeredInitializers = make(map[reflect.Value]int)
}
//only add initializer if it doesn't already exist
tpoint := reflect.ValueOf(initializer)
if _, ok := p.registeredInitializers[tpoint]; !ok {
p.globalInitializers = append(p.globalInitializers, initializer)
p.registeredInitializers[tpoint] = len(p.globalInitializers)
}

}

//RegisterOperationInitializer add operation initializer if it's not already there
func (p *RESTAPI) RegisterOperationInitializer(initializer OperationInitializer) {
if p.registeredInitializers == nil {
Expand Down Expand Up @@ -245,6 +260,11 @@ func (p *RESTAPI) GetProjection(name string) (projections.Projection, error) {
return nil, fmt.Errorf("projection '%s' not found", name)
}

//GetGlobalInitializers get global intializers in the order they were registered
func (p *RESTAPI) GetGlobalInitializers() []GlobalInitializer {
return p.globalInitializers
}

//GetOperationInitializers get operation intializers in the order they were registered
func (p *RESTAPI) GetOperationInitializers() []OperationInitializer {
return p.operationInitializers
Expand Down Expand Up @@ -277,24 +297,24 @@ const SWAGGERUIENDPOINT = "/_discover/"
const SWAGGERJSONENDPOINT = "/_discover_json"

//RegisterSwaggerAPI creates default swagger api from binary
func (p *RESTAPI) RegisterDefaultSwaggerAPI() error {
func (p *RESTAPI) RegisterDefaultSwaggerAPI(pathMiddleware []echo.MiddlewareFunc) error {
statikFS, err := fs.New()
if err != nil {
return NewControllerError("Got an error formatting response", err, http.StatusInternalServerError)
}
static := http.FileServer(statikFS)
sh := http.StripPrefix(SWAGGERUIENDPOINT, static)
handler := echo.WrapHandler(sh)
p.e.GET(SWAGGERUIENDPOINT+"*", handler)
p.e.GET(SWAGGERUIENDPOINT+"*", handler, pathMiddleware...)

return nil
}

//RegisterDefaultSwaggerJson registers a default swagger json response
func (p *RESTAPI) RegisterDefaultSwaggerJSON() error {
func (p *RESTAPI) RegisterDefaultSwaggerJSON(pathMiddleware []echo.MiddlewareFunc) error {
p.e.GET(SWAGGERJSONENDPOINT, func(c echo.Context) error {
return c.JSON(http.StatusOK, p.Swagger)
})
}, pathMiddleware...)
return nil
}

Expand All @@ -312,13 +332,16 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {

//register standard middleware
p.RegisterMiddleware("Context", Context)
p.RegisterMiddleware("OpenIDMiddleware", OpenIDMiddleware)
p.RegisterMiddleware("CreateMiddleware", CreateMiddleware)
p.RegisterMiddleware("CreateBatchMiddleware", CreateBatchMiddleware)
p.RegisterMiddleware("UpdateMiddleware", UpdateMiddleware)
p.RegisterMiddleware("ListMiddleware", ListMiddleware)
p.RegisterMiddleware("ViewMiddleware", ViewMiddleware)
p.RegisterMiddleware("DeleteMiddleware", DeleteMiddleware)
p.RegisterMiddleware("Recover", Recover)
//register standard global initializers
p.RegisterGlobalInitializer(Security)
//register standard operation initializers
p.RegisterOperationInitializer(ContextInitializer)
p.RegisterOperationInitializer(EntityFactoryInitializer)
Expand All @@ -328,10 +351,6 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
//register standard post path initializers
p.RegisterPostPathInitializer(CORsInitializer)

//make default endpoints for returning swagger configuration to user
p.RegisterDefaultSwaggerAPI()
p.RegisterDefaultSwaggerJSON()

//these are the dynamic struct builders for the schemas in the OpenAPI
var schemas map[string]ds.Builder

Expand All @@ -356,6 +375,40 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
}
p.RegisterProjection("Default", defaultProjection)
}

//This will check the enum types on run and output an error
for _, scheme := range p.Swagger.Components.Schemas {
for pName, prop := range scheme.Value.Properties {
if prop.Value.Enum != nil {
t := prop.Value.Type
for _, v := range prop.Value.Enum {
switch t {
case "string":
if reflect.TypeOf(v).String() != "string" {
return fmt.Errorf("Expected field: %s, of type %s, to have enum options of the same type", pName, t)
}
case "integer":
if reflect.TypeOf(v).String() != "float64" {
if v.(string) == "null" {
continue
} else {
return fmt.Errorf("Expected field: %s, of type %s, to have enum options of the same type", pName, t)
}
}
case "number":
if reflect.TypeOf(v).String() != "float64" {
if v.(string) == "null" {
continue
} else {
return fmt.Errorf("Expected field: %s, of type %s, to have enum options of the same type", pName, t)
}
}
}
}
}
}
}

//get the database schema
schemas = CreateSchema(ctxt, p.EchoInstance(), p.Swagger)
p.Schemas = schemas
Expand Down Expand Up @@ -450,12 +503,20 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
//setup routes
knownActions := []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE", "CONNECT"}
var err error
globalContext := context.Background()
//run global initializers
for _, initializer := range p.GetGlobalInitializers() {
globalContext, err = initializer(globalContext, p, p.Swagger)
if err != nil {
return err
}
}
for path, pathData := range p.Swagger.Paths {
var methodsFound []string
pathContext := context.Background()

//run pre path initializers
for _, initializer := range p.GetPrePathInitializers() {
pathContext, err = initializer(pathContext, p, path, p.Swagger, pathData)
globalContext, err = initializer(globalContext, p, path, p.Swagger, pathData)
if err != nil {
return err
}
Expand All @@ -465,7 +526,7 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
operationData := pathData.GetOperation(strings.ToUpper(method))
if operationData != nil {
methodsFound = append(methodsFound, strings.ToUpper(method))
operationContext := context.WithValue(context.Background(), weoscontext.SCHEMA_BUILDERS, schemas) //TODO fix this because this feels hacky
operationContext := context.WithValue(globalContext, weoscontext.SCHEMA_BUILDERS, schemas) //TODO fix this because this feels hacky
for _, initializer := range p.GetOperationInitializers() {
operationContext, err = initializer(operationContext, p, path, method, p.Swagger, pathData, operationData)
if err != nil {
Expand All @@ -476,9 +537,9 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
}

//run post path initializers
pathContext = context.WithValue(pathContext, weoscontext.METHODS_FOUND, methodsFound)
globalContext = context.WithValue(globalContext, weoscontext.METHODS_FOUND, methodsFound)
for _, initializer := range p.GetPostPathInitializers() {
pathContext, err = initializer(pathContext, p, path, p.Swagger, pathData)
globalContext, err = initializer(globalContext, p, path, p.Swagger, pathData)
}
//output registered endpoints for debugging purposes
for _, route := range p.EchoInstance().Routes() {
Expand Down
1 change: 1 addition & 0 deletions controllers/rest/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ func TestRESTAPI_Initialize_RequiredField(t *testing.T) {
body := bytes.NewReader(reqBytes)
resp := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/blogs", body)
req.Header.Set("Content-Type", "application/json")
e.ServeHTTP(resp, req)
if resp.Result().StatusCode != http.StatusBadRequest {
t.Errorf("expected the response code to be %d, got %d", http.StatusBadRequest, resp.Result().StatusCode)
Expand Down
92 changes: 92 additions & 0 deletions controllers/rest/controller_standard.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/coreos/go-oidc/v3/oidc"
"net/http"
"strconv"
"strings"
"time"

"github.com/wepala/weos/projections"

Expand Down Expand Up @@ -681,3 +683,93 @@ func HealthCheck(api *RESTAPI, projection projections.Projection, commandDispatc
}

}

//OpenIDMiddleware handling JWT in incoming Authorization header
func OpenIDMiddleware(api *RESTAPI, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc {
var openIdConnectUrl string
securityCheck := true
var verifiers []*oidc.IDTokenVerifier
algs := []string{"RS256", "RS384", "RS512", "HS256"}
if operation.Security != nil && len(*operation.Security) == 0 {
securityCheck = false
}
for _, schemes := range api.Swagger.Components.SecuritySchemes {
//checks if the security scheme type is openIdConnect
if schemes.Value.Type == "openIdConnect" {
//get the open id connect url
if openIdUrl, ok := schemes.Value.ExtensionProps.Extensions[OPENIDCONNECTURLEXTENSION]; ok {
err := json.Unmarshal(openIdUrl.(json.RawMessage), &openIdConnectUrl)
if err != nil {
api.EchoInstance().Logger.Errorf("unable to unmarshal open id connect url '%s'", err)
} else {
//get the Jwk url from open id connect url and validate url
jwksUrl, err := GetJwkUrl(openIdConnectUrl)
if err != nil {
api.EchoInstance().Logger.Warnf("invalid open id connect url: %s", err)
} else {
//by default skipExpiryCheck is false meaning it will not run an expiry check
skipExpiryCheck := false
//get skipexpirycheck that is an extension in the openapi spec
if expireCheck, ok := schemes.Value.ExtensionProps.Extensions[SKIPEXPIRYCHECKEXTENSION]; ok {
err := json.Unmarshal(expireCheck.(json.RawMessage), &skipExpiryCheck)
if err != nil {
api.EchoInstance().Logger.Errorf("unable to unmarshal skip expiry '%s'", err)
}
}
//create key set and verifier
keySet := oidc.NewRemoteKeySet(context.Background(), jwksUrl)
tokenVerifier := oidc.NewVerifier(openIdConnectUrl, keySet, &oidc.Config{
ClientID: "",
SupportedSigningAlgs: algs,
SkipClientIDCheck: true,
SkipExpiryCheck: skipExpiryCheck,
SkipIssuerCheck: true,
Now: time.Now,
})
verifiers = append(verifiers, tokenVerifier)
}

}
}

}
}

return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctxt echo.Context) error {
var err error
var token string
if !securityCheck {
return next(ctxt)
}
if len(verifiers) == 0 {
api.e.Logger.Debugf("unexpected error no verifiers were set")
return NewControllerError("unexpected error no verifiers were set", nil, http.StatusBadRequest)
}
newContext := ctxt.Request().Context()
//get the token from request header since this runs before the context middleware
if ctxt.Request().Header[weoscontext.AUTHORIZATION] != nil {
token = ctxt.Request().Header[weoscontext.AUTHORIZATION][0]
}
if token == "" {
api.e.Logger.Debugf("no JWT token was found")
return NewControllerError("no JWT token was found", nil, http.StatusUnauthorized)
}
jwtToken := strings.Replace(token, "Bearer ", "", -1)
var idToken *oidc.IDToken
for _, tokenVerifier := range verifiers {
idToken, err = tokenVerifier.Verify(newContext, jwtToken)
if err != nil || idToken == nil {
api.e.Logger.Debugf(err.Error())
return NewControllerError("unexpected error verifying token", err, http.StatusUnauthorized)
}
}

newContext = context.WithValue(newContext, weoscontext.USER_ID, idToken.Subject)
request := ctxt.Request().WithContext(newContext)
ctxt.SetRequest(request)
return next(ctxt)

}
}
}
Loading

0 comments on commit 790ed0c

Please sign in to comment.