diff --git a/controller/internal/routes/identity_api_model.go b/controller/internal/routes/identity_api_model.go index a52246912..9cfb4cc58 100644 --- a/controller/internal/routes/identity_api_model.go +++ b/controller/internal/routes/identity_api_model.go @@ -233,6 +233,7 @@ func MapIdentityToRestModel(ae *env.AppEnv, identity *model.Identity) (*rest_mod if entity.Method == persistence.MethodEnrollUpdb { ret.Enrollment.Updb = &rest_model.IdentityEnrollmentsUpdb{ + ID: entity.Id, Jwt: entity.Jwt, Token: entity.Token, ExpiresAt: expiresAt, @@ -241,6 +242,7 @@ func MapIdentityToRestModel(ae *env.AppEnv, identity *model.Identity) (*rest_mod if entity.Method == persistence.MethodEnrollOtt { ret.Enrollment.Ott = &rest_model.IdentityEnrollmentsOtt{ + ID: entity.Id, Jwt: entity.Jwt, Token: entity.Token, ExpiresAt: expiresAt, @@ -248,18 +250,17 @@ func MapIdentityToRestModel(ae *env.AppEnv, identity *model.Identity) (*rest_mod } if entity.Method == persistence.MethodEnrollOttCa { - ca, err := ae.Handlers.Ca.Read(*entity.CaId) - - if err != nil { - return err - } - - ret.Enrollment.Ottca = &rest_model.IdentityEnrollmentsOttca{ - Ca: ToEntityRef(ca.Name, ca, CaLinkFactory), - CaID: ca.Id, - Jwt: entity.Jwt, - Token: entity.Token, - ExpiresAt: expiresAt, + if ca, err := ae.Handlers.Ca.Read(*entity.CaId); err == nil { + ret.Enrollment.Ottca = &rest_model.IdentityEnrollmentsOttca{ + ID: entity.Id, + Ca: ToEntityRef(ca.Name, ca, CaLinkFactory), + CaID: ca.Id, + Jwt: entity.Jwt, + Token: entity.Token, + ExpiresAt: expiresAt, + } + } else { + pfxlog.Logger().Errorf("could not read CA [%s] to render ottca enrollment for identity [%s]", stringz.OrEmpty(entity.CaId), identity.Id) } } diff --git a/controller/persistence/ca_store.go b/controller/persistence/ca_store.go index bcd63e352..d229cf65c 100644 --- a/controller/persistence/ca_store.go +++ b/controller/persistence/ca_store.go @@ -32,6 +32,7 @@ const ( FieldCaIsOttCaEnrollmentEnabled = "isOttCaEnrollmentEnabled" FieldCaIsAuthEnabled = "isAuthEnabled" FieldCaIdentityNameFormat = "identityNameFormat" + FieldCaEnrollments = "enrollments" ) type Ca struct { @@ -102,7 +103,8 @@ func newCaStore(stores *stores) *caStoreImpl { type caStoreImpl struct { *baseStore - indexName boltz.ReadIndex + indexName boltz.ReadIndex + symbolEnrollments boltz.EntitySetSymbol } func (store *caStoreImpl) NewStoreEntity() boltz.Entity { @@ -119,6 +121,8 @@ func (store *caStoreImpl) initializeLocal() { store.AddSymbol(FieldCaIsOttCaEnrollmentEnabled, ast.NodeTypeBool) store.AddSymbol(FieldCaIsAuthEnabled, ast.NodeTypeBool) store.AddSetSymbol(FieldIdentityRoles, ast.NodeTypeString) + store.symbolEnrollments = store.AddFkSetSymbol(FieldCaEnrollments, store.stores.enrollment) + } func (store *caStoreImpl) initializeLinked() { @@ -149,7 +153,12 @@ func (store *caStoreImpl) LoadOneByQuery(tx *bbolt.Tx, query string) (*Ca, error } func (store *caStoreImpl) DeleteById(ctx boltz.MutateContext, id string) error { - // TODO: Delete enrollment certs + for _, enrollmentId := range store.GetRelatedEntitiesIdList(ctx.Tx(), id, FieldCaEnrollments) { + if err := store.stores.enrollment.DeleteById(ctx, enrollmentId); err != nil { + return err + } + } + return store.baseStore.DeleteById(ctx, id) } diff --git a/controller/persistence/enrollment_store.go b/controller/persistence/enrollment_store.go index ff0697166..d3016c63d 100644 --- a/controller/persistence/enrollment_store.go +++ b/controller/persistence/enrollment_store.go @@ -115,6 +115,7 @@ type enrollmentStoreImpl struct { symbolIdentity boltz.EntitySymbol symbolEdgeRouter boltz.EntitySymbol symbolTransitRouter boltz.EntitySymbol + symbolCa boltz.EntitySymbol } func (store *enrollmentStoreImpl) NewStoreEntity() boltz.Entity { @@ -129,12 +130,14 @@ func (store *enrollmentStoreImpl) initializeLocal() { store.symbolIdentity = store.AddFkSymbol(FieldEnrollIdentity, store.stores.identity) store.symbolEdgeRouter = store.AddFkSymbol(FieldEnrollEdgeRouter, store.stores.edgeRouter) store.symbolTransitRouter = store.AddFkSymbol(FieldEnrollTransitRouter, store.stores.transitRouter) + store.symbolCa = store.AddFkSymbol(FieldEnrollmentCaId, store.stores.ca) } func (store *enrollmentStoreImpl) initializeLinked() { store.AddNullableFkIndex(store.symbolIdentity, store.stores.identity.symbolEnrollments) store.AddNullableFkIndex(store.symbolEdgeRouter, store.stores.edgeRouter.symbolEnrollments) store.AddNullableFkIndex(store.symbolTransitRouter, store.stores.transitRouter.symbolEnrollments) + store.AddNullableFkIndex(store.symbolCa, store.stores.ca.symbolEnrollments) } func (store *enrollmentStoreImpl) LoadOneById(tx *bbolt.Tx, id string) (*Enrollment, error) { diff --git a/controller/persistence/migration_v16.go b/controller/persistence/migration_v16.go new file mode 100644 index 000000000..9e35c4696 --- /dev/null +++ b/controller/persistence/migration_v16.go @@ -0,0 +1,47 @@ +package persistence + +import ( + "fmt" + "github.com/michaelquigley/pfxlog" + "github.com/openziti/foundation/storage/ast" + "github.com/openziti/foundation/storage/boltz" +) + +func (m *Migrations) removeOrphanedOttCaEnrollments(step *boltz.MigrationStep) { + var enrollmentsToDelete []string + + for cursor := m.stores.Enrollment.IterateIds(step.Ctx.Tx(), ast.BoolNodeTrue); cursor.IsValid(); cursor.Next() { + current := cursor.Current() + currentEnrollmentId := string(current) + + enrollment, err := m.stores.Enrollment.LoadOneById(step.Ctx.Tx(), currentEnrollmentId) + + if err != nil { + step.SetError(fmt.Errorf("error interating ids of enrollments, enrollment [%s]: %v", currentEnrollmentId, err)) + return + } + + if enrollment.CaId != nil && *enrollment.CaId != "" { + _, err := m.stores.Ca.LoadOneById(step.Ctx.Tx(), *enrollment.CaId) + + if err != nil && boltz.IsErrNotFoundErr(err) { + enrollmentsToDelete = append(enrollmentsToDelete, currentEnrollmentId) + } + } + } + + //clear caIds that are invalid via CheckIntegrity + m.stores.Enrollment.CheckIntegrity(step.Ctx.Tx(), true, func(err error, fixed bool) { + if !fixed { + pfxlog.Logger().Errorf("unfixable error during orphaned ottca enrollment integrity check: %v", err) + } + }) + + for _, enrollmentId := range enrollmentsToDelete { + pfxlog.Logger().Infof("removing invalid ottca enrollment [%s]", enrollmentId) + if err := m.stores.Enrollment.DeleteById(step.Ctx, enrollmentId); err != nil { + + step.SetError(fmt.Errorf("could not delete enrollment [%s] with invalid CA reference: %v", enrollmentId, err)) + } + } +} diff --git a/controller/persistence/migrations.go b/controller/persistence/migrations.go index a5aa8338d..e72188770 100644 --- a/controller/persistence/migrations.go +++ b/controller/persistence/migrations.go @@ -23,7 +23,7 @@ import ( ) const ( - CurrentDbVersion = 15 + CurrentDbVersion = 16 FieldVersion = "version" ) @@ -120,6 +120,10 @@ func (m *Migrations) migrate(step *boltz.MigrationStep) int { step.SetError(m.stores.ConfigType.Update(step.Ctx, serverConfigTypeV1, nil)) } + if step.CurrentVersion < 16 { + m.removeOrphanedOttCaEnrollments(step) + } + // current version if step.CurrentVersion <= CurrentDbVersion { return CurrentDbVersion diff --git a/go.sum b/go.sum index 20e9b3247..bbfba23b0 100644 --- a/go.sum +++ b/go.sum @@ -366,12 +366,14 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1 h1:Q6uM1SfwyYPCBtezf829EqAqolrIGhAm6KfVx3QBRWg= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= +github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943 h1:REP5ibuxEIcBcG837UAei093pVqmOPm/tw0TbUd18YI= github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -439,6 +441,7 @@ github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcK github.com/mdlayher/netlink v1.1.1 h1:VqG+Voq9V4uZ+04vjIrcSCWDpf91B1xxbP4QBUmUJE8= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8= +github.com/mdlayher/netlink v1.2.1 h1:Q5xDCtos3xv72vqzjuAh5Ns/IUlFfoy+948yBNSktTc= github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -447,8 +450,7 @@ github.com/michaelquigley/pfxlog v0.0.0-20190813191113-2be43bd0dccc h1:PQrqw9XKY github.com/michaelquigley/pfxlog v0.0.0-20190813191113-2be43bd0dccc/go.mod h1:Z0K0l/NScHvLeGoy4SZTps+ZxGv85ql4AvZqTqOxBY8= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs= -github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/miekg/dns v1.1.38 h1:MtIY+fmHUVVgv1AXzmKMWcwdCYxTRPG1EDjpqF4RCEw= github.com/miekg/dns v1.1.38/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -801,6 +803,7 @@ golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061 h1:DQmQoKxQWtyybCtX/3dIuDBcAhFszqq8YiNeS6sNu1c= golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/rest_model/identity_enrollments.go b/rest_model/identity_enrollments.go index f031a3e5a..5e7d6c8f6 100644 --- a/rest_model/identity_enrollments.go +++ b/rest_model/identity_enrollments.go @@ -154,6 +154,9 @@ type IdentityEnrollmentsOtt struct { // Format: date-time ExpiresAt strfmt.DateTime `json:"expiresAt,omitempty"` + // id + ID string `json:"id,omitempty"` + // jwt Jwt string `json:"jwt,omitempty"` @@ -221,6 +224,9 @@ type IdentityEnrollmentsOttca struct { // Format: date-time ExpiresAt strfmt.DateTime `json:"expiresAt,omitempty"` + // id + ID string `json:"id,omitempty"` + // jwt Jwt string `json:"jwt,omitempty"` @@ -304,6 +310,9 @@ type IdentityEnrollmentsUpdb struct { // Format: date-time ExpiresAt strfmt.DateTime `json:"expiresAt,omitempty"` + // id + ID string `json:"id,omitempty"` + // jwt Jwt string `json:"jwt,omitempty"` diff --git a/rest_server/embedded_spec.go b/rest_server/embedded_spec.go index 3743e83d6..206799f94 100644 --- a/rest_server/embedded_spec.go +++ b/rest_server/embedded_spec.go @@ -8246,6 +8246,9 @@ func init() { "type": "string", "format": "date-time" }, + "id": { + "type": "string" + }, "jwt": { "type": "string" }, @@ -8267,6 +8270,9 @@ func init() { "type": "string", "format": "date-time" }, + "id": { + "type": "string" + }, "jwt": { "type": "string" }, @@ -8282,6 +8288,9 @@ func init() { "type": "string", "format": "date-time" }, + "id": { + "type": "string" + }, "jwt": { "type": "string" }, @@ -24975,6 +24984,9 @@ func init() { "type": "string", "format": "date-time" }, + "id": { + "type": "string" + }, "jwt": { "type": "string" }, @@ -24996,6 +25008,9 @@ func init() { "type": "string", "format": "date-time" }, + "id": { + "type": "string" + }, "jwt": { "type": "string" }, @@ -25011,6 +25026,9 @@ func init() { "type": "string", "format": "date-time" }, + "id": { + "type": "string" + }, "jwt": { "type": "string" }, @@ -27756,6 +27774,9 @@ func init() { "type": "string", "format": "date-time" }, + "id": { + "type": "string" + }, "jwt": { "type": "string" }, @@ -27777,6 +27798,9 @@ func init() { "type": "string", "format": "date-time" }, + "id": { + "type": "string" + }, "jwt": { "type": "string" }, @@ -27792,6 +27816,9 @@ func init() { "type": "string", "format": "date-time" }, + "id": { + "type": "string" + }, "jwt": { "type": "string" }, diff --git a/specs/swagger.yml b/specs/swagger.yml index 68bc95262..db6e65933 100644 --- a/specs/swagger.yml +++ b/specs/swagger.yml @@ -4612,6 +4612,8 @@ definitions: updb: type: object properties: + id: + type: string token: type: string jwt: @@ -4622,6 +4624,8 @@ definitions: ott: type: object properties: + id: + type: string token: type: string jwt: @@ -4632,6 +4636,8 @@ definitions: ottca: type: object properties: + id: + type: string token: type: string jwt: diff --git a/tests/authenticate.go b/tests/authenticate.go index 9711c1f9a..4ed1c64bd 100644 --- a/tests/authenticate.go +++ b/tests/authenticate.go @@ -499,6 +499,13 @@ func (request *authenticatedRequests) RequireNewIdentityWithOtt(isAdmin bool, ro return identity } +func (request *authenticatedRequests) RequireNewIdentityWithCaOtt(isAdmin bool, caId string, roleAttributes ...string) *identity { + identity := newTestIdentity(isAdmin, roleAttributes...) + identity.enrollment = map[string]interface{}{"ottca": caId} + request.requireCreateEntity(identity) + return identity +} + func (request *authenticatedRequests) requireCreateEntity(entity entity) string { resp := request.createEntity(entity) standardJsonResponseTests(resp, http.StatusCreated, request.testContext.testing) diff --git a/tests/ca_test.go b/tests/ca_test.go index 879da8208..9d92f2dee 100644 --- a/tests/ca_test.go +++ b/tests/ca_test.go @@ -4,6 +4,7 @@ package tests import ( "fmt" + "github.com/Jeffail/gabs" "github.com/openziti/edge/controller/model" "github.com/openziti/edge/eid" "net/http" @@ -281,4 +282,69 @@ func Test_CA(t *testing.T) { ctx.Req.Empty(enrolledSession) }) }) + + t.Run("deleting a CA should", func(t *testing.T) { + + t.Run("clean up outstanding enrollments", func(t *testing.T) { + ctx.testContextChanged(t) + + //shared across tests + enrollmentId := "" + var unenrolledOttCaIdentity *identity + var unenrolledOttCaIdentityContainer *gabs.Container + + ca := newTestCa() + + ca.id = ctx.AdminSession.requireCreateEntity(ca) + + caValues := ctx.AdminSession.requireQuery("cas/" + ca.id) + verificationToken := caValues.Path("data.verificationToken").Data().(string) + ctx.Req.NotEmpty(verificationToken) + + validationAuth := ca.CreateSignedCert(verificationToken) + + resp, err := ctx.AdminSession.newAuthenticatedRequest(). + SetHeader("content-type", "text/plain"). + SetBody(validationAuth.certPem). + Post("cas/" + ca.id + "/verify") + + ctx.Req.NoError(err) + ctx.logJson(resp.Body()) + ctx.Req.Equal(http.StatusOK, resp.StatusCode()) + + unenrolledOttCaIdentity = ctx.AdminSession.RequireNewIdentityWithCaOtt(false, ca.id) + + unenrolledOttCaIdentityContainer = ctx.AdminSession.requireQuery("/identities/" + unenrolledOttCaIdentity.Id) + + ctx.Req.True(unenrolledOttCaIdentityContainer.ExistsP("data.enrollment.ottca.id"), "expected ottca to have an enrollment id") + + enrollmentId = unenrolledOttCaIdentityContainer.Path("data.enrollment.ottca.id").Data().(string) + ctx.Req.NotEmpty(enrollmentId, "enrollment id should not be empty string") + + ctx.AdminSession.requireDeleteEntity(ca) + + t.Run("enrollment should have been removed", func(t *testing.T) { + ctx.testContextChanged(t) + + status, _ := ctx.AdminSession.query("/enrollments/" + enrollmentId) + + ctx.Req.Equal(http.StatusNotFound, status, "expected enrollment to not be found") + + }) + + t.Run("identities with previous enrollments tied to deleted CAs should not error on list", func(t *testing.T) { + ctx.testContextChanged(t) + + ctx.Req.NotEmpty(unenrolledOttCaIdentity.Id) + _ = ctx.AdminSession.requireQuery(fmt.Sprintf(`/identities?filter=id="%s"`, unenrolledOttCaIdentity.Id)) + }) + + t.Run("identities with previous enrollments tied to deleted CAs should not error on detail", func(t *testing.T) { + ctx.testContextChanged(t) + + ctx.Req.NotEmpty(unenrolledOttCaIdentity.Id) + _ = ctx.AdminSession.requireQuery("/identities/" + unenrolledOttCaIdentity.Id) + }) + }) + }) }