Files
ziti/controller/env/appenv.go
T
2020-03-19 13:47:42 -04:00

546 lines
15 KiB
Go

/*
Copyright NetFoundry, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package env
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/tls"
"fmt"
"github.com/netfoundry/ziti-foundation/storage/boltz"
"github.com/netfoundry/ziti-foundation/util/mirror"
"github.com/pkg/errors"
"io/ioutil"
"net/http"
"path/filepath"
"reflect"
"strings"
"sync"
jwt2 "github.com/dgrijalva/jwt-go"
"github.com/gobuffalo/packr"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/michaelquigley/pfxlog"
"github.com/netfoundry/ziti-edge/controller/apierror"
edgeconfig "github.com/netfoundry/ziti-edge/controller/config"
"github.com/netfoundry/ziti-edge/controller/internal/permissions"
"github.com/netfoundry/ziti-edge/controller/middleware"
"github.com/netfoundry/ziti-edge/controller/model"
"github.com/netfoundry/ziti-edge/controller/persistence"
"github.com/netfoundry/ziti-edge/controller/response"
"github.com/netfoundry/ziti-edge/internal/cert"
"github.com/netfoundry/ziti-edge/internal/jwt"
"github.com/netfoundry/ziti-edge/migration"
"github.com/netfoundry/ziti-fabric/controller/network"
"github.com/netfoundry/ziti-fabric/xctrl"
"github.com/netfoundry/ziti-fabric/xmgmt"
"github.com/netfoundry/ziti-foundation/common/constants"
"github.com/netfoundry/ziti-sdk-golang/ziti/config"
"github.com/xeipuuv/gojsonschema"
"gopkg.in/oleiade/reflections.v1"
)
type AppEnv struct {
BoltStores *persistence.Stores
Handlers *model.Handlers
Embedded *packr.Box
Schemes *Schemes
Config *edgeconfig.Config
EnrollmentJwtGenerator jwt.EnrollmentGenerator
Versions *config.Versions
AuthHeaderName string
AuthCookieName string
ApiServerCsrSigner cert.Signer
ApiClientCsrSigner cert.Signer
ControlClientCsrSigner cert.Signer
FingerprintGenerator cert.FingerprintGenerator
RootRouter *mux.Router
CurrentIdentityRouter *mux.Router
RequestResponderFactory response.RequestResponderFactory
ModelHandlers *migration.ModelHandlers
AuthRegistry model.AuthRegistry
EnrollRegistry model.EnrollmentRegistry
Broker *Broker
HostController HostController
}
func (ae *AppEnv) GetApiServerCsrSigner() cert.Signer {
return ae.ApiServerCsrSigner
}
func (ae *AppEnv) GetControlClientCsrSigner() cert.Signer {
return ae.ControlClientCsrSigner
}
func (ae *AppEnv) GetHostController() model.HostController {
return ae.HostController
}
func (ae *AppEnv) GetSchemas() model.Schemas {
return ae.Schemes
}
func (ae *AppEnv) GetHandlers() *model.Handlers {
return ae.Handlers
}
func (ae *AppEnv) GetConfig() *edgeconfig.Config {
return ae.Config
}
func (ae *AppEnv) GetEnrollmentJwtGenerator() jwt.EnrollmentGenerator {
return ae.EnrollmentJwtGenerator
}
func (ae *AppEnv) GetDbProvider() persistence.DbProvider {
return ae.HostController.GetNetwork()
}
func (ae *AppEnv) GetStores() *persistence.Stores {
return ae.BoltStores
}
func (ae *AppEnv) GetAuthRegistry() model.AuthRegistry {
return ae.AuthRegistry
}
func (ae *AppEnv) GetEnrollRegistry() model.EnrollmentRegistry {
return ae.EnrollRegistry
}
func (ae *AppEnv) IsEdgeRouterOnline(id string) bool {
return ae.Broker.GetOnlineEdgeRouter(id) != nil
}
func (ae *AppEnv) GetApiClientCsrSigner() cert.Signer {
return ae.ApiClientCsrSigner
}
type HostController interface {
RegisterXctrl(x xctrl.Xctrl) error
RegisterXmgmt(x xmgmt.Xmgmt) error
GetNetwork() *network.Network
}
type Schemes struct {
Association *BasicEntitySchema
Authenticator *BasicEntitySchema
AuthenticatorSelf *BasicEntitySchema
Ca *BasicEntitySchema
Config *BasicEntitySchema
ConfigType *BasicEntitySchema
Enroller *BasicEntitySchema
EnrollEr *BasicEntitySchema
EnrollUpdb *BasicEntitySchema
EdgeRouter *BasicEntitySchema
EdgeRouterPolicy *BasicEntitySchema
Identity *IdentityEntitySchema
Service *BasicEntitySchema
ServiceEdgeRouterPolicy *BasicEntitySchema
ServicePolicy *BasicEntitySchema
Session *BasicEntitySchema
Terminator *BasicEntitySchema
}
func (s Schemes) GetEnrollErPost() *gojsonschema.Schema {
return s.EnrollEr.Post
}
func (s Schemes) GetEnrollUpdbPost() *gojsonschema.Schema {
return s.EnrollUpdb.Post
}
type IdentityEntitySchema struct {
Post *gojsonschema.Schema
Patch *gojsonschema.Schema
Put *gojsonschema.Schema
ServiceConfigs *gojsonschema.Schema
}
type BasicEntitySchema struct {
Post *gojsonschema.Schema
Patch *gojsonschema.Schema
Put *gojsonschema.Schema
}
type AppHandler func(ae *AppEnv, rc *response.RequestContext)
type AppMiddleware func(*AppEnv, http.Handler) http.Handler
func NewAppEnv(c *edgeconfig.Config) *AppEnv {
// See README.md in /ziti/edge/embedded for details
// This path is relative to source location of this file
embedded := packr.NewBox("../embedded")
c.Persistence.AddMigrationBox(&embedded)
ae := &AppEnv{
Config: c,
Embedded: &embedded,
Versions: &config.Versions{
Api: "1.0.0",
EnrollmentApi: "1.0.0",
},
AuthCookieName: constants.ZitiSession,
AuthHeaderName: constants.ZitiSession,
RootRouter: mux.NewRouter(),
RequestResponderFactory: response.NewRequestResponder,
ModelHandlers: migration.GetModelHandlers(),
AuthRegistry: &model.AuthProcessorRegistryImpl{},
EnrollRegistry: &model.EnrollmentRegistryImpl{},
}
ae.RootRouter.NotFoundHandler = NewNotFoundHandler(ae)
ae.RootRouter.MethodNotAllowedHandler = NewMethodNotAllowedHandler(ae)
ae.CurrentIdentityRouter = ae.RootRouter.PathPrefix("/current-identity").Subrouter()
sm := getJwtSigningMethod(c.Api.Identity.ServerCert())
key := c.Api.Identity.ServerCert().PrivateKey
ae.EnrollmentJwtGenerator = jwt.NewJwtIdentityEnrollmentGenerator(ae.Config.Api.Advertise, sm, key)
ae.ApiClientCsrSigner = cert.NewClientSigner(ae.Config.Enrollment.SigningCert.Cert().Leaf, ae.Config.Enrollment.SigningCert.Cert().PrivateKey)
ae.ApiServerCsrSigner = cert.NewServerSigner(ae.Config.Enrollment.SigningCert.Cert().Leaf, ae.Config.Enrollment.SigningCert.Cert().PrivateKey)
ae.ControlClientCsrSigner = cert.NewClientSigner(ae.Config.Enrollment.SigningCert.Cert().Leaf, ae.Config.Enrollment.SigningCert.Cert().PrivateKey)
ae.Schemes = &Schemes{}
err := ae.LoadSchemas()
ae.FingerprintGenerator = cert.NewFingerprintGenerator()
if err != nil {
log := pfxlog.Logger()
log.WithField("cause", err).Fatal("could not load schemas")
}
return ae
}
func (ae *AppEnv) InitPersistence() error {
var err error
db := InitPersistence(&ae.Config.Persistence)
var dbStores *migration.Stores
if db != nil {
dbWithPreload := db.New().Set("gorm:auto_preload", true)
dbStores = migration.NewGormStores(db, dbWithPreload)
}
ae.BoltStores, err = persistence.NewBoltStores(ae.HostController.GetNetwork())
if err == nil {
err = persistence.RunMigrations(ae.GetDbProvider().GetDb(), ae.BoltStores, dbStores)
}
if err == nil {
ae.Handlers = model.InitHandlers(ae)
}
return err
}
func getJwtSigningMethod(cert *tls.Certificate) jwt2.SigningMethod {
var sm jwt2.SigningMethod = jwt2.SigningMethodNone
switch cert.Leaf.PublicKey.(type) {
case *ecdsa.PublicKey:
key := cert.Leaf.PublicKey.(*ecdsa.PublicKey)
switch key.Params().BitSize {
case jwt2.SigningMethodES256.CurveBits:
sm = jwt2.SigningMethodES256
case jwt2.SigningMethodES384.CurveBits:
sm = jwt2.SigningMethodES384
case jwt2.SigningMethodES512.CurveBits:
sm = jwt2.SigningMethodES512
default:
pfxlog.Logger().Panic("unsupported EC key size: ", key.Params().BitSize)
}
case *rsa.PublicKey:
sm = jwt2.SigningMethodRS256
default:
pfxlog.Logger().Panic("unknown certificate type, unable to determine signing method")
}
return sm
}
func (ae *AppEnv) getSessionTokenFromRequest(r *http.Request) string {
token := r.Header.Get(ae.AuthHeaderName)
if token == "" {
sessionCookie, _ := r.Cookie(ae.AuthCookieName)
if sessionCookie != nil {
token = sessionCookie.Value
}
}
return token
}
func (ae *AppEnv) WrapHandler(f AppHandler, prs ...permissions.Resolver) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rid := uuid.New()
sw, ok := w.(*middleware.StatusWriter)
if ok {
rid = sw.RequestId
}
rc := &response.RequestContext{
Id: rid,
Identity: nil,
ApiSession: nil,
ActivePermissions: []string{},
ResponseWriter: w,
Request: r,
EventLogger: &DefaultEventLogger{Ae: ae},
}
rc.RequestResponder = ae.RequestResponderFactory(rc)
sessionToken := ae.getSessionTokenFromRequest(r)
log := pfxlog.Logger()
_, err := uuid.Parse(sessionToken)
if err != nil {
log.WithError(err).Debug("failed to parse session id")
sessionToken = ""
} else {
log.Tracef("authorizing request using session id '%v'", sessionToken)
}
if sessionToken != "" {
rc.ApiSession, err = ae.GetHandlers().ApiSession.ReadByToken(sessionToken)
if err != nil {
//don't error on "not found", just an un-authed session, rely on permissions below
//error on anything else as we failed to work with the store
if !boltz.IsErrNotFoundErr(err) {
log.WithError(err).Debug("error requesting session")
rc.RequestResponder.RespondWithError(err)
return
}
}
}
//updates updatedAt for session timeouts
if rc.ApiSession != nil {
err := ae.GetHandlers().ApiSession.Update(rc.ApiSession)
if err != nil && !boltz.IsErrNotFoundErr(err) {
log.WithError(err).Debug("failed to update session activity")
rc.RequestResponder.RespondWithError(err)
return
}
//re-read session to get new updatedAt
rc.ApiSession, _ = ae.GetHandlers().ApiSession.Read(rc.ApiSession.Id)
}
if rc.ApiSession != nil {
rc.Identity, err = ae.GetHandlers().Identity.Read(rc.ApiSession.IdentityId)
if err != nil {
if boltz.IsErrNotFoundErr(err) {
apiErr := apierror.NewUnauthorized()
apiErr.Cause = fmt.Errorf("associated identity %s not found", rc.ApiSession.IdentityId)
apiErr.AppendCause = true
rc.RequestResponder.RespondWithApiError(apiErr)
} else {
rc.RequestResponder.RespondWithError(err)
}
return
}
}
if rc.Identity != nil {
rc.ActivePermissions = append(rc.ActivePermissions, permissions.AuthenticatedPermission)
if rc.Identity.IsAdmin {
rc.ActivePermissions = append(rc.ActivePermissions, permissions.AdminPermission)
}
}
for _, pr := range prs {
if !pr.IsAllowed(rc.ActivePermissions...) {
rc.RequestResponder.RespondWithUnauthorizedError(rc)
return
}
}
f(ae, rc)
}
}
func (ae *AppEnv) WrapMiddleware(f AppMiddleware) mux.MiddlewareFunc {
return func(h http.Handler) http.Handler {
return f(ae, h)
}
}
func (ae *AppEnv) GetEmbeddedFileContent(filePath string) (string, error) {
file, err := ae.Embedded.Open(filePath)
if err != nil {
return "", err
}
fileContents, err := ioutil.ReadAll(file)
if err != nil {
return "", err
}
return string(fileContents), err
}
func (ae *AppEnv) LoadSchemas() error {
// add custom format checkers
gojsonschema.FormatCheckers.Add("uuid", gojsonschema.UUIDFormatChecker{})
defContent, err := ae.GetEmbeddedFileContent("api-schema/definitions.all.json")
if err != nil {
panic(err.Error())
}
defLoader := gojsonschema.NewStringLoader(defContent)
return ae.Embedded.WalkPrefix("api-schema/entities/", func(s string, f packr.File) error {
if err != nil {
panic(err.Error())
}
fileName := filepath.Base(s)
fileNameParts := strings.Split(fileName, ".")
if len(fileNameParts) < 3 {
return fmt.Errorf("schema file name '%s' is invalid, ensture <entity.method>.<ext> format and packing", fileName)
}
entityName := fileNameParts[0]
entityPropertyName := kebabToCamelCase(entityName)
method := kebabToCamelCase(fileNameParts[1])
schemaContent, err := ioutil.ReadAll(f)
if err != nil {
panic(err)
}
entitySchemaLoader := gojsonschema.NewStringLoader(string(schemaContent))
schemaLoader := gojsonschema.NewSchemaLoader()
if err := schemaLoader.AddSchemas(defLoader); err != nil {
pfxlog.Logger().WithField("cause", err).Panic("error adding definitions schema")
}
schema, err := schemaLoader.Compile(entitySchemaLoader)
if err != nil {
pfxlog.Logger().WithField("cause", err).Panic("error compiling schema")
}
if hasProp, _ := reflections.HasField(ae.Schemes, entityPropertyName); !hasProp {
return fmt.Errorf("found schema with property name '%s' from file '%s', no matching schema property", entityPropertyName, fileName)
}
entitySchema, err := reflections.GetField(ae.Schemes, entityPropertyName)
if entitySchema == nil || reflect.ValueOf(entitySchema).IsNil() {
if err := mirror.InitializeStructField(ae.Schemes, entityPropertyName); err != nil {
return errors.Errorf("found schema with property name '%s', unable to initialized: %w", entityPropertyName, err)
}
entitySchema, err = reflections.GetField(ae.Schemes, entityPropertyName)
}
if err != nil {
return err
}
err = reflections.SetField(entitySchema, method, schema)
if err != nil {
return fmt.Errorf("found schema with property name '%s', could not set methods '%s'", entityPropertyName, method)
}
return nil
})
}
func kebabToCamelCase(kebab string) (camelCase string) {
isToUpper := true
for _, runeValue := range kebab {
if isToUpper {
camelCase += strings.ToUpper(string(runeValue))
isToUpper = false
} else {
if runeValue == '-' {
isToUpper = true
} else {
camelCase += string(runeValue)
}
}
}
return
}
type NotFoundHandler struct {
appEnv *AppEnv
handler func(http.ResponseWriter, *http.Request)
handlerOnce sync.Once
}
func (handler *NotFoundHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
handler.handlerOnce.Do(func() {
handler.handler = handler.appEnv.WrapHandler(func(ae *AppEnv, rc *response.RequestContext) {
rc.RequestResponder.RespondWithNotFound()
})
})
handler.handler(rw, r)
}
func NewNotFoundHandler(ae *AppEnv) http.Handler {
return &NotFoundHandler{
appEnv: ae,
}
}
type MethodNotAllowedHandler struct {
appEnv *AppEnv
handler func(http.ResponseWriter, *http.Request)
handlerOnce sync.Once
}
func (handler *MethodNotAllowedHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
handler.handlerOnce.Do(func() {
handler.handler = handler.appEnv.WrapHandler(func(ae *AppEnv, rc *response.RequestContext) {
rc.RequestResponder.RespondWithMethodNotAllowed()
})
})
handler.handler(rw, r)
}
func NewMethodNotAllowedHandler(ae *AppEnv) http.Handler {
return &MethodNotAllowedHandler{
appEnv: ae,
}
}