From 770c7bcd06232dca6adaab771c04843c1e62f0a6 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Thu, 16 Dec 2021 13:13:21 -0500 Subject: [PATCH 1/2] adds re-enrollment logic for edge-routers --- controller/apierror/helpers.go | 10 ++++ controller/apierror/messages.go | 4 ++ .../internal/routes/edge_router_router.go | 31 ++++++++++ controller/model/edge_router_handlers.go | 60 +++++++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/controller/apierror/helpers.go b/controller/apierror/helpers.go index 97bf5405c..c008afc51 100644 --- a/controller/apierror/helpers.go +++ b/controller/apierror/helpers.go @@ -311,3 +311,13 @@ func NewInvalidMfaTokenError() *errorz.ApiError { Status: MfaInvalidTokenStatus, } } + +func NewEdgeRouterFailedReEnrollment(cause error) *errorz.ApiError{ + return &errorz.ApiError{ + Code: EdgeRouterFailedReEnrollmentCode, + Message: EdgeRouterFailedReEnrollmentMessage, + Status: EdgeRouterFailedReEnrollmentStatus, + Cause: cause, + AppendCause: true, + } +} \ No newline at end of file diff --git a/controller/apierror/messages.go b/controller/apierror/messages.go index bb134d7d1..7c3085103 100644 --- a/controller/apierror/messages.go +++ b/controller/apierror/messages.go @@ -166,4 +166,8 @@ const ( MfaNotEnrolledCode string = "MFA_NOT_ENROLLED" MfaNotEnrolledMessage string = "The current identity is not enrolled in MFA" MfaNotEnrolledStatus int = http.StatusConflict + + EdgeRouterFailedReEnrollmentCode = "FAILED_ER_REENROLLMENT" + EdgeRouterFailedReEnrollmentMessage = "the edge router failed to be re-enrolled, see cause" + EdgeRouterFailedReEnrollmentStatus int = http.StatusInternalServerError ) diff --git a/controller/internal/routes/edge_router_router.go b/controller/internal/routes/edge_router_router.go index 0c85ee3e1..be78f4357 100644 --- a/controller/internal/routes/edge_router_router.go +++ b/controller/internal/routes/edge_router_router.go @@ -18,8 +18,10 @@ package routes import ( "github.com/go-openapi/runtime/middleware" + "github.com/openziti/edge/controller/apierror" "github.com/openziti/edge/controller/env" "github.com/openziti/edge/controller/internal/permissions" + "github.com/openziti/edge/controller/model" "github.com/openziti/edge/controller/response" "github.com/openziti/edge/rest_management_api_server/operations/edge_router" "github.com/openziti/fabric/controller/api" @@ -68,6 +70,11 @@ func (r *EdgeRouterRouter) Register(ae *env.AppEnv) { return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) { r.Patch(ae, rc, params) }, params.HTTPRequest, params.ID, "", permissions.IsAdmin()) }) + //special actions + ae.ManagementApi.EdgeRouterReEnrollEdgeRouterHandler = edge_router.ReEnrollEdgeRouterHandlerFunc(func(params edge_router.ReEnrollEdgeRouterParams, _ interface{}) middleware.Responder { + return ae.IsAllowed(r.ReEnroll, params.HTTPRequest, params.ID, "", permissions.IsAdmin()) + }) + // additional lists ae.ManagementApi.EdgeRouterListEdgeRouterEdgeRouterPoliciesHandler = edge_router.ListEdgeRouterEdgeRouterPoliciesHandlerFunc(func(params edge_router.ListEdgeRouterEdgeRouterPoliciesParams, _ interface{}) middleware.Responder { return ae.IsAllowed(r.listEdgeRouterPolicies, params.HTTPRequest, params.ID, "", permissions.IsAdmin()) @@ -146,3 +153,27 @@ func (r *EdgeRouterRouter) listServices(ae *env.AppEnv, rc *response.RequestCont filterTemplate := `not isEmpty(from serviceEdgeRouterPolicies where anyOf(routers) = "%v")` ListAssociationsWithFilter(ae, rc, filterTemplate, ae.Handlers.EdgeService, MapServiceToRestEntity) } + +func (r *EdgeRouterRouter) ReEnroll(ae *env.AppEnv, rc *response.RequestContext) { + id, _ := rc.GetEntityId() + + var router *model.EdgeRouter + var err error + + if router, err = ae.GetHandlers().EdgeRouter.Read(id); err != nil { + rc.RespondWithError(err) + return + } + + if router == nil { + rc.RespondWithNotFound() + return + } + + if err := ae.GetHandlers().EdgeRouter.ReEnroll(router); err != nil { + rc.RespondWithApiError(apierror.NewEdgeRouterFailedReEnrollment(err)) + return + } + + rc.RespondWithEmptyOk() +} diff --git a/controller/model/edge_router_handlers.go b/controller/model/edge_router_handlers.go index 9109e4309..3c0d9ac53 100644 --- a/controller/model/edge_router_handlers.go +++ b/controller/model/edge_router_handlers.go @@ -260,6 +260,66 @@ func (handler *EdgeRouterHandler) collectEnrollmentsInTx(tx *bbolt.Tx, id string return nil } +// ReEnroll creates a new JWT enrollment for an existing edge router. If the edge router already exists +// with a JWT, a new JWT is created. If the edge router was already enrolled, all record of the enrollment is +// reset and the edge router is disconnected forcing the edge router to complete enrollment before connecting. +func (handler *EdgeRouterHandler) ReEnroll(router *EdgeRouter) error { + log := pfxlog.Logger().WithField("routerId", router.Id) + + log.Info("attempting to set edge router state to unenrolled") + enrollment := &Enrollment{ + BaseEntity: models.BaseEntity{}, + Method: MethodEnrollEdgeRouterOtt, + EdgeRouterId: &router.Id, + } + + if err := enrollment.FillJwtInfo(handler.env, router.Id); err != nil { + return fmt.Errorf("unable to fill jwt info for re-enrolling edge router: %v", err) + } + + err := handler.GetDb().Update(func(tx *bbolt.Tx) error { + ctx := boltz.NewMutateContext(tx) + if id, err := handler.GetEnv().GetHandlers().Enrollment.createEntityInTx(ctx, enrollment); err != nil { + return fmt.Errorf("could not create enrollment for re-enrolling edge router: %v", err) + } else { + log.WithField("enrollmentId", id).Infof("edge router re-enrollment entity created") + } + router.Fingerprint = nil + router.CertPem = nil + router.IsVerified = false + + if err := handler.PatchUnrestricted(router, boltz.MapFieldChecker{ + db.FieldRouterFingerprint: struct{}{}, + persistence.FieldEdgeRouterCertPEM: struct{}{}, + persistence.FieldEdgeRouterIsVerified: struct{}{}, + }); err != nil { + err = fmt.Errorf("unable to patch re-enrolling edge router: %v", err) + log.Error(err) + return err + } + return nil + }) + + if err != nil { + return fmt.Errorf("unabled to alter db for re-enrolling edge router: %v", err) + } + + log.Info("clearing cached fingerprint for re-enrolling edge router") + handler.env.GetHostController().GetNetwork().Routers.UpdateCachedFingerprint(router.Id, "") + + log.Info("closing existing connections for re-enrolling edge router") + connectedRouter := handler.env.GetHostController().GetNetwork().GetConnectedRouter(router.Id) + if connectedRouter.Control != nil && !connectedRouter.Control.IsClosed() { + log = log.WithField("channel", connectedRouter.Control.Id()) + log.Info("closing channel, router is flagged for re-enrollment and an existing open channel was found") + if err := connectedRouter.Control.Close(); err != nil { + log.Warnf("unexpected error closing channel for router flagged for re-enrollment: %v", err) + } + } + + return nil +} + type ExtendedCerts struct { RawClientCert []byte RawServerCert []byte From cf20c2dfa84b9d16baf2de78d923e673d52bc926 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Mon, 3 Jan 2022 15:54:51 -0500 Subject: [PATCH 2/2] adds api tests --- .../internal/routes/edge_router_api_model.go | 44 +++---- controller/model/edge_router_handlers.go | 19 +-- tests/context.go | 15 ++- tests/router_reenroll_test.go | 124 ++++++++++++++++++ 4 files changed, 167 insertions(+), 35 deletions(-) create mode 100644 tests/router_reenroll_test.go diff --git a/controller/internal/routes/edge_router_api_model.go b/controller/internal/routes/edge_router_api_model.go index b2c066425..fb8ba9379 100644 --- a/controller/internal/routes/edge_router_api_model.go +++ b/controller/internal/routes/edge_router_api_model.go @@ -153,19 +153,18 @@ func MapEdgeRouterToRestModel(ae *env.AppEnv, router *model.EdgeRouter) (*rest_m SyncStatus: &syncStatusStr, AppData: &appData, }, - RoleAttributes: &roleAttributes, - EnrollmentToken: nil, - EnrollmentCreatedAt: nil, - EnrollmentExpiresAt: nil, - EnrollmentJwt: nil, - IsVerified: &router.IsVerified, - Fingerprint: stringz.OrEmpty(router.Fingerprint), - VersionInfo: MapVersionInfoToRestModel(routerState.VersionInfo), - IsTunnelerEnabled: &router.IsTunnelerEnabled, - CertPem: router.CertPem, + RoleAttributes: &roleAttributes, + EnrollmentToken: nil, + EnrollmentCreatedAt: nil, + EnrollmentExpiresAt: nil, + EnrollmentJwt: nil, + IsVerified: &router.IsVerified, + Fingerprint: stringz.OrEmpty(router.Fingerprint), + VersionInfo: MapVersionInfoToRestModel(routerState.VersionInfo), + IsTunnelerEnabled: &router.IsTunnelerEnabled, + CertPem: router.CertPem, UnverifiedFingerprint: router.UnverifiedFingerprint, - UnverifiedCertPem: router.UnverifiedCertPem, - + UnverifiedCertPem: router.UnverifiedCertPem, } if !router.IsVerified { @@ -180,18 +179,17 @@ func MapEdgeRouterToRestModel(ae *env.AppEnv, router *model.EdgeRouter) (*rest_m return nil, err } - if len(enrollments) != 1 { - return nil, fmt.Errorf("expected enrollment not found for unverified edge router %s", router.Id) + if len(enrollments) > 0 { + enrollment := enrollments[0] + + createdAt := strfmt.DateTime(enrollment.CreatedAt) + expiresAt := strfmt.DateTime(*enrollment.ExpiresAt) + + ret.EnrollmentExpiresAt = &expiresAt + ret.EnrollmentCreatedAt = &createdAt + ret.EnrollmentJwt = &enrollment.Jwt + ret.EnrollmentToken = &enrollment.Token } - enrollment := enrollments[0] - - createdAt := strfmt.DateTime(enrollment.CreatedAt) - expiresAt := strfmt.DateTime(*enrollment.ExpiresAt) - - ret.EnrollmentExpiresAt = &expiresAt - ret.EnrollmentCreatedAt = &createdAt - ret.EnrollmentJwt = &enrollment.Jwt - ret.EnrollmentToken = &enrollment.Token } return ret, nil diff --git a/controller/model/edge_router_handlers.go b/controller/model/edge_router_handlers.go index 3c0d9ac53..878c5b9c9 100644 --- a/controller/model/edge_router_handlers.go +++ b/controller/model/edge_router_handlers.go @@ -288,15 +288,6 @@ func (handler *EdgeRouterHandler) ReEnroll(router *EdgeRouter) error { router.CertPem = nil router.IsVerified = false - if err := handler.PatchUnrestricted(router, boltz.MapFieldChecker{ - db.FieldRouterFingerprint: struct{}{}, - persistence.FieldEdgeRouterCertPEM: struct{}{}, - persistence.FieldEdgeRouterIsVerified: struct{}{}, - }); err != nil { - err = fmt.Errorf("unable to patch re-enrolling edge router: %v", err) - log.Error(err) - return err - } return nil }) @@ -304,6 +295,16 @@ func (handler *EdgeRouterHandler) ReEnroll(router *EdgeRouter) error { return fmt.Errorf("unabled to alter db for re-enrolling edge router: %v", err) } + if err := handler.PatchUnrestricted(router, boltz.MapFieldChecker{ + db.FieldRouterFingerprint: struct{}{}, + persistence.FieldEdgeRouterCertPEM: struct{}{}, + persistence.FieldEdgeRouterIsVerified: struct{}{}, + }); err != nil { + err = fmt.Errorf("unable to patch re-enrolling edge router: %v", err) + log.Error(err) + return err + } + log.Info("clearing cached fingerprint for re-enrolling edge router") handler.env.GetHostController().GetNetwork().Routers.UpdateCachedFingerprint(router.Id, "") diff --git a/tests/context.go b/tests/context.go index f0e341a18..15d597527 100644 --- a/tests/context.go +++ b/tests/context.go @@ -329,6 +329,12 @@ func (ctx *TestContext) StartServerFor(test string, clean bool) { } func (ctx *TestContext) createAndEnrollEdgeRouter(tunneler bool, roleAttributes ...string) *edgeRouter { + ctx.requireCreateEdgeRouter(tunneler, roleAttributes...) + ctx.requireEnrollEdgeRouter(tunneler, ctx.edgeRouterEntity.id) + return ctx.edgeRouterEntity +} + +func (ctx *TestContext) requireCreateEdgeRouter(tunneler bool, roleAttributes ...string) *edgeRouter { // If an edge router has already been created, delete it and create a new one if ctx.edgeRouterEntity != nil { ctx.AdminManagementSession.requireDeleteEntity(ctx.edgeRouterEntity) @@ -342,7 +348,12 @@ func (ctx *TestContext) createAndEnrollEdgeRouter(tunneler bool, roleAttributes } else { ctx.edgeRouterEntity = ctx.AdminManagementSession.requireNewEdgeRouter(roleAttributes...) } - jwt := ctx.AdminManagementSession.getEdgeRouterJwt(ctx.edgeRouterEntity.id) + + return ctx.edgeRouterEntity +} + +func (ctx *TestContext) requireEnrollEdgeRouter(tunneler bool, routerId string) { + jwt := ctx.AdminManagementSession.getEdgeRouterJwt(routerId) configFile := EdgeRouterConfFile if tunneler { @@ -356,8 +367,6 @@ func (ctx *TestContext) createAndEnrollEdgeRouter(tunneler bool, roleAttributes var keyAlg sdkConfig.KeyAlgVar _ = keyAlg.Set("RSA") ctx.Req.NoError(enroller.Enroll([]byte(jwt), true, "", keyAlg)) - - return ctx.edgeRouterEntity } func (ctx *TestContext) createAndEnrollTransitRouter() *transitRouter { diff --git a/tests/router_reenroll_test.go b/tests/router_reenroll_test.go new file mode 100644 index 000000000..716c90cb4 --- /dev/null +++ b/tests/router_reenroll_test.go @@ -0,0 +1,124 @@ +//go:build apitests +// +build apitests + +/* + 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 tests + +import ( + "github.com/openziti/edge/rest_model" + "net/http" + "testing" +) + +func Test_Router_ReEnroll(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + ctx.RequireAdminManagementApiLogin() + + t.Run("unenrolled router", func(t *testing.T) { + ctx.testContextChanged(t) + edgeRouter := ctx.requireCreateEdgeRouter(false) + + t.Run("can start re-enrollment", func(t *testing.T) { + ctx.testContextChanged(t) + + resp, err := ctx.AdminManagementSession.newAuthenticatedRequest().SetBody("{}").Post("edge-routers/" + edgeRouter.id + "/re-enroll") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, resp.StatusCode()) + + edgeRouterDetail := &rest_model.EdgeRouterDetail{} + envelope := &rest_model.DetailedEdgeRouterEnvelope{ + Data: edgeRouterDetail, + Meta: &rest_model.Meta{}, + } + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetResult(envelope).Get("edge-routers/" + edgeRouter.id) + + ctx.Req.NoError(err) + ctx.Req.NotNil(resp) + ctx.Req.Equal(http.StatusOK, resp.StatusCode()) + ctx.Req.NotNil(edgeRouterDetail.EnrollmentJwt) + + t.Run("router has no current fingerprint", func(t *testing.T) { + ctx.testContextChanged(t) + ctx.Req.Empty(edgeRouterDetail.Fingerprint) + }) + + t.Run("router has a new enrollment JWT", func(t *testing.T) { + ctx.testContextChanged(t) + ctx.Req.NotNil(edgeRouterDetail.EnrollmentJwt) + + t.Run("router can enroll", func(t *testing.T) { + ctx.testContextChanged(t) + ctx.requireEnrollEdgeRouter(false, *edgeRouterDetail.ID) + }) + }) + }) + }) + + t.Run("enrolled router", func(t *testing.T) { + + enrolledRouter := ctx.createAndEnrollEdgeRouter(false) + + t.Run("has no current enrollment", func(t *testing.T) { + ctx.testContextChanged(t) + + edgeRouterDetail := &rest_model.EdgeRouterDetail{} + resp, err := ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(edgeRouterDetail).Get("edge-routers/" + enrolledRouter.id) + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, resp.StatusCode()) + ctx.Req.Nil(edgeRouterDetail.EnrollmentJwt) + }) + + t.Run("can start re-enroll", func(t *testing.T) { + ctx.testContextChanged(t) + + resp, err := ctx.AdminManagementSession.newAuthenticatedRequest().SetBody("{}").Post("edge-routers/" + enrolledRouter.id + "/re-enroll") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, resp.StatusCode()) + + edgeRouterDetail := &rest_model.EdgeRouterDetail{} + envelope := &rest_model.DetailedEdgeRouterEnvelope{ + Data: edgeRouterDetail, + Meta: &rest_model.Meta{}, + } + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetResult(envelope).Get("edge-routers/" + enrolledRouter.id) + + ctx.Req.NoError(err) + ctx.Req.NotNil(resp) + ctx.Req.Equal(http.StatusOK, resp.StatusCode()) + ctx.Req.NotNil(edgeRouterDetail.EnrollmentJwt) + + t.Run("router has no current fingerprint", func(t *testing.T) { + ctx.testContextChanged(t) + ctx.Req.Empty(edgeRouterDetail.Fingerprint) + }) + + t.Run("router has a new enrollment JWT", func(t *testing.T) { + ctx.testContextChanged(t) + ctx.Req.NotNil(edgeRouterDetail.EnrollmentJwt) + + t.Run("router can enroll with JWT", func(t *testing.T) { + ctx.testContextChanged(t) + ctx.requireEnrollEdgeRouter(false, *edgeRouterDetail.ID) + }) + }) + }) + }) +}