diff --git a/controller/embedded/api-schema/entities/transitRouter.patch.json b/controller/embedded/api-schema/entities/transitRouter.patch.json new file mode 100644 index 000000000..bd3585933 --- /dev/null +++ b/controller/embedded/api-schema/entities/transitRouter.patch.json @@ -0,0 +1,13 @@ +{ + "$id": "http://ziti-edge.netfoundry.io/schemas/fabric-routers.patch.json", + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "$ref": "definitions.all.json#/definitions/name-null" + }, + "tags": { + "$ref": "definitions.all.json#/definitions/tags-null" + } + } +} diff --git a/controller/embedded/api-schema/entities/transitRouter.post.json b/controller/embedded/api-schema/entities/transitRouter.post.json new file mode 100644 index 000000000..95abb944b --- /dev/null +++ b/controller/embedded/api-schema/entities/transitRouter.post.json @@ -0,0 +1,16 @@ +{ + "$id": "http://ziti-edge.netfoundry.io/schemas/fabric-routers.post.json", + "type": "object", + "additionalProperties": true, + "required": [ + "name" + ], + "properties": { + "name": { + "$ref": "definitions.all.json#/definitions/name" + }, + "tags": { + "$ref": "definitions.all.json#/definitions/tags-null" + } + } +} diff --git a/controller/embedded/api-schema/entities/transitRouter.put.json b/controller/embedded/api-schema/entities/transitRouter.put.json new file mode 100644 index 000000000..2bbc58831 --- /dev/null +++ b/controller/embedded/api-schema/entities/transitRouter.put.json @@ -0,0 +1,17 @@ +{ + "$id": "http://ziti-edge.netfoundry.io/schemas/fabric-routers.put.json", + "type": "object", + "additionalProperties": true, + "required": [ + "name", + "tags" + ], + "properties": { + "name": { + "$ref": "definitions.all.json#/definitions/name" + }, + "tags": { + "$ref": "definitions.all.json#/definitions/tags" + } + } +} diff --git a/controller/env/a_env-packr.go b/controller/env/a_env-packr.go index 16e969be3..3e8be001b 100644 --- a/controller/env/a_env-packr.go +++ b/controller/env/a_env-packr.go @@ -52,6 +52,9 @@ func init() { _ = packr.PackJSONBytes("../embedded", "api-schema/entities/terminator.patch.json", "\"H4sIAAAAAAAA/6yRsW7DMAxE93yFoGZspN1f0T3IoJi0zUCRVIoa0sL/XlBxm3TPYBh3PLwjqO+dMXZPYAdjF5EyeP9FQgeEGV1CmXJLwDdH2ddxwWuoXpCvlIJkdiXIuLhLzcm+K0huBZWUzxcc5e4FABLKKcQPzgVZCKsdjHDDPmf8bMSoGxx3xpy6WZ6juqMGcxPkP/2oO27aGFuFKc29eHNSi9Fu8tT/631qz5RAwy8DBgDGWl8HlDD/owFOoUVRa/0F2D3jpDcHnCj1S1cXYuyv8uafXK+4w6Nt3em3/gQAAP//ec1C3AICAAA=\"") _ = packr.PackJSONBytes("../embedded", "api-schema/entities/terminator.post.json", "\"H4sIAAAAAAAA/4yRMW7rMAyGd5+C0MuYWLtP8fYgg2LRDgNFckmqQBr47oXUuLHRJYMHfv75kZIeDYDZkTcdmIvq1Fn7RUoH9CO2EXVIOXq+t5Ss9Be8ObGKfKPoNHE7JdH2KimaffHofcIiSucr9vrDnPeklKIL/zlNyEoopgPljPU/40cmxrLAsQEAMIL8ST3W9hJIWZGXynnPKGIagFPtn9bWx9awgNVqokxxNBXP2wlvps8UfYF/48dnDb+N+xeJOQTzLE8b4XKkN+erGzdZj4PLQQual3lmxzgUgceBYr1/aV0I9a3+2RW1RXd4LTc35Zu/AwAA//9siIA/FwIAAA==\"") _ = packr.PackJSONBytes("../embedded", "api-schema/entities/terminator.put.json", "\"H4sIAAAAAAAA/4yQsW7DMAxEd38FoWZMrD1f0T3IoFi0w0CRXJIqkAb+90Kq3djo0kHDnXiPBz4bALMjb45grqrj0dovUjqgH7CNqH3K0fOjpWSlu+LdiVXkO0WnidsxibY3SdHsC0cfIxZQutyw0x/PeU9KKbrwzmlEVkIxR1DOWP8ZPzIxlgKnBgDACPIndVjjZSBlRV6U855RxDQA55of19TnJrLoVTNRpjiYak8z8kLRF/Pv+GnW8Bvcv5yYQzCzPG+AS8d/7lc3bGY99i4HLda07DM7xr4APPYU60GldSHU47/ZlWsL7vAqNzXlTd8BAAD//xFy8DDoAQAA\"") + _ = packr.PackJSONBytes("../embedded", "api-schema/entities/transitRouter.patch.json", "\"H4sIAAAAAAAA/5zOMW7EIBAF0N6nQCTlGnqfIleYhWE9KxbQMBSbyHePBkuR6xQUfPQf/2cxxn5StJuxu0jbvP8moRXjA11BSXWUyG9H1few4wu6T3BnCivXIcjdNZCwu2evxd4Uk3dD1er9iUHODGIkoVogf3FtyELY7WaEB873dk11kjG2wAv/bjqSMakbMVGZWneQ8/z5w19Sr821jJzt7B63ExR49P+B2ryCi55j+Q0AAP//uFLZijwBAAA=\"") + _ = packr.PackJSONBytes("../embedded", "api-schema/entities/transitRouter.post.json", "\"H4sIAAAAAAAA/5yPsXIDIQxEe75CQ1L6oL+vSJ9JgQ+dLQ9GRBKF4/G/ZzjHM1enoGCXfbvcHYB/p+xn8GezNsf4Q0YT5hOGirZyr1lugTjqcsZr0rimo9AyCXdD0dBYLVyUqz8Mlt0aDhgfL7jYU0s5kxHXVD6EG4oRqp/BpOPmC353EhwjPh0AgK/pit4BfG1+26fuuxev2/iD4Dp6M65UtzYNqZRt2VvcqfHFBngcnixLJ/0faySn2kv5A7pxHu43AAD//+YEIOVWAQAA\"") + _ = packr.PackJSONBytes("../embedded", "api-schema/entities/transitRouter.put.json", "\"H4sIAAAAAAAA/5yPsW7EIAyGd57Coh17sOcpulcduGDuHCVAjRnSKO9eQRWJ+QYGjP/v/zgUgH4nryfQT5E8WftLQjf0DzQRJaQaPe+Gki3zEzdXbHB3pvnGqQpyMbmKWUqK+qOhZM/YWOm+4Cz/M+c9CaXo1k9OGVkIi55AuGJ/Z/ypxNgcvhQAgI5uw55tRPcoWgF89908Eo5h+7q17zCG5uAxUOzNxbh17ZZvdpjanuyxc2x7iXV5ApyqnVP9BQAA//+mADgbXAEAAA==\"") _ = packr.PackJSONBytes("../embedded", "db/postgres/migrations/20180828083820_init-schema.down.sql", "\"H4sIAAAAAAAA/1SNMcvCMBiE9/dX3BDI9y3+geJQk7c2YFNpInYrQaNmaAuayV8vtCB4yw13D49uIQTteG8sEWAqcG+cd3+EJY4PrDxel0ccwzCFMaLq2gZpus3PMeQ0T8M6btbKAeeaO/5BtpDvlNMQr/coCfgnwNdsFwn3rE6eIXXXHuFUzU2J7x2qdKrULAsC2GqYqiBiq0mI4hMAAP//7wIpBL4AAAA=\"") _ = packr.PackJSONBytes("../embedded", "db/postgres/migrations/20180828083820_init-schema.up.sql", "\"H4sIAAAAAAAA/2SNsUrGMBzE9/9T3BCILr5AcIjJ1Qb8UkkiflsJGjVD+4F28umFFgTxlhvufvz8BKXkjvchigBhAM8hl3wl2JP5QFfw9fLRljqvdWkY0nRCX98un0vd+mWdj/HmqK3ieWTiH+QW+rtvfW6v700LcC1AGRl3Cc90T4XQPk2PyG7kyeL3Dmezs57aCMDoEQYjwuhFKSPiEm3hP8r8BAAA//+lS2Hg2AAAAA==\"") _ = packr.PackJSONBytes("../embedded", "db/postgres/migrations/20180905100347_initial-tables.down.sql", "\"H4sIAAAAAAAA/wEAAP//AAAAAAAAAAA=\"") diff --git a/controller/env/appenv.go b/controller/env/appenv.go index ac376d40f..20b636797 100644 --- a/controller/env/appenv.go +++ b/controller/env/appenv.go @@ -149,6 +149,7 @@ type Schemes struct { EnrollUpdb *BasicEntitySchema EdgeRouter *BasicEntitySchema EdgeRouterPolicy *BasicEntitySchema + TransitRouter *BasicEntitySchema Identity *IdentityEntitySchema Service *BasicEntitySchema ServiceEdgeRouterPolicy *BasicEntitySchema diff --git a/controller/internal/routes/a_routes-packr.go b/controller/internal/routes/a_routes-packr.go index c5bed8942..5ca48957b 100644 --- a/controller/internal/routes/a_routes-packr.go +++ b/controller/internal/routes/a_routes-packr.go @@ -7,5 +7,5 @@ import "github.com/gobuffalo/packr" // You can use the "packr clean" command to clean up this, // and any other packr generated files. func init() { - _ = packr.PackJSONBytes("../../../specs", "swagger.yml", "\"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\"") + _ = packr.PackJSONBytes("../../../specs", "swagger.yml", "\"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\"") } diff --git a/controller/internal/routes/edge_router_api_model.go b/controller/internal/routes/edge_router_api_model.go index 34db5a7b2..9b6187594 100644 --- a/controller/internal/routes/edge_router_api_model.go +++ b/controller/internal/routes/edge_router_api_model.go @@ -150,17 +150,40 @@ func MapEdgeRouterToApiList(ae *env.AppEnv, i *model.EdgeRouter) (*EdgeRouterApi BaseApi: env.FromBaseModelEntity(i), Name: &i.Name, RoleAttributes: i.RoleAttributes, - EnrollmentToken: i.EnrollmentToken, - EnrollmentCreatedAt: i.EnrollmentCreatedAt, - EnrollmentExpiresAt: i.EnrollmentExpiresAt, + EnrollmentToken: nil, + EnrollmentCreatedAt: nil, + EnrollmentExpiresAt: nil, + EnrollmentJwt: nil, IsOnline: &isOnline, IsVerified: &i.IsVerified, Fingerprint: i.Fingerprint, - EnrollmentJwt: i.EnrollmentJwt, Hostname: &hostname, SupportedProtocols: protocols, } + if !i.IsVerified { + var enrollments []*model.Enrollment + + err := ae.GetHandlers().EdgeRouter.CollectEnrollments(i.Id, func(entity *model.Enrollment) error { + enrollments = append(enrollments, entity) + return nil + }) + + if err != nil { + return nil, err + } + + if len(enrollments) != 1 { + return nil, fmt.Errorf("expected enrollment not found for unverified edge router %s", i.Id) + } + enrollment := enrollments[0] + + ret.EnrollmentExpiresAt = enrollment.ExpiresAt + ret.EnrollmentCreatedAt = &enrollment.CreatedAt + ret.EnrollmentJwt = &enrollment.Jwt + ret.EnrollmentToken = &enrollment.Token + } + ret.PopulateLinks() return ret, nil diff --git a/controller/internal/routes/enrollment_api_model.go b/controller/internal/routes/enrollment_api_model.go index 79bb57b91..9633c9329 100644 --- a/controller/internal/routes/enrollment_api_model.go +++ b/controller/internal/routes/enrollment_api_model.go @@ -31,11 +31,13 @@ const EntityNameEnrollment = "enrollments" type EnrollmentApiList struct { *env.BaseApi - Token *string `json:"token"` - Method *string `json:"method"` - ExpiresAt *time.Time `json:"expiresAt"` - Identity *EntityApiRef `json:"identity"` - Details interface{} `json:"details"` + Token *string `json:"token"` + Method *string `json:"method"` + ExpiresAt *time.Time `json:"expiresAt"` + Identity *EntityApiRef `json:"identity"` + Details interface{} `json:"details"` + EdgeRouter *EntityApiRef `json:"edgeRouter"` + TransitRouter *EntityApiRef `json:"transitRouter"` } func (e *EnrollmentApiList) GetSelfLink() *response.Link { @@ -101,19 +103,35 @@ func MapEnrollmentToApiEntity(appEnv *env.AppEnv, context *response.RequestConte } func MapToEnrollmentApiList(ae *env.AppEnv, enrollment *model.Enrollment) (*EnrollmentApiList, error) { - - identity, err := ae.Handlers.Identity.Read(enrollment.IdentityId) - - if err != nil { - return nil, err - } - ret := &EnrollmentApiList{ BaseApi: env.FromBaseModelEntity(enrollment), Token: &enrollment.Token, Method: &enrollment.Method, ExpiresAt: enrollment.ExpiresAt, - Identity: NewIdentityEntityRef(identity), + } + + if enrollment.IdentityId != nil { + identity, err := ae.Handlers.Identity.Read(*enrollment.IdentityId) + if err != nil { + return nil, err + } + ret.Identity = NewIdentityEntityRef(identity) + } + + if enrollment.EdgeRouterId != nil { + edgeRouter, err := ae.Handlers.EdgeRouter.Read(*enrollment.EdgeRouterId) + if err != nil { + return nil, err + } + ret.EdgeRouter = NewEdgeRouterEntityRef(edgeRouter) + } + + if enrollment.TransitRouterId != nil { + transitRouter, err := ae.Handlers.TransitRouter.Read(*enrollment.TransitRouterId) + if err != nil { + return nil, err + } + ret.TransitRouter = NewTransitRouterEntityRef(transitRouter) } ret.PopulateLinks() diff --git a/controller/internal/routes/transit_router_api_model.go b/controller/internal/routes/transit_router_api_model.go new file mode 100644 index 000000000..275f83c7b --- /dev/null +++ b/controller/internal/routes/transit_router_api_model.go @@ -0,0 +1,130 @@ +package routes + +import ( + "fmt" + "github.com/michaelquigley/pfxlog" + "github.com/netfoundry/ziti-edge/controller/env" + "github.com/netfoundry/ziti-edge/controller/model" + "github.com/netfoundry/ziti-edge/controller/response" + "github.com/netfoundry/ziti-fabric/controller/models" + "time" +) + +const EntityNameTransitRouter = "transit-routers" + +func NewTransitRouterEntityRef(entity *model.TransitRouter) *EntityApiRef { + links := &response.Links{ + "self": NewTransitRouterLink(entity.Id), + } + + return &EntityApiRef{ + Entity: EntityNameEdgeRouter, + Id: entity.Id, + Name: &entity.Name, + Links: links, + } +} + +func NewTransitRouterLink(id string) *response.Link { + return response.NewLink(fmt.Sprintf("./%s/%s", EntityNameTransitRouter, id)) +} + +type TransitRouterApi struct { + Name string `json:"name"` + Tags map[string]interface{} `json:"tags"` +} + +func (i TransitRouterApi) ToModel(id string) *model.TransitRouter { + ret := &model.TransitRouter{ + BaseEntity: models.BaseEntity{ + Tags: i.Tags, + Id: id, + }, + Name: i.Name, + } + return ret +} + +type TransitRouterApiList struct { + *env.BaseApi + Name string `json:"name"` + Fingerprint string `json:"fingerprint"` + IsVerified bool `json:"isVerified"` + IsOnline bool `json:"isOnline"` + EnrollmentToken *string `json:"enrollmentToken"` + EnrollmentJwt *string `json:"enrollmentJwt"` + EnrollmentCreatedAt *time.Time `json:"enrollmentCreatedAt"` + EnrollmentExpiresAt *time.Time `json:"enrollmentExpiresAt"` +} + +func (e TransitRouterApiList) GetSelfLink() *response.Link { + return e.BuildSelfLink(e.Id) +} + +func (e TransitRouterApiList) PopulateLinks() { + if e.Links == nil { + e.Links = &response.Links{ + EntityNameSelf: e.GetSelfLink(), + } + } +} + +func (e TransitRouterApiList) ToEntityApiRef() *EntityApiRef { + e.PopulateLinks() + return &EntityApiRef{ + Entity: EntityNameTransitRouter, + Name: nil, + Id: e.Id, + Links: e.Links, + } +} + +func (e TransitRouterApiList) BuildSelfLink(id string) *response.Link { + return NewTransitRouterLink(id) +} + +func MapTransitRouterToApiEntity(appEnv *env.AppEnv, context *response.RequestContext, entity models.Entity) (BaseApiEntity, error) { + txRouter, ok := entity.(*model.TransitRouter) + + if !ok { + err := fmt.Errorf("entity is not a transit router \"%s\"", entity.GetId()) + log := pfxlog.Logger() + log.Error(err) + return nil, err + } + + ret := &TransitRouterApiList{ + BaseApi: env.FromBaseModelEntity(entity), + Name: txRouter.Name, + Fingerprint: txRouter.Fingerprint, + IsVerified: txRouter.IsVerified, + IsOnline: appEnv.GetHandlers().Router.IsConnected(entity.GetId()), + } + + if !txRouter.IsBase && !txRouter.IsVerified { + var enrollments []*model.Enrollment + + err := appEnv.GetHandlers().TransitRouter.CollectEnrollments(txRouter.Id, func(entity *model.Enrollment) error { + enrollments = append(enrollments, entity) + return nil + }) + + if err != nil { + return nil, err + } + + if len(enrollments) != 1 { + return nil, fmt.Errorf("expected enrollment not found for unverified transit router %s", txRouter.Id) + } + enrollment := enrollments[0] + + ret.EnrollmentExpiresAt = enrollment.ExpiresAt + ret.EnrollmentCreatedAt = &enrollment.CreatedAt + ret.EnrollmentJwt = &enrollment.Jwt + ret.EnrollmentToken = &enrollment.Token + } + + ret.PopulateLinks() + + return ret, nil +} diff --git a/controller/internal/routes/transit_router_router.go b/controller/internal/routes/transit_router_router.go new file mode 100644 index 000000000..67d8df23d --- /dev/null +++ b/controller/internal/routes/transit_router_router.go @@ -0,0 +1,83 @@ +/* + 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 routes + +import ( + "github.com/netfoundry/ziti-edge/controller/env" + "github.com/netfoundry/ziti-edge/controller/internal/permissions" + "github.com/netfoundry/ziti-edge/controller/response" +) + +func init() { + r := NewTransitRouterRouter() + env.AddRouter(r) +} + +type TransitRouterRouter struct { + BasePath string + IdType response.IdType +} + +func NewTransitRouterRouter() *TransitRouterRouter { + return &TransitRouterRouter{ + BasePath: "/" + EntityNameTransitRouter, + IdType: response.IdTypeUuid, + } +} + +func (ir *TransitRouterRouter) Register(ae *env.AppEnv) { + _ = registerCrudRouter(ae, ae.RootRouter, ir.BasePath, ir, &crudResolvers{ + Create: permissions.IsAdmin(), + Read: permissions.IsAdmin(), + Update: permissions.IsAdmin(), + Delete: permissions.IsAdmin(), + Default: permissions.IsAdmin(), + }) +} + +func (ir *TransitRouterRouter) List(ae *env.AppEnv, rc *response.RequestContext) { + ListWithHandler(ae, rc, ae.Handlers.TransitRouter, MapTransitRouterToApiEntity) +} + +func (ir *TransitRouterRouter) Detail(ae *env.AppEnv, rc *response.RequestContext) { + DetailWithHandler(ae, rc, ae.Handlers.TransitRouter, MapTransitRouterToApiEntity, ir.IdType) +} + +func (ir *TransitRouterRouter) Create(ae *env.AppEnv, rc *response.RequestContext) { + transitRouterCreate := &TransitRouterApi{} + Create(rc, rc.RequestResponder, ae.Schemes.TransitRouter.Post, transitRouterCreate, (&TransitRouterApiList{}).BuildSelfLink, func() (string, error) { + return ae.Handlers.TransitRouter.Create(transitRouterCreate.ToModel("")) + }) +} + +func (ir *TransitRouterRouter) Delete(ae *env.AppEnv, rc *response.RequestContext) { + DeleteWithHandler(rc, ir.IdType, ae.Handlers.TransitRouter) +} + +func (ir *TransitRouterRouter) Update(ae *env.AppEnv, rc *response.RequestContext) { + transitRouterUpdate := &TransitRouterApi{} + Update(rc, ae.Schemes.TransitRouter.Put, ir.IdType, transitRouterUpdate, func(id string) error { + return ae.Handlers.TransitRouter.Update(transitRouterUpdate.ToModel(id)) + }) +} + +func (ir *TransitRouterRouter) Patch(ae *env.AppEnv, rc *response.RequestContext) { + transitRouterUpdate := &TransitRouterApi{} + Patch(rc, ae.Schemes.TransitRouter.Patch, ir.IdType, transitRouterUpdate, func(id string, fields JsonFields) error { + return ae.Handlers.TransitRouter.Patch(transitRouterUpdate.ToModel(id), fields.ConcatNestedNames().FilterMaps("tags")) + }) +} diff --git a/controller/model/edge_router_handlers.go b/controller/model/edge_router_handlers.go index 2505a3d3d..dec78e15a 100644 --- a/controller/model/edge_router_handlers.go +++ b/controller/model/edge_router_handlers.go @@ -18,6 +18,8 @@ package model import ( "fmt" + "github.com/google/uuid" + "github.com/michaelquigley/pfxlog" "github.com/netfoundry/ziti-edge/controller/persistence" "github.com/netfoundry/ziti-fabric/controller/models" "github.com/netfoundry/ziti-foundation/storage/boltz" @@ -49,7 +51,14 @@ func (handler *EdgeRouterHandler) newModelEntity() boltEntitySink { } func (handler *EdgeRouterHandler) Create(modelEntity *EdgeRouter) (string, error) { - return handler.createEntity(modelEntity) + enrollment := &Enrollment{ + BaseEntity: models.BaseEntity{}, + Method: MethodEnrollEdgeRouterOtt, + } + + id, _, err := handler.CreateWithEnrollment(modelEntity, enrollment) + + return id, err } func (handler *EdgeRouterHandler) Read(id string) (*EdgeRouter, error) { @@ -154,6 +163,77 @@ func (handler *EdgeRouterHandler) QueryRoleAttributes(queryString string) ([]str return handler.queryRoleAttributes(index, queryString) } +func (handler *EdgeRouterHandler) CreateWithEnrollment(edgeRouter *EdgeRouter, enrollment *Enrollment) (string, string, error) { + if edgeRouter.Id == "" { + edgeRouter.Id = uuid.New().String() + } + + if enrollment.Id == "" { + enrollment.Id = uuid.New().String() + } + + err := handler.GetDb().Update(func(tx *bbolt.Tx) error { + ctx := boltz.NewMutateContext(tx) + boltEdgeRouter, err := edgeRouter.toBoltEntityForCreate(tx, handler.impl) + if err != nil { + return err + } + if err := handler.GetStore().Create(ctx, boltEdgeRouter); err != nil { + pfxlog.Logger().WithError(err).Errorf("could not create %v in bolt storage", handler.GetStore().GetSingularEntityType()) + return err + } + + enrollment.EdgeRouterId = &edgeRouter.Id + + err = enrollment.FillJwtInfo(handler.env, edgeRouter.Id) + + if err != nil { + return err + } + + _, err = handler.env.GetHandlers().Enrollment.createEntityInTx(ctx, enrollment) + + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return "", "", err + } + + return edgeRouter.Id, enrollment.Id, nil +} + +func (handler *EdgeRouterHandler) CollectEnrollments(id string, collector func(entity *Enrollment) error) error { + return handler.GetDb().View(func(tx *bbolt.Tx) error { + return handler.collectEnrollmentsInTx(tx, id, collector) + }) +} + +func (handler *EdgeRouterHandler) collectEnrollmentsInTx(tx *bbolt.Tx, id string, collector func(entity *Enrollment) error) error { + _, err := handler.readInTx(tx, id) + if err != nil { + return err + } + + associationIds := handler.GetStore().GetRelatedEntitiesIdList(tx, id, persistence.FieldEdgeRouterEnrollments) + for _, enrollmentId := range associationIds { + enrollment, err := handler.env.GetHandlers().Enrollment.readInTx(tx, enrollmentId) + if err != nil { + return err + } + err = collector(enrollment) + + if err != nil { + return err + } + } + return nil +} + type EdgeRouterListResult struct { handler *EdgeRouterHandler EdgeRouters []*EdgeRouter diff --git a/controller/model/edge_router_model.go b/controller/model/edge_router_model.go index f60bc0e4e..298532d40 100644 --- a/controller/model/edge_router_model.go +++ b/controller/model/edge_router_model.go @@ -17,17 +17,12 @@ package model import ( - "fmt" - "github.com/dgrijalva/jwt-go" - "github.com/google/uuid" "github.com/netfoundry/ziti-edge/controller/persistence" "github.com/netfoundry/ziti-fabric/controller/models" "github.com/netfoundry/ziti-foundation/storage/boltz" - "github.com/netfoundry/ziti-sdk-golang/ziti/config" "github.com/pkg/errors" "go.etcd.io/bbolt" "reflect" - "time" ) type EdgeRouter struct { @@ -37,57 +32,19 @@ type EdgeRouter struct { IsVerified bool Fingerprint *string CertPem *string - EnrollmentToken *string Hostname *string - EnrollmentJwt *string - EnrollmentCreatedAt *time.Time - EnrollmentExpiresAt *time.Time EdgeRouterProtocols map[string]string } func (entity *EdgeRouter) toBoltEntityForCreate(_ *bbolt.Tx, handler Handler) (boltz.Entity, error) { - et := uuid.New().String() boltEntity := &persistence.EdgeRouter{ - BaseExtEntity: *boltz.NewExtEntity(entity.Id, entity.Tags), - Name: entity.Name, - RoleAttributes: entity.RoleAttributes, - Fingerprint: nil, - IsVerified: false, - EnrollmentToken: &et, + BaseExtEntity: *boltz.NewExtEntity(entity.Id, entity.Tags), + Name: entity.Name, + RoleAttributes: entity.RoleAttributes, + Fingerprint: nil, + IsVerified: false, } - env := handler.GetEnv() - - now := time.Now() - exp := now.Add(env.GetConfig().Enrollment.EdgeRouter.DurationMinutes) - - boltEntity.EnrollmentCreatedAt = &now - boltEntity.EnrollmentExpiresAt = &exp - - enrollmentMethod := "erott" - - claims := &config.EnrollmentClaims{ - EnrollmentMethod: enrollmentMethod, - StandardClaims: jwt.StandardClaims{ - ExpiresAt: exp.Unix(), - Id: *boltEntity.EnrollmentToken, - Issuer: fmt.Sprintf(`https://%s`, env.GetConfig().Api.Advertise), - Subject: boltEntity.Id, - }, - } - mapClaims, err := claims.ToMapClaims() - - if err != nil { - return nil, fmt.Errorf("could not convert edge router enrollment claims to interface map: %s", err) - } - - jwtJson, err := env.GetEnrollmentJwtGenerator().Generate(boltEntity.Id, boltEntity.Id, mapClaims) - - if err != nil { - return nil, err - } - - boltEntity.EnrollmentJwt = &jwtJson return boltEntity, nil } @@ -100,11 +57,7 @@ func (entity *EdgeRouter) toBoltEntityForUpdate(_ *bbolt.Tx, _ Handler) (boltz.E IsVerified: entity.IsVerified, Fingerprint: entity.Fingerprint, CertPem: entity.CertPem, - EnrollmentToken: entity.EnrollmentToken, Hostname: entity.Hostname, - EnrollmentJwt: entity.EnrollmentJwt, - EnrollmentCreatedAt: entity.EnrollmentCreatedAt, - EnrollmentExpiresAt: entity.EnrollmentExpiresAt, EdgeRouterProtocols: entity.EdgeRouterProtocols, }, nil } @@ -125,11 +78,7 @@ func (entity *EdgeRouter) fillFrom(_ Handler, _ *bbolt.Tx, boltEntity boltz.Enti entity.IsVerified = boltEdgeRouter.IsVerified entity.Fingerprint = boltEdgeRouter.Fingerprint entity.CertPem = boltEdgeRouter.CertPem - entity.EnrollmentToken = boltEdgeRouter.EnrollmentToken entity.Hostname = boltEdgeRouter.Hostname - entity.EnrollmentJwt = boltEdgeRouter.EnrollmentJwt - entity.EnrollmentCreatedAt = boltEdgeRouter.EnrollmentCreatedAt - entity.EnrollmentExpiresAt = boltEdgeRouter.EnrollmentExpiresAt entity.EdgeRouterProtocols = boltEdgeRouter.EdgeRouterProtocols return nil diff --git a/controller/model/enrollment_handler.go b/controller/model/enrollment_handler.go index aee21a1bd..57026640b 100644 --- a/controller/model/enrollment_handler.go +++ b/controller/model/enrollment_handler.go @@ -17,7 +17,6 @@ package model import ( - "fmt" "github.com/netfoundry/ziti-edge/controller/apierror" "github.com/netfoundry/ziti-edge/controller/persistence" "github.com/netfoundry/ziti-foundation/storage/boltz" @@ -57,22 +56,11 @@ func (handler *EnrollmentHandler) getEnrollmentMethod(ctx EnrollmentContext) (st } if enrollment == nil { - edgeRouter, err := handler.env.GetHandlers().EdgeRouter.ReadOneByQuery(fmt.Sprintf("enrollmentToken = \"%v\"", ctx.GetToken())) - - if err != nil { - return "", err - } - - if edgeRouter != nil { - method = MethodEnrollErOtt - } else { - return "", apierror.NewInvalidEnrollmentToken() - } - - } else { - method = enrollment.Method + return "", apierror.NewInvalidEnrollmentToken() } + method = enrollment.Method + return method, nil } @@ -142,3 +130,11 @@ func (handler *EnrollmentHandler) readInTx(tx *bbolt.Tx, id string) (*Enrollment func (handler *EnrollmentHandler) Delete(id string) error { return handler.deleteEntity(id) } + +func (handler *EnrollmentHandler) Read(id string) (*Enrollment, error) { + entity := &Enrollment{} + if err := handler.readEntity(id, entity); err != nil { + return nil, err + } + return entity, nil +} diff --git a/controller/model/enrollment_mod_erott.go b/controller/model/enrollment_mod_erott.go index 6b29b723e..fd2abd479 100644 --- a/controller/model/enrollment_mod_erott.go +++ b/controller/model/enrollment_mod_erott.go @@ -31,8 +31,7 @@ import ( const ( EdgeRouterEnrollmentCommonNameInvalidCode = "EDGE_ROUTER_ENROLL_COMMON_NAME_INVALID" EdgeRouterEnrollmentCommonNameInvalidMessage = "The edge router CSR enrollment must have a common name that matches the edge router's id" - - MethodEnrollErOtt = "erott" + MethodEnrollEdgeRouterOtt = "erott" ) type EnrollModuleEr struct { @@ -41,10 +40,10 @@ type EnrollModuleEr struct { fingerprintGenerator cert.FingerprintGenerator } -func NewEnrollModuleEr(env Env) *EnrollModuleEr { +func NewEnrollModuleEdgeRouterOtt(env Env) *EnrollModuleEr { handler := &EnrollModuleEr{ env: env, - method: MethodEnrollErOtt, + method: MethodEnrollEdgeRouterOtt, fingerprintGenerator: cert.NewFingerprintGenerator(), } @@ -56,18 +55,23 @@ func (module *EnrollModuleEr) CanHandle(method string) bool { } func (module *EnrollModuleEr) Process(context EnrollmentContext) (*EnrollmentResult, error) { - query := fmt.Sprintf(`isVerified = false and enrollmentToken = "%v"`, context.GetToken()) - gw, err := module.env.GetHandlers().EdgeRouter.ReadOneByQuery(query) + enrollment, err := module.env.GetHandlers().Enrollment.ReadByToken(context.GetToken()) if err != nil { return nil, err } - if gw == nil { + if enrollment == nil || enrollment.EdgeRouterId == nil { return nil, apierror.NewInvalidEnrollmentToken() } - if time.Now().After(*gw.EnrollmentExpiresAt) { + edgeRouter, err := module.env.GetHandlers().EdgeRouter.Read(*enrollment.EdgeRouterId) + + if edgeRouter == nil { + return nil, apierror.NewInvalidEnrollmentToken() + } + + if time.Now().After(*enrollment.ExpiresAt) { return nil, apierror.NewEnrollmentExpired() } @@ -136,7 +140,7 @@ func (module *EnrollModuleEr) Process(context EnrollmentContext) (*EnrollmentRes URIs: cr.URIs, } - if cr.Subject.CommonName != gw.Id { + if cr.Subject.CommonName != edgeRouter.Id { return nil, &apierror.ApiError{ Code: EdgeRouterEnrollmentCommonNameInvalidCode, @@ -162,16 +166,16 @@ func (module *EnrollModuleEr) Process(context EnrollmentContext) (*EnrollmentRes cltFp := module.fingerprintGenerator.FromPem(cltPem) - gw.IsVerified = true - gw.EnrollmentCreatedAt = nil - gw.EnrollmentExpiresAt = nil - gw.EnrollmentJwt = nil - gw.EnrollmentToken = nil - gw.Fingerprint = &cltFp - if err := module.env.GetHandlers().EdgeRouter.Update(gw, false); err != nil { + edgeRouter.IsVerified = true + edgeRouter.Fingerprint = &cltFp + if err := module.env.GetHandlers().EdgeRouter.Update(edgeRouter, false); err != nil { return nil, fmt.Errorf("could not update edge router: %s", err) } + if err := module.env.GetHandlers().Enrollment.Delete(enrollment.Id); err != nil { + return nil, fmt.Errorf("could not delete enrollment: %s", err) + } + if err := module.createRouter(cr.Subject.CommonName, cltFp); err != nil { return nil, err } diff --git a/controller/model/enrollment_mod_ott.go b/controller/model/enrollment_mod_ott.go index e831c2073..2d0f72c0a 100644 --- a/controller/model/enrollment_mod_ott.go +++ b/controller/model/enrollment_mod_ott.go @@ -51,11 +51,11 @@ func (module *EnrollModuleOtt) Process(ctx EnrollmentContext) (*EnrollmentResult return nil, err } - if enrollment == nil { + if enrollment == nil || enrollment.IdentityId == nil { return nil, apierror.NewInvalidEnrollmentToken() } - identity, err := module.env.GetHandlers().Identity.Read(enrollment.IdentityId) + identity, err := module.env.GetHandlers().Identity.Read(*enrollment.IdentityId) if err != nil { return nil, err @@ -86,7 +86,7 @@ func (module *EnrollModuleOtt) Process(ctx EnrollmentContext) (*EnrollmentResult Id: uuid.New().String(), }, Method: persistence.MethodAuthenticatorCert, - IdentityId: enrollment.IdentityId, + IdentityId: *enrollment.IdentityId, SubType: &AuthenticatorCert{ Fingerprint: fp, Pem: string(certPem), diff --git a/controller/model/enrollment_mod_ottca.go b/controller/model/enrollment_mod_ottca.go index e337d3e11..eed7b5256 100644 --- a/controller/model/enrollment_mod_ottca.go +++ b/controller/model/enrollment_mod_ottca.go @@ -51,11 +51,11 @@ func (module *EnrollModuleOttCa) Process(ctx EnrollmentContext) (*EnrollmentResu return nil, err } - if enrollment == nil { + if enrollment == nil || enrollment.IdentityId == nil { return nil, apierror.NewInvalidEnrollmentToken() } - identity, err := module.env.GetHandlers().Identity.Read(enrollment.IdentityId) + identity, err := module.env.GetHandlers().Identity.Read(*enrollment.IdentityId) if err != nil { return nil, err diff --git a/controller/model/enrollment_mod_trott.go b/controller/model/enrollment_mod_trott.go new file mode 100644 index 000000000..18855dd5f --- /dev/null +++ b/controller/model/enrollment_mod_trott.go @@ -0,0 +1,182 @@ +/* + 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 model + +import ( + "encoding/json" + "fmt" + "github.com/netfoundry/ziti-edge/controller/apierror" + "github.com/netfoundry/ziti-edge/controller/validation" + "github.com/netfoundry/ziti-edge/internal/cert" + "github.com/xeipuuv/gojsonschema" + "time" +) + +const ( + MethodEnrollTransitRouterOtt = "trott" +) + +type EnrollModuleRouterOtt struct { + env Env + method string + fingerprintGenerator cert.FingerprintGenerator +} + +func NewEnrollModuleTransitRouterOtt(env Env) *EnrollModuleRouterOtt { + handler := &EnrollModuleRouterOtt{ + env: env, + method: MethodEnrollTransitRouterOtt, + fingerprintGenerator: cert.NewFingerprintGenerator(), + } + + return handler +} + +func (module *EnrollModuleRouterOtt) CanHandle(method string) bool { + return method == module.method +} + +func (module *EnrollModuleRouterOtt) Process(context EnrollmentContext) (*EnrollmentResult, error) { + enrollment, err := module.env.GetHandlers().Enrollment.ReadByToken(context.GetToken()) + + if err != nil { + return nil, err + } + + if enrollment == nil || enrollment.TransitRouterId == nil { + return nil, apierror.NewInvalidEnrollmentToken() + } + + txRouter, err := module.env.GetHandlers().TransitRouter.Read(*enrollment.TransitRouterId) + + if txRouter == nil { + return nil, apierror.NewInvalidEnrollmentToken() + } + + if time.Now().After(*enrollment.ExpiresAt) { + return nil, apierror.NewEnrollmentExpired() + } + enrollData := context.GetDataAsMap() + result, err := module.env.GetSchemas().GetEnrollErPost().Validate(gojsonschema.NewGoLoader(enrollData)) + + if err != nil { + return nil, err + } + + if !result.Valid() { + var errs []*validation.SchemaValidationError + for _, re := range result.Errors() { + errs = append(errs, validation.NewValidationError(re)) + } + apiError := apierror.NewCouldNotValidate() + + if len(errs) > 0 { + apiError.Cause = errs[0] + apiError.AppendCause = true + } + + return nil, apiError + } + + sr, err := cert.ParseCsr([]byte(enrollData["serverCertCsr"].(string))) + + if err != nil { + apiErr := apierror.NewCouldNotProcessCsr() + apiErr.Cause = err + apiErr.AppendCause = true + return nil, apiErr + } + + so := &cert.SigningOpts{ + DNSNames: sr.DNSNames, + EmailAddresses: sr.EmailAddresses, + IPAddresses: sr.IPAddresses, + URIs: sr.URIs, + } + + srvCert, err := module.env.GetApiServerCsrSigner().Sign([]byte(enrollData["serverCertCsr"].(string)), so) + + if err != nil { + return nil, apierror.NewCouldNotProcessCsr() + } + + srvPem, err := module.env.GetApiServerCsrSigner().ToPem(srvCert) + + srvChain := string(srvPem) + module.env.GetApiServerCsrSigner().SigningCertPEM() + + if err != nil { + return nil, apierror.NewCouldNotProcessCsr() + } + + cr, err := cert.ParseCsr([]byte(enrollData["certCsr"].(string))) + + if err != nil { + return nil, apierror.NewCouldNotProcessCsr() + } + + so = &cert.SigningOpts{ + DNSNames: cr.DNSNames, + EmailAddresses: cr.EmailAddresses, + IPAddresses: cr.IPAddresses, + URIs: cr.URIs, + } + + cr.Subject.CommonName = txRouter.Id + + cltCert, err := module.env.GetControlClientCsrSigner().Sign([]byte(enrollData["certCsr"].(string)), so) + + if err != nil { + return nil, apierror.NewCouldNotProcessCsr() + } + + cltPem, err := module.env.GetControlClientCsrSigner().ToPem(cltCert) + + if err != nil { + return nil, apierror.NewCouldNotProcessCsr() + } + + cltFp := module.fingerprintGenerator.FromPem(cltPem) + + txRouter.IsVerified = true + txRouter.Fingerprint = cltFp + + if err := module.env.GetHandlers().TransitRouter.Update(txRouter); err != nil { + return nil, fmt.Errorf("could not update edge router: %s", err) + } + + if err := module.env.GetHandlers().Enrollment.Delete(enrollment.Id); err != nil { + return nil, fmt.Errorf("could not delete enrollment: %s", err) + } + + content, err := json.Marshal(&map[string]interface{}{ + "serverCert": srvChain, + "cert": string(cltPem), + "ca": string(module.env.GetConfig().CaPems()), + }) + + if err != nil { + return nil, err + } + + return &EnrollmentResult{ + Identity: nil, + Authenticator: nil, + Content: content, + ContentType: "application/json", + Status: 200, + }, nil +} diff --git a/controller/model/enrollment_mod_updb.go b/controller/model/enrollment_mod_updb.go index 4f0f0763b..f8891a6f1 100644 --- a/controller/model/enrollment_mod_updb.go +++ b/controller/model/enrollment_mod_updb.go @@ -54,11 +54,11 @@ func (module *EnrollModuleUpdb) Process(ctx EnrollmentContext) (*EnrollmentResul return nil, err } - if enrollment == nil { + if enrollment == nil || enrollment.IdentityId == nil { return nil, apierror.NewInvalidEnrollmentToken() } - identity, err := module.env.GetHandlers().Identity.Read(enrollment.IdentityId) + identity, err := module.env.GetHandlers().Identity.Read(*enrollment.IdentityId) if err != nil { return nil, err @@ -98,7 +98,7 @@ func (module *EnrollModuleUpdb) Process(ctx EnrollmentContext) (*EnrollmentResul Id: uuid.New().String(), }, Method: persistence.MethodAuthenticatorUpdb, - IdentityId: enrollment.IdentityId, + IdentityId: *enrollment.IdentityId, SubType: &AuthenticatorUpdb{ Username: *enrollment.Username, Password: encodedPassword, diff --git a/controller/model/enrollment_model.go b/controller/model/enrollment_model.go index 4ec76cbdf..965d8d786 100644 --- a/controller/model/enrollment_model.go +++ b/controller/model/enrollment_model.go @@ -33,17 +33,19 @@ import ( type Enrollment struct { models.BaseEntity - Method string - IdentityId string - Token string - IssuedAt *time.Time - ExpiresAt *time.Time - Jwt string - CaId *string - Username *string + Method string + IdentityId *string + TransitRouterId *string + EdgeRouterId *string + Token string + IssuedAt *time.Time + ExpiresAt *time.Time + Jwt string + CaId *string + Username *string } -func (entity *Enrollment) FillJwtInfo(env Env) error { +func (entity *Enrollment) FillJwtInfo(env Env, subject string) error { now := time.Now() expiresAt := now.Add(env.GetConfig().Enrollment.EdgeIdentity.DurationMinutes) entity.IssuedAt = &now @@ -61,7 +63,7 @@ func (entity *Enrollment) FillJwtInfo(env Env) error { Id: entity.Token, Issuer: fmt.Sprintf("https://%s", env.GetConfig().Api.Advertise), NotBefore: 0, - Subject: entity.IdentityId, + Subject: subject, }, } @@ -71,7 +73,7 @@ func (entity *Enrollment) FillJwtInfo(env Env) error { return err } - signedJwt, err := env.GetEnrollmentJwtGenerator().Generate(entity.IdentityId, entity.Id, mapClaims) + signedJwt, err := env.GetEnrollmentJwtGenerator().Generate(subject, entity.Id, mapClaims) if err != nil { return err @@ -90,6 +92,8 @@ func (entity *Enrollment) fillFrom(_ Handler, _ *bbolt.Tx, boltEntity boltz.Enti entity.FillCommon(boltEnrollment) entity.Method = boltEnrollment.Method entity.IdentityId = boltEnrollment.IdentityId + entity.TransitRouterId = boltEnrollment.TransitRouterId + entity.EdgeRouterId = boltEnrollment.EdgeRouterId entity.CaId = boltEnrollment.CaId entity.Username = boltEnrollment.Username entity.Token = boltEnrollment.Token @@ -120,15 +124,17 @@ func (entity *Enrollment) toBoltEntity(handler Handler) (boltz.Entity, error) { } boltEntity := &persistence.Enrollment{ - BaseExtEntity: *boltz.NewExtEntity(entity.Id, entity.Tags), - Method: entity.Method, - IdentityId: entity.IdentityId, - Token: entity.Token, - IssuedAt: entity.IssuedAt, - ExpiresAt: entity.ExpiresAt, - Jwt: entity.Jwt, - CaId: entity.CaId, - Username: entity.Username, + BaseExtEntity: *boltz.NewExtEntity(entity.Id, entity.Tags), + Method: entity.Method, + IdentityId: entity.IdentityId, + EdgeRouterId: entity.EdgeRouterId, + TransitRouterId: entity.TransitRouterId, + Token: entity.Token, + IssuedAt: entity.IssuedAt, + ExpiresAt: entity.ExpiresAt, + Jwt: entity.Jwt, + CaId: entity.CaId, + Username: entity.Username, } return boltEntity, nil diff --git a/controller/model/handlers.go b/controller/model/handlers.go index 9bcab8a81..91a71db89 100644 --- a/controller/model/handlers.go +++ b/controller/model/handlers.go @@ -33,6 +33,7 @@ type Handlers struct { EdgeRouterPolicy *EdgeRouterPolicyHandler EdgeService *EdgeServiceHandler EventLog *EventLogHandler + TransitRouter *TransitRouterHandler GeoRegion *GeoRegionHandler Identity *IdentityHandler IdentityType *IdentityTypeHandler @@ -64,6 +65,7 @@ func InitHandlers(env Env) *Handlers { handlers.Identity = NewIdentityHandler(env) handlers.IdentityType = NewIdentityTypeHandler(env) handlers.EdgeService = NewEdgeServiceHandler(env) + handlers.TransitRouter = NewTransitRouterHandler(env) handlers.ServiceEdgeRouterPolicy = NewServiceEdgeRouterPolicyHandler(env) handlers.ServicePolicy = NewServicePolicyHandler(env) handlers.Session = NewSessionHandler(env) diff --git a/controller/model/identity_handlers.go b/controller/model/identity_handlers.go index d7305d68b..4524c29c5 100644 --- a/controller/model/identity_handlers.go +++ b/controller/model/identity_handlers.go @@ -104,9 +104,9 @@ func (handler *IdentityHandler) CreateWithEnrollments(identityModel *Identity, e } for _, enrollmentModel := range enrollmentsModels { - enrollmentModel.IdentityId = identityModel.Id + enrollmentModel.IdentityId = &identityModel.Id - err := enrollmentModel.FillJwtInfo(handler.env) + err := enrollmentModel.FillJwtInfo(handler.env, identityModel.Id) if err != nil { return err diff --git a/controller/model/transit_router_handlers.go b/controller/model/transit_router_handlers.go new file mode 100644 index 000000000..4f20fbc12 --- /dev/null +++ b/controller/model/transit_router_handlers.go @@ -0,0 +1,162 @@ +/* + 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 model + +import ( + "github.com/google/uuid" + "github.com/michaelquigley/pfxlog" + "github.com/netfoundry/ziti-edge/controller/persistence" + "github.com/netfoundry/ziti-fabric/controller/models" + "github.com/netfoundry/ziti-foundation/storage/boltz" + "go.etcd.io/bbolt" +) + +func NewTransitRouterHandler(env Env) *TransitRouterHandler { + handler := &TransitRouterHandler{ + baseHandler: newBaseHandler(env, env.GetStores().TransitRouter), + } + handler.impl = handler + return handler +} + +type TransitRouterHandler struct { + baseHandler +} + +func (handler *TransitRouterHandler) Delete(id string) error { + return handler.deleteEntity(id) +} + +func (handler *TransitRouterHandler) newModelEntity() boltEntitySink { + return &TransitRouter{} +} + +func (handler *TransitRouterHandler) Create(entity *TransitRouter) (string, error) { + enrollment := &Enrollment{ + BaseEntity: models.BaseEntity{}, + Method: MethodEnrollTransitRouterOtt, + } + + id, _, err := handler.CreateWithEnrollment(entity, enrollment) + return id, err +} + +func (handler *TransitRouterHandler) CreateWithEnrollment(txRouter *TransitRouter, enrollment *Enrollment) (string, string, error) { + + if txRouter.Id == "" { + txRouter.Id = uuid.New().String() + } + var enrollmentId string + + err := handler.GetDb().Update(func(tx *bbolt.Tx) error { + ctx := boltz.NewMutateContext(tx) + boltEntity, err := txRouter.toBoltEntityForCreate(tx, handler.impl) + if err != nil { + return err + } + if err := handler.GetStore().Create(ctx, boltEntity); err != nil { + pfxlog.Logger().WithError(err).Errorf("could not create %v in bolt storage", handler.GetStore().GetSingularEntityType()) + return err + } + + enrollment.TransitRouterId = &txRouter.Id + + err = enrollment.FillJwtInfo(handler.env, txRouter.Id) + + if err != nil { + return err + } + + enrollmentId, err = handler.env.GetHandlers().Enrollment.createEntityInTx(ctx, enrollment) + + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return "", "", err + } + + return txRouter.Id, enrollmentId, nil +} + +func (handler *TransitRouterHandler) Update(entity *TransitRouter) error { + return handler.updateEntity(entity, nil) +} + +func (handler *TransitRouterHandler) Patch(entity *TransitRouter, checker boltz.FieldChecker) error { + return handler.patchEntity(entity, checker) +} + +func (handler *TransitRouterHandler) ReadOneByQuery(query string) (*TransitRouter, error) { + result, err := handler.readEntityByQuery(query) + if err != nil { + return nil, err + } + if result == nil { + return nil, nil + } + return result.(*TransitRouter), nil +} + +func (handler *TransitRouterHandler) Read(id string) (*TransitRouter, error) { + result := &TransitRouter{} + + if err := handler.readEntity(id, result); err != nil { + return nil, err + } + + return result, nil +} + +func (handler *TransitRouterHandler) readInTx(tx *bbolt.Tx, id string) (*TransitRouter, error) { + modelEntity := &TransitRouter{} + if err := handler.readEntityInTx(tx, id, modelEntity); err != nil { + return nil, err + } + return modelEntity, nil +} + +func (handler *TransitRouterHandler) CollectEnrollments(id string, collector func(entity *Enrollment) error) error { + return handler.GetDb().View(func(tx *bbolt.Tx) error { + return handler.collectEnrollmentsInTx(tx, id, collector) + }) +} + +func (handler *TransitRouterHandler) collectEnrollmentsInTx(tx *bbolt.Tx, id string, collector func(entity *Enrollment) error) error { + _, err := handler.readInTx(tx, id) + if err != nil { + return err + } + + associationIds := handler.GetStore().GetRelatedEntitiesIdList(tx, id, persistence.FieldTransitRouterEnrollments) + for _, enrollmentId := range associationIds { + enrollment, err := handler.env.GetHandlers().Enrollment.readInTx(tx, enrollmentId) + if err != nil { + return err + } + err = collector(enrollment) + + if err != nil { + return err + } + } + return nil +} diff --git a/controller/model/transit_router_model.go b/controller/model/transit_router_model.go new file mode 100644 index 000000000..e947f28f6 --- /dev/null +++ b/controller/model/transit_router_model.go @@ -0,0 +1,77 @@ +/* + 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 model + +import ( + "github.com/netfoundry/ziti-edge/controller/persistence" + "github.com/netfoundry/ziti-fabric/controller/db" + "github.com/netfoundry/ziti-fabric/controller/models" + "github.com/netfoundry/ziti-foundation/storage/boltz" + "github.com/pkg/errors" + "go.etcd.io/bbolt" + "reflect" +) + +type TransitRouter struct { + models.BaseEntity + Name string + Fingerprint string + IsVerified bool + IsBase bool +} + +func (entity *TransitRouter) toBoltEntityForCreate(tx *bbolt.Tx, handler Handler) (boltz.Entity, error) { + boltEntity := &persistence.TransitRouter{ + Router: db.Router{ + BaseExtEntity: *boltz.NewExtEntity(entity.Id, entity.Tags), + Fingerprint: entity.Fingerprint, + }, + Name: entity.Name, + IsVerified: false, + } + + return boltEntity, nil +} + +func (entity *TransitRouter) toBoltEntityForUpdate(tx *bbolt.Tx, handler Handler) (boltz.Entity, error) { + ret := &persistence.TransitRouter{ + Router: db.Router{ + BaseExtEntity: *boltz.NewExtEntity(entity.Id, entity.Tags), + }, + Name: entity.Name, + } + + return ret, nil +} + +func (entity *TransitRouter) toBoltEntityForPatch(tx *bbolt.Tx, handler Handler) (boltz.Entity, error) { + return entity.toBoltEntityForUpdate(tx, handler) +} + +func (entity *TransitRouter) fillFrom(_ Handler, _ *bbolt.Tx, boltEntity boltz.Entity) error { + boltTransitRouter, ok := boltEntity.(*persistence.TransitRouter) + if !ok { + return errors.Errorf("unexpected type %v when filling model transitRouter", reflect.TypeOf(boltEntity)) + } + entity.FillCommon(boltTransitRouter) + entity.Name = boltTransitRouter.Name + entity.IsVerified = boltTransitRouter.IsVerified + entity.IsBase = boltTransitRouter.IsBase + entity.Fingerprint = boltTransitRouter.Fingerprint + + return nil +} diff --git a/controller/persistence/base_entity.go b/controller/persistence/base_entity.go index 50bd9be89..c6643e4ff 100644 --- a/controller/persistence/base_entity.go +++ b/controller/persistence/base_entity.go @@ -26,6 +26,7 @@ const ( EntityTypeConfigs = "configs" EntityTypeConfigTypes = "configTypes" EntityTypeEdgeRouters = "edgeRouters" + EntityTypeTransitRouters = "transitRouters" EntityTypeEdgeRouterPolicies = "edgeRouterPolicies" EntityTypeEventLogs = "eventLogs" EntityTypeGeoRegions = "geoRegions" diff --git a/controller/persistence/base_store.go b/controller/persistence/base_store.go index 261ae7f14..bf4b497e1 100644 --- a/controller/persistence/base_store.go +++ b/controller/persistence/base_store.go @@ -64,6 +64,12 @@ func newChildBaseStore(stores *stores, parent boltz.CrudStore, entityType string } } +func newExtendedBaseStore(stores *stores, parent boltz.CrudStore, entityType string) *baseStore { + store := newChildBaseStore(stores, parent, entityType) + store.BaseStore.Extended() + return store +} + type baseStore struct { stores *stores *boltz.BaseStore diff --git a/controller/persistence/edge_router_store.go b/controller/persistence/edge_router_store.go index 2a9102222..174a1ccbe 100644 --- a/controller/persistence/edge_router_store.go +++ b/controller/persistence/edge_router_store.go @@ -17,6 +17,7 @@ package persistence import ( + "fmt" "github.com/google/uuid" "github.com/netfoundry/ziti-foundation/storage/ast" "github.com/netfoundry/ziti-foundation/storage/boltz" @@ -26,16 +27,15 @@ import ( ) const ( - FieldEdgeRouterFingerprint = "fingerprint" - FieldEdgeRouterCertPEM = "certPem" - FieldEdgeRouterCluster = "cluster" - FieldEdgeRouterIsVerified = "isVerified" - FieldEdgeRouterEnrollmentToken = "enrollmentToken" - FieldEdgeRouterHostname = "hostname" - FieldEdgeRouterEnrollmentJwt = "enrollmentJwt" - FieldEdgeRouterEnrollmentCreatedAt = "enrollmentCreatedAt" - FieldEdgeRouterEnrollmentExpiresAt = "enrollmentExpiresAt" - FieldEdgeRouterProtocols = "protocols" + FieldEdgeRouterFingerprint = "fingerprint" + FieldEdgeRouterCertPEM = "certPem" + FieldEdgeRouterCluster = "cluster" + FieldEdgeRouterIsVerified = "isVerified" + FieldEdgeRouterHostname = "hostname" + FieldEdgeRouterProtocols = "protocols" + FieldEdgeRouterEnrollments = "enrollments" + + MethodEnrollEdgeRouterOtt = "erott" ) func newEdgeRouter(name string, roleAttributes ...string) *EdgeRouter { @@ -53,13 +53,16 @@ type EdgeRouter struct { IsVerified bool Fingerprint *string CertPem *string - EnrollmentToken *string Hostname *string + EdgeRouterProtocols map[string]string + RoleAttributes []string + Enrollments []string + + //old v4, migrations only + EnrollmentToken *string EnrollmentJwt *string EnrollmentCreatedAt *time.Time EnrollmentExpiresAt *time.Time - EdgeRouterProtocols map[string]string - RoleAttributes []string } var edgeRouterFieldMappings = map[string]string{FieldEdgeRouterCluster: "clusterId"} @@ -72,11 +75,9 @@ func (entity *EdgeRouter) LoadValues(_ boltz.CrudStore, bucket *boltz.TypedBucke entity.ClusterId = bucket.GetString(FieldEdgeRouterCluster) entity.IsVerified = bucket.GetBoolWithDefault(FieldEdgeRouterIsVerified, false) - entity.EnrollmentToken = bucket.GetString(FieldEdgeRouterEnrollmentToken) + //old v4, migrations only + entity.Enrollments = bucket.GetStringList(FieldEdgeRouterEnrollments) entity.Hostname = bucket.GetString(FieldEdgeRouterHostname) - entity.EnrollmentJwt = bucket.GetString(FieldEdgeRouterEnrollmentJwt) - entity.EnrollmentCreatedAt = bucket.GetTime(FieldEdgeRouterEnrollmentCreatedAt) - entity.EnrollmentExpiresAt = bucket.GetTime(FieldEdgeRouterEnrollmentExpiresAt) entity.EdgeRouterProtocols = toStringStringMap(bucket.GetMap(FieldEdgeRouterProtocols)) entity.RoleAttributes = bucket.GetStringList(FieldRoleAttributes) } @@ -91,13 +92,10 @@ func (entity *EdgeRouter) SetValues(ctx *boltz.PersistContext) { ctx.SetStringP(FieldEdgeRouterCertPEM, entity.CertPem) ctx.SetStringP(FieldEdgeRouterCluster, entity.ClusterId) ctx.SetBool(FieldEdgeRouterIsVerified, entity.IsVerified) - ctx.SetStringP(FieldEdgeRouterEnrollmentToken, entity.EnrollmentToken) ctx.SetStringP(FieldEdgeRouterHostname, entity.Hostname) - ctx.SetStringP(FieldEdgeRouterEnrollmentJwt, entity.EnrollmentJwt) - ctx.SetTimeP(FieldEdgeRouterEnrollmentCreatedAt, entity.EnrollmentCreatedAt) - ctx.SetTimeP(FieldEdgeRouterEnrollmentExpiresAt, entity.EnrollmentExpiresAt) ctx.SetMap(FieldEdgeRouterProtocols, toStringInterfaceMap(entity.EdgeRouterProtocols)) ctx.SetStringList(FieldRoleAttributes, entity.RoleAttributes) + ctx.SetLinkedIds(FieldEdgeRouterEnrollments, entity.Enrollments) // index change won't fire if we don't have any roles on create, but we need to evaluate if we match any #all roles if ctx.IsCreate && len(entity.RoleAttributes) == 0 { @@ -137,6 +135,7 @@ type edgeRouterStoreImpl struct { symbolEdgeRouterPolicies boltz.EntitySetSymbol symbolServiceEdgeRouterPolicies boltz.EntitySetSymbol + symbolEnrollments boltz.EntitySetSymbol } func (store *edgeRouterStoreImpl) NewStoreEntity() boltz.Entity { @@ -155,9 +154,7 @@ func (store *edgeRouterStoreImpl) initializeLocal() { store.AddSymbol(FieldEdgeRouterFingerprint, ast.NodeTypeString) store.AddSymbol(FieldEdgeRouterIsVerified, ast.NodeTypeBool) - store.AddSymbol(FieldEdgeRouterEnrollmentToken, ast.NodeTypeString) - store.AddSymbol(FieldEdgeRouterEnrollmentCreatedAt, ast.NodeTypeString) - store.AddSymbol(FieldEdgeRouterEnrollmentExpiresAt, ast.NodeTypeString) + store.symbolEnrollments = store.AddFkSetSymbol(FieldEdgeRouterEnrollments, store.stores.enrollment) store.symbolEdgeRouterPolicies = store.AddFkSetSymbol(EntityTypeEdgeRouterPolicies, store.stores.edgeRouterPolicy) store.symbolServiceEdgeRouterPolicies = store.AddFkSetSymbol(EntityTypeServiceEdgeRouterPolicies, store.stores.serviceEdgeRouterPolicy) @@ -181,6 +178,7 @@ func (store *edgeRouterStoreImpl) rolesChanged(tx *bbolt.Tx, rowId []byte, _ []b func (store *edgeRouterStoreImpl) initializeLinked() { store.AddLinkCollection(store.symbolEdgeRouterPolicies, store.stores.edgeRouterPolicy.symbolEdgeRouters) store.AddLinkCollection(store.symbolServiceEdgeRouterPolicies, store.stores.serviceEdgeRouterPolicy.symbolEdgeRouters) + store.AddLinkCollection(store.symbolEnrollments, store.stores.enrollment.symbolEdgeRouter) } func (store *edgeRouterStoreImpl) GetNameIndex() boltz.ReadIndex { @@ -213,6 +211,11 @@ func (store *edgeRouterStoreImpl) DeleteById(ctx boltz.MutateContext, id string) if err := store.deleteEntityReferences(ctx.Tx(), entity, store.stores.serviceEdgeRouterPolicy.symbolEdgeRouterRoles); err != nil { return err } + + // Remove outstanding enrollments + if err := store.stores.enrollment.DeleteWhere(ctx, fmt.Sprintf(`edgeRouter="%s"`, entity.Id)); err != nil { + return err + } } if store.stores.Router.IsEntityPresent(ctx.Tx(), id) { diff --git a/controller/persistence/enrollment_store.go b/controller/persistence/enrollment_store.go index 7329e676c..812ffc353 100644 --- a/controller/persistence/enrollment_store.go +++ b/controller/persistence/enrollment_store.go @@ -27,6 +27,8 @@ const ( FieldEnrollmentToken = "token" FieldEnrollmentMethod = "method" FieldEnrollIdentity = "identity" + FieldEnrollEdgeRouter = "edgeRouter" + FieldEnrollTransitRouter = "transitRouter" FieldEnrollmentExpiresAt = "expiresAt" FieldEnrollmentIssuedAt = "issuedAt" FieldEnrollmentCaId = "caId" @@ -41,22 +43,30 @@ const ( type Enrollment struct { boltz.BaseExtEntity - Token string - Method string - IdentityId string - ExpiresAt *time.Time - IssuedAt *time.Time - CaId *string - Username *string - Jwt string + Token string + Method string + IdentityId *string + TransitRouterId *string + EdgeRouterId *string + ExpiresAt *time.Time + IssuedAt *time.Time + CaId *string + Username *string + Jwt string } -var enrollmentFieldMappings = map[string]string{FieldEnrollIdentity: "identityId"} +var enrollmentFieldMappings = map[string]string{ + FieldEnrollIdentity: "identityId", + FieldEnrollEdgeRouter: "edgeRouterId", + FieldEnrollTransitRouter: "transitRouterId", +} func (entity *Enrollment) LoadValues(_ boltz.CrudStore, bucket *boltz.TypedBucket) { entity.Token = bucket.GetStringWithDefault(FieldEnrollmentToken, "") entity.Method = bucket.GetStringWithDefault(FieldEnrollmentMethod, "") - entity.IdentityId = bucket.GetStringOrError(FieldEnrollIdentity) + entity.IdentityId = bucket.GetString(FieldEnrollIdentity) + entity.EdgeRouterId = bucket.GetString(FieldEnrollEdgeRouter) + entity.TransitRouterId = bucket.GetString(FieldEnrollTransitRouter) entity.ExpiresAt = bucket.GetTime(FieldEnrollmentExpiresAt) entity.IssuedAt = bucket.GetTime(FieldEnrollmentIssuedAt) entity.CaId = bucket.GetString(FieldEnrollmentCaId) @@ -70,7 +80,9 @@ func (entity *Enrollment) SetValues(ctx *boltz.PersistContext) { ctx.SetString(FieldEnrollmentToken, entity.Token) ctx.SetString(FieldEnrollmentMethod, entity.Method) ctx.SetTimeP(FieldEnrollmentExpiresAt, entity.ExpiresAt) - ctx.SetString(FieldEnrollIdentity, entity.IdentityId) + ctx.SetStringP(FieldEnrollIdentity, entity.IdentityId) + ctx.SetStringP(FieldEnrollEdgeRouter, entity.EdgeRouterId) + ctx.SetStringP(FieldEnrollTransitRouter, entity.TransitRouterId) ctx.SetStringP(FieldEnrollmentCaId, entity.CaId) ctx.SetStringP(FieldEnrollmentUsername, entity.Username) ctx.SetTimeP(FieldEnrollmentIssuedAt, entity.IssuedAt) @@ -99,8 +111,10 @@ func newEnrollmentStore(stores *stores) *enrollmentStoreImpl { type enrollmentStoreImpl struct { *baseStore - tokenIndex boltz.ReadIndex - symbolIdentityId boltz.EntitySymbol + tokenIndex boltz.ReadIndex + symbolIdentity boltz.EntitySymbol + symbolEdgeRouter boltz.EntitySymbol + symbolTransitRouter boltz.EntitySymbol } func (store *enrollmentStoreImpl) NewStoreEntity() boltz.Entity { @@ -109,14 +123,18 @@ func (store *enrollmentStoreImpl) NewStoreEntity() boltz.Entity { func (store *enrollmentStoreImpl) initializeLocal() { store.AddExtEntitySymbols() + symbolToken := store.AddSymbol(FieldEnrollmentToken, ast.NodeTypeString) store.tokenIndex = store.AddUniqueIndex(symbolToken) - - store.symbolIdentityId = store.AddFkSymbol(FieldEnrollIdentity, store.stores.identity) + store.symbolIdentity = store.AddFkSymbol(FieldEnrollIdentity, store.stores.identity) + store.symbolEdgeRouter = store.AddFkSymbol(FieldEnrollEdgeRouter, store.stores.edgeRouter) + store.symbolTransitRouter = store.AddFkSymbol(FieldEnrollTransitRouter, store.stores.transitRouter) } func (store *enrollmentStoreImpl) initializeLinked() { - store.AddFkIndex(store.symbolIdentityId, store.stores.identity.symbolEnrollments) + store.AddNullableFkIndex(store.symbolIdentity, store.stores.identity.symbolEnrollments) + store.AddNullableFkIndex(store.symbolEdgeRouter, store.stores.edgeRouter.symbolEnrollments) + store.AddNullableFkIndex(store.symbolTransitRouter, store.stores.transitRouter.symbolEnrollments) } func (store *enrollmentStoreImpl) LoadOneById(tx *bbolt.Tx, id string) (*Enrollment, error) { diff --git a/controller/persistence/identity_store.go b/controller/persistence/identity_store.go index 57949accc..d31073eb7 100644 --- a/controller/persistence/identity_store.go +++ b/controller/persistence/identity_store.go @@ -179,7 +179,7 @@ func (store *identityStoreImpl) rolesChanged(tx *bbolt.Tx, rowId []byte, _ []bol func (store *identityStoreImpl) initializeLinked() { store.AddLinkCollection(store.symbolAuthenticators, store.stores.authenticator.symbolIdentityId) - store.AddLinkCollection(store.symbolEnrollments, store.stores.enrollment.symbolIdentityId) + store.AddLinkCollection(store.symbolEnrollments, store.stores.enrollment.symbolIdentity) store.AddLinkCollection(store.symbolEdgeRouterPolicies, store.stores.edgeRouterPolicy.symbolIdentities) store.AddLinkCollection(store.symbolServicePolicies, store.stores.servicePolicy.symbolIdentities) } diff --git a/controller/persistence/migration_v5.go b/controller/persistence/migration_v5.go new file mode 100644 index 000000000..cb80fe50a --- /dev/null +++ b/controller/persistence/migration_v5.go @@ -0,0 +1,55 @@ +/* + 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 persistence + +import ( + + "github.com/netfoundry/ziti-foundation/storage/boltz" +) + +func (m *Migrations) createEnrollmentsForEdgeRouters(step *boltz.MigrationStep) { + edgeRouterIds, _, err := m.stores.EdgeRouter.QueryIds(step.Ctx.Tx(), "true") + step.SetError(err) + + for _, edgeRouterId := range edgeRouterIds { + edgeRouter, err := m.stores.EdgeRouter.LoadOneById(step.Ctx.Tx(), edgeRouterId) + if step.SetError(err) { + return + } + if edgeRouter.EnrollmentJwt == nil { + continue + } + + enrollment := &Enrollment{ + Token: *edgeRouter.EnrollmentToken, + Method: MethodEnrollEdgeRouterOtt, + EdgeRouterId: &edgeRouter.Id, + ExpiresAt: edgeRouter.EnrollmentExpiresAt, + IssuedAt: edgeRouter.EnrollmentCreatedAt, + Jwt: *edgeRouter.EnrollmentJwt, + } + + step.SetError(m.stores.Enrollment.Create(step.Ctx, enrollment)) + + edgeRouter.EnrollmentJwt = nil + edgeRouter.EnrollmentCreatedAt = nil + edgeRouter.EnrollmentExpiresAt = nil + edgeRouter.EnrollmentToken = nil + + step.SetError(m.stores.EdgeRouter.Update(step.Ctx, edgeRouter, nil)) + } +} diff --git a/controller/persistence/migrations.go b/controller/persistence/migrations.go index 9edd8e98e..f38e44c37 100644 --- a/controller/persistence/migrations.go +++ b/controller/persistence/migrations.go @@ -63,9 +63,13 @@ func (m *Migrations) migrate(step *boltz.MigrationStep) int { return 4 } - // current version if step.CurrentVersion == 4 { - return 4 + m.createEnrollmentsForEdgeRouters(step) + return 5 + } + // current version + if step.CurrentVersion == 5 { + return 5 } step.SetError(errors.Errorf("Unsupported edge datastore version: %v", step.CurrentVersion)) diff --git a/controller/persistence/migrations_v1_pg.go b/controller/persistence/migrations_v1_pg.go index dc385802b..9f9588e00 100644 --- a/controller/persistence/migrations_v1_pg.go +++ b/controller/persistence/migrations_v1_pg.go @@ -149,7 +149,7 @@ func (m *Migrations) migrateEnrollmentsFromPG(step *boltz.MigrationStep) error { BaseExtEntity: *toBaseBoltEntity(&pgEnrollment.BaseDbEntity), Token: *pgEnrollment.Token, Method: *pgEnrollment.Method, - IdentityId: *pgEnrollment.IdentityID, + IdentityId: pgEnrollment.IdentityID, ExpiresAt: pgEnrollment.ExpiresAt, IssuedAt: pgEnrollment.CreatedAt, CaId: nil, diff --git a/controller/persistence/stores.go b/controller/persistence/stores.go index 4c08f2575..769a5d0a4 100644 --- a/controller/persistence/stores.go +++ b/controller/persistence/stores.go @@ -51,6 +51,7 @@ type Stores struct { Session SessionStore ServiceEdgeRouterPolicy ServiceEdgeRouterPolicyStore ServicePolicy ServicePolicyStore + TransitRouter TransitRouterStore Enrollment EnrollmentStore Authenticator AuthenticatorStore @@ -108,6 +109,7 @@ type stores struct { serviceEdgeRouterPolicy *serviceEdgeRouterPolicyStoreImpl servicePolicy *servicePolicyStoreImpl session *sessionStoreImpl + transitRouter *transitRouterStoreImpl enrollment *enrollmentStoreImpl authenticator *authenticatorStoreImpl } @@ -134,6 +136,7 @@ func NewBoltStores(dbProvider DbProvider) (*Stores, error) { internalStores.edgeRouterPolicy = newEdgeRouterPolicyStore(internalStores) internalStores.edgeService = newEdgeServiceStore(internalStores) internalStores.eventLog = newEventLogStore(internalStores) + internalStores.transitRouter = newTransitRouterStore(internalStores) internalStores.geoRegion = newGeoRegionStore(internalStores) internalStores.identity = newIdentityStore(internalStores) internalStores.identityType = newIdentityTypeStore(internalStores) @@ -159,6 +162,7 @@ func NewBoltStores(dbProvider DbProvider) (*Stores, error) { EdgeRouterPolicy: internalStores.edgeRouterPolicy, EdgeService: internalStores.edgeService, EventLog: internalStores.eventLog, + TransitRouter: internalStores.transitRouter, GeoRegion: internalStores.geoRegion, Identity: internalStores.identity, IdentityType: internalStores.identityType, diff --git a/controller/persistence/transit_router_store.go b/controller/persistence/transit_router_store.go new file mode 100644 index 000000000..851fc7c3d --- /dev/null +++ b/controller/persistence/transit_router_store.go @@ -0,0 +1,183 @@ +/* + 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 persistence + +import ( + "fmt" + "github.com/google/uuid" + "github.com/netfoundry/ziti-fabric/controller/db" + "github.com/netfoundry/ziti-foundation/storage/boltz" + "go.etcd.io/bbolt" +) + +const ( + FieldTransitRouterIsVerified = "isVerified" + FieldTransitRouterEnrollments = "enrollments" + + //old + FieldTransitRouterEnrollmentToken = "enrollmentToken" + FieldTransitRouterEnrollmentJwt = "enrollmentJwt" + FieldTransitRouterEnrollmentCreatedAt = "enrollmentCreatedAt" + FieldTransitRouterEnrollmentExpiresAt = "enrollmentExpiresAt" +) + +type TransitRouter struct { + db.Router + Name string + IsVerified bool + Enrollments []string + IsBase bool +} + +func (entity *TransitRouter) GetId() string { + return entity.Id +} + +func (entity *TransitRouter) SetId(id string) { + entity.Id = id +} + +func newTransitRouter(name string, roleAttributes ...string) *TransitRouter { + return &TransitRouter{ + Router: db.Router{ + BaseExtEntity: boltz.BaseExtEntity{Id: uuid.New().String()}, + }, + Name: name, + } +} + +func (entity *TransitRouter) LoadValues(store boltz.CrudStore, bucket *boltz.TypedBucket) { + _, err := store.GetParentStore().BaseLoadOneById(bucket.Tx(), entity.Id, &entity.Router) + bucket.SetError(err) + + if bucket.Bucket == nil { + entity.IsVerified = true + entity.IsBase = true + return + } + entity.LoadBaseValues(bucket) + entity.Name = bucket.GetStringOrError(FieldName) + entity.IsVerified = bucket.GetBoolWithDefault(FieldTransitRouterIsVerified, false) + entity.Enrollments = bucket.GetStringList(FieldTransitRouterEnrollments) +} + +func (entity *TransitRouter) SetValues(ctx *boltz.PersistContext) { + entity.Router.SetValues(ctx.GetParentContext()) + entity.SetBaseValues(ctx) + ctx.SetString(FieldName, entity.Name) +} + +func (entity *TransitRouter) GetEntityType() string { + return EntityTypeTransitRouters +} + +func (entity *TransitRouter) GetName() string { + return entity.Name +} + +type TransitRouterStore interface { + NameIndexedStore + LoadOneById(tx *bbolt.Tx, id string) (*TransitRouter, error) + LoadOneByName(tx *bbolt.Tx, id string) (*TransitRouter, error) +} + +func newTransitRouterStore(stores *stores) *transitRouterStoreImpl { + store := &transitRouterStoreImpl{ + baseStore: newExtendedBaseStore(stores, stores.Router, EntityTypeTransitRouters), + } + store.InitImpl(store) + return store +} + +type transitRouterStoreImpl struct { + *baseStore + indexName boltz.ReadIndex + symbolEnrollments boltz.EntitySetSymbol +} + +func (store *transitRouterStoreImpl) NewStoreEntity() boltz.Entity { + return &TransitRouter{} +} + +func (store *transitRouterStoreImpl) initializeLocal() { + store.AddExtEntitySymbols() + store.GetParentStore().GrantSymbols(store) + + store.indexName = store.addUniqueNameField() + store.symbolEnrollments = store.AddFkSetSymbol(FieldTransitRouterEnrollments, store.stores.enrollment) +} + +func (store *transitRouterStoreImpl) initializeLinked() { + store.AddLinkCollection(store.symbolEnrollments, store.stores.enrollment.symbolIdentity) +} + +func (store *transitRouterStoreImpl) CleanupExternal(ctx boltz.MutateContext, id string) error { + entity, err := store.LoadOneById(ctx.Tx(), id) + if err != nil { + return err + } + + //no edge cleanup + if entity.IsBase { + return nil + } + + //edge cleanup + if err = store.stores.enrollment.DeleteWhere(ctx, fmt.Sprintf(`transitRouter="%s"`, id)); err != nil { + return nil + } + + return store.BaseStore.CleanupExternal(ctx, id) +} + +func (store *transitRouterStoreImpl) GetNameIndex() boltz.ReadIndex { + return store.indexName +} + +func (store *transitRouterStoreImpl) LoadOneById(tx *bbolt.Tx, id string) (*TransitRouter, error) { + entity := &TransitRouter{} + if err := store.baseLoadOneById(tx, id, entity); err != nil { + return nil, err + } + return entity, nil +} + +func (store *transitRouterStoreImpl) LoadOneByName(tx *bbolt.Tx, name string) (*TransitRouter, error) { + id := store.indexName.Read(tx, []byte(name)) + if id != nil { + return store.LoadOneById(tx, string(id)) + } + return nil, nil +} + +func (store *transitRouterStoreImpl) LoadOneByQuery(tx *bbolt.Tx, query string) (*TransitRouter, error) { + entity := &TransitRouter{} + if found, err := store.BaseLoadOneByQuery(tx, query, entity); !found || err != nil { + return nil, err + } + return entity, nil +} + +func (store *transitRouterStoreImpl) DeleteById(ctx boltz.MutateContext, id string) error { + if entity, _ := store.LoadOneById(ctx.Tx(), id); entity != nil { + // Remove outstanding enrollments + if err := store.stores.enrollment.DeleteWhere(ctx, fmt.Sprintf(`transitRouter="%s"`, entity.Id)); err != nil { + return err + } + } + return store.BaseStore.DeleteById(ctx, id) +} diff --git a/controller/response/context.go b/controller/response/context.go index cc1192e01..0f0447367 100644 --- a/controller/response/context.go +++ b/controller/response/context.go @@ -20,7 +20,6 @@ import ( "fmt" "github.com/google/uuid" "github.com/gorilla/mux" - "github.com/netfoundry/ziti-edge/controller/apierror" "github.com/netfoundry/ziti-edge/controller/model" "net/http" ) @@ -59,13 +58,13 @@ func (rc *RequestContext) GetIdFromRequest(idType IdType) (string, error) { return "", fmt.Errorf("id property '%s' not found in request", IdPropertyName) } - if idType == IdTypeUuid { - _, err := uuid.Parse(id) - - if err != nil { - return "", apierror.NewInvalidUuid(id) - } - } + //if idType == IdTypeUuid { + // _, err := uuid.Parse(id) + // + // if err != nil { + // return "", apierror.NewInvalidUuid(id) + // } + //} return id, nil } diff --git a/controller/server/controller.go b/controller/server/controller.go index 1cf4762a3..97e66c2fc 100644 --- a/controller/server/controller.go +++ b/controller/server/controller.go @@ -188,7 +188,8 @@ func (c *Controller) initializeAuthModules() { c.AppEnv.EnrollRegistry.Add(model.NewEnrollModuleCa(c.AppEnv)) c.AppEnv.EnrollRegistry.Add(model.NewEnrollModuleOttCa(c.AppEnv)) c.AppEnv.EnrollRegistry.Add(model.NewEnrollModuleOtt(c.AppEnv)) - c.AppEnv.EnrollRegistry.Add(model.NewEnrollModuleEr(c.AppEnv)) + c.AppEnv.EnrollRegistry.Add(model.NewEnrollModuleEdgeRouterOtt(c.AppEnv)) + c.AppEnv.EnrollRegistry.Add(model.NewEnrollModuleTransitRouterOtt(c.AppEnv)) c.AppEnv.EnrollRegistry.Add(model.NewEnrollModuleUpdb(c.AppEnv)) }) } diff --git a/gateway/enroll/enroll.go b/gateway/enroll/enroll.go index 13bfe6c89..da724d8b0 100644 --- a/gateway/enroll/enroll.go +++ b/gateway/enroll/enroll.go @@ -60,7 +60,7 @@ func NewRestEnroller() Enroller { func (re *RestEnroller) parseCfgMap(cfgmap map[interface{}]interface{}) (*gateway.Config, error) { config := gateway.NewConfig() - if err := config.LoadConfigFromMap(cfgmap); err != nil { + if err := config.LoadConfigFromMapForEnrollment(cfgmap); err != nil { return nil, err } diff --git a/gateway/internal/gateway/config.go b/gateway/internal/gateway/config.go index a4f7cd3c6..a0efbf316 100644 --- a/gateway/internal/gateway/config.go +++ b/gateway/internal/gateway/config.go @@ -53,6 +53,32 @@ func NewConfig() *Config { return &Config{} } +func (config *Config) LoadConfigFromMapForEnrollment(configMap map[interface{}]interface{}) error { + //enrollment config loading is more lax on where the CSR section lives (i.e. under edge: or at the root level) + + if val, ok := configMap["edge"]; ok && val != nil { + var edgeConfigMap map[interface{}]interface{} = nil + config.Enabled = true + if edgeConfigMap, ok = val.(map[interface{}]interface{}); !ok { + return fmt.Errorf("expected map as edge configuration") + } + + if err := config.loadCsr(edgeConfigMap, "edge"); err != nil { + return err + } + } else if val, ok := configMap["csr"]; ok && val != nil { + if err := config.loadCsr(configMap, ""); err != nil { + return err + } + } else { + return fmt.Errorf("expected enrollment CSR section") + } + + config.loadIdentity(configMap) + + return nil +} + func (config *Config) LoadConfigFromMap(configMap map[interface{}]interface{}) error { var err error config.Enabled = false @@ -72,7 +98,7 @@ func (config *Config) LoadConfigFromMap(configMap map[interface{}]interface{}) e return err } - if err = config.loadCsr(edgeConfigMap); err != nil { + if err = config.loadCsr(edgeConfigMap, "edge"); err != nil { return err } @@ -186,22 +212,26 @@ func (config *Config) loadListener(rootConfigMap map[interface{}]interface{}) er return nil } -func (config *Config) loadCsr(edgeConfigMap map[interface{}]interface{}) error { +func (config *Config) loadCsr(configMap map[interface{}]interface{}, pathPrefix string) error { config.Csr = Csr{} - if value, found := edgeConfigMap["csr"]; found { + if pathPrefix != "" { + pathPrefix = pathPrefix + "." + } + + if value, found := configMap["csr"]; found { submap := value.(map[interface{}]interface{}) if submap == nil { - return errors.New("required section [edge.csr] is not a map") + return fmt.Errorf("required section [%scsr] is not a map", pathPrefix) } if err := mapstructure.Decode(submap, &config.Csr); err != nil { - return fmt.Errorf("failed to load [edge.csr]: %s", err) + return fmt.Errorf("failed to load [%scsr]: %s", pathPrefix, err) } } else { - return errors.New("required section [edge.csr] not found") + return fmt.Errorf("required section [%scsr] not found", pathPrefix) } for _, uristr := range config.Csr.Sans.UriAddresses { diff --git a/specs/swagger.yml b/specs/swagger.yml index 41ffab42c..7a94da49e 100644 --- a/specs/swagger.yml +++ b/specs/swagger.yml @@ -2488,6 +2488,133 @@ paths: '409': $ref: '#/responses/cannotDeleteReferencedResourceResponse' + ################################################################### + # Transit Routers + ################################################################## + '/transit-routers': + get: + summary: List transit routers + description: | + Retrieves a list of transit router resources; supports filtering, sorting, and pagination. Requires admin access. + security: + - ztSession: [] + tags: + - Transit Router + operationId: listTransitRouters + parameters: + - $ref: '#/parameters/limit' + - $ref: '#/parameters/offset' + - $ref: '#/parameters/filter' + responses: + '200': + $ref: '#/responses/listTransitRouters' + '401': + $ref: '#/responses/unauthorizedResponse' + post: + summary: Create a transit router resource + description: Create a transit router resource. Requires admin access. + security: + - ztSession: [] + tags: + - Transit Router + operationId: createTransitRouter + parameters: + - name: Body + in: body + required: true + description: A transit router to create + schema: + $ref: '#/definitions/transitRouterCreate' + responses: + '200': + $ref: '#/responses/createResponse' + '400': + $ref: '#/responses/badRequestResponse' + '401': + $ref: '#/responses/unauthorizedResponse' + '/transit-routers/{id}': + parameters: + - $ref: '#/parameters/id' + get: + summary: Retrieves a single transit router + description: Retrieves a single transit router by id. Requires admin access. + security: + - ztSession: [] + tags: + - Transit Router + operationId: detailTransitRouter + responses: + '200': + $ref: '#/responses/detailTransitRouter' + '404': + $ref: '#/responses/notFoundResponse' + '401': + $ref: '#/responses/unauthorizedResponse' + put: + summary: Update all fields on a transit router + description: Update all fields on a transit router by id. Requires admin access. + security: + - ztSession: [] + tags: + - Transit Router + operationId: updateTransitRouter + parameters: + - name: Body + in: body + required: true + description: A transit router update object + schema: + $ref: '#/definitions/transitRouterUpdate' + responses: + '200': + $ref: '#/responses/updateResponse' + '400': + $ref: '#/responses/badRequestResponse' + '404': + $ref: '#/responses/notFoundResponse' + '401': + $ref: '#/responses/unauthorizedResponse' + patch: + summary: Update the supplied fields on a transit router + description: Update the supplied fields on a transit router. Requires admin access. + security: + - ztSession: [] + tags: + - Transit Router + operationId: patchTransitRouter + parameters: + - name: Body + in: body + required: true + description: A transit router patch object + schema: + $ref: '#/definitions/transitRouterPatch' + responses: + '200': + $ref: '#/responses/patchResponse' + '400': + $ref: '#/responses/badRequestResponse' + '404': + $ref: '#/responses/notFoundResponse' + '401': + $ref: '#/responses/unauthorizedResponse' + delete: + summary: Delete a transit router + description: Delete a transit router by id. Requires admin access. + security: + - ztSession: [] + tags: + - Transit Router + operationId: deleteTransitRouter + responses: + '200': + $ref: '#/responses/deleteResponse' + '400': + $ref: '#/responses/badRequestResponse' + '401': + $ref: '#/responses/unauthorizedResponse' + '409': + $ref: '#/responses/cannotDeleteReferencedResourceResponse' ####################################################################################################################### # # Parameters - Reusable parameters @@ -2962,6 +3089,17 @@ responses: description: Returns the document that represents the specification schema: $ref: '#/definitions/detailSpecBodyEnvelope' + ################################################################### + # Transit Routers + ################################################################## + listTransitRouters: + description: A list of specifications + schema: + $ref: '#/definitions/listTransitRoutersEnvelope' + detailTransitRouter: + description: A single transit router + schema: + $ref: '#/definitions/detailTransitRouterEnvelope' ####################################################################################################################### # # Definitions - In & Out Models Only @@ -3050,31 +3188,6 @@ definitions: $ref: '#/definitions/sdkInfo' configTypes: $ref: '#/definitions/configTypes' - authenticateUPDB: - type: object - description: A username and password pair - required: [username, password] - properties: - username: - $ref: '#/definitions/username' - password: - $ref: '#/definitions/password' - envInfo: - $ref: '#/definitions/envInfo' - sdkInfo: - $ref: '#/definitions/sdkInfo' - configTypes: - $ref: '#/definitions/configTypes' - authenticateCert: - type: object - description: A cert based authentication request. The certificate used by the client as its TLS client certificate will be interrogated for authentication - properties: - envInfo: - $ref: '#/definitions/envInfo' - sdkInfo: - $ref: '#/definitions/sdkInfo' - configTypes: - $ref: '#/definitions/configTypes' ################################################################### # Current Session ################################################################## @@ -4694,17 +4807,17 @@ definitions: tags: $ref: '#/definitions/tags' serviceId: - type: "string" + type: string service: $ref: '#/definitions/entityRef' routerId: - type: "string" + type: string router: $ref: '#/definitions/entityRef' binding: - type: "string" + type: string address: - type: "string" + type: string terminatorCreate: type: object required: @@ -4798,7 +4911,69 @@ definitions: tags: $ref: '#/definitions/tags' name: - type: "string" - + type: string specBodyDetail: type: string + ################################################################### + # Transit Routers + ################################################################## + listTransitRoutersEnvelope: + type: object + properties: + meta: + $ref: '#/definitions/meta' + data: + $ref: '#/definitions/transitRouterList' + detailTransitRouterEnvelope: + type: object + properties: + meta: + $ref: '#/definitions/meta' + data: + $ref: '#/definitions/transitRouterDetail' + transitRouterList: + type: array + items: + $ref: '#/definitions/transitRouterDetail' + transitRouterDetail: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + _links: + $ref: '#/definitions/links' + tags: + $ref: '#/definitions/tags' + name: + type: string + fingerprint: + type: string + isVerified: + type: boolean + isOnline: + type: boolean + transitRouterCreate: + type: object + required: + - name + properties: + name: + type: string + transitRouterUpdate: + type: object + required: + - name + properties: + name: + type: string + transitRouterPatch: + type: object + properties: + name: + type: string \ No newline at end of file diff --git a/tests/ats-transit.router.yml b/tests/ats-transit.router.yml new file mode 100644 index 000000000..469016dbb --- /dev/null +++ b/tests/ats-transit.router.yml @@ -0,0 +1,62 @@ +v: 2 + +# (required) The certificates, ca chains, and private key used to represent this routers identity +# +# If the 'edge" section is enabled and enrollment is run, the files in this section will be written to with the results. +# The "key" value can be provided through any of the support private key engines (file, etc.). +# +# If the 'key' value is a file and that file exists; it is expected to a pre-generated private key. It will be used for +# the enrollment process and it will back the certificates generated during enrollment. If the file does not exist, it +# will be generated. +# +# The 'cert', 'server_cert', and 'ca' files will be overwritten with the results of the enrollment process. +# +# When running the ziti-router the files/values in this section will be read and used. The 'cert' value will be used +# as the client certificate to connect to the controller. The 'server_cert' is used for all servers/listeners. The +# 'ca' value is used to populate an internally Trusted CA pool. +identity: + cert: testdata/transit-router/transit-router-client.cert.pem + server_cert: testdata/transit-router/transit-router-server.cert.pem + key: testdata/transit-router/transit-router.key.pem + ca: testdata/transit-router/transit-router-ca-chain.cert.pem + +#trace: +# path: 001.trace + +#profile: +# memory: +# path: 001.memprof +# cpu: +# path: 001.cpuprof + +ctrl: + endpoint: tls:127.0.0.1:6262 + + +csr: + country: US + province: NC + locality: Charlotte + organization: NetFoundry + organizationalUnit: Ziti + sans: + dns: + - "localhost" + - "test-network" + - "test-network.localhost" + - "ziti-dev-ingress01" + email: + - "admin@example.com" + ip: + - "127.0.0.1" + uri: + - "ziti://ziti-dev-gateway01/made/up/example" + +dialers: + - binding: udp + - binding: transport + +listeners: + - binding: transport + address: tls:0.0.0.0:7099 + diff --git a/tests/authenticate.go b/tests/authenticate.go index e887165d8..13250cac6 100644 --- a/tests/authenticate.go +++ b/tests/authenticate.go @@ -239,9 +239,9 @@ func (request *authenticatedRequests) requireCreateIdentity(name string, isAdmin request.testContext.setJsonValue(entityData, enrollments, "enrollment") entityJson := entityData.String() - httpCode, body := request.createEntityOfType("identities", entityJson) - request.testContext.req.Equal(http.StatusCreated, httpCode) - id := request.testContext.getEntityId(body) + resp := request.createEntityOfType("identities", entityJson) + request.testContext.req.Equal(http.StatusCreated, resp.StatusCode()) + id := request.testContext.getEntityId(resp.Body()) return id } @@ -263,9 +263,9 @@ func (request *authenticatedRequests) requireCreateIdentityWithUpdbEnrollment(na request.testContext.setJsonValue(entityData, enrollments, "enrollment") entityJson := entityData.String() - httpCode, body := request.createEntityOfType("identities", entityJson) - request.testContext.req.Equal(http.StatusCreated, httpCode) - id := request.testContext.getEntityId(body) + resp := request.createEntityOfType("identities", entityJson) + request.testContext.req.Equal(http.StatusCreated, resp.StatusCode()) + id := request.testContext.getEntityId(resp.Body()) request.testContext.completeUpdbEnrollment(id, password) return id, userAuth } @@ -283,9 +283,9 @@ func (request *authenticatedRequests) requireCreateIdentityOttEnrollment(name st request.testContext.setJsonValue(entityData, enrollments, "enrollment") entityJson := entityData.String() - httpCode, body := request.createEntityOfType("identities", entityJson) - request.testContext.req.Equal(http.StatusCreated, httpCode) - id := request.testContext.getEntityId(body) + resp := request.createEntityOfType("identities", entityJson) + request.testContext.req.Equal(http.StatusCreated, resp.StatusCode()) + id := request.testContext.getEntityId(resp.Body()) return id, request.testContext.completeOttEnrollment(id) } @@ -307,6 +307,12 @@ func (request *authenticatedRequests) requireNewEdgeRouter(roleAttributes ...str return edgeRouter } +func (request *authenticatedRequests) requireNewTransitRouter() *transitRouter { + transitRouter := newTestTransitRouter() + request.requireCreateEntity(transitRouter) + return transitRouter +} + func (request *authenticatedRequests) requireNewServicePolicy(policyType string, serviceRoles, identityRoles []string) *servicePolicy { policy := newServicePolicy(policyType, nil, serviceRoles, identityRoles) request.requireCreateEntity(policy) @@ -357,21 +363,21 @@ func (request *authenticatedRequests) requireNewIdentityWithOtt(isAdmin bool, ro } func (request *authenticatedRequests) requireCreateEntity(entity entity) string { - httpStatus, body := request.createEntity(entity) - request.testContext.req.Equal(http.StatusCreated, httpStatus) - id := request.testContext.getEntityId(body) + resp := request.createEntity(entity) + standardJsonResponseTests(resp, http.StatusCreated, request.testContext.testing) + id := request.testContext.getEntityId(resp.Body()) entity.setId(id) return id } func (request *authenticatedRequests) requireDeleteEntity(entity entity) { - httpStatus, _ := request.deleteEntityOfType(entity.getEntityType(), entity.getId()) - request.testContext.req.Equal(http.StatusOK, httpStatus) + resp := request.deleteEntityOfType(entity.getEntityType(), entity.getId()) + standardJsonResponseTests(resp, http.StatusOK, request.testContext.testing) } func (request *authenticatedRequests) requireUpdateEntity(entity entity) { - httpStatus, _ := request.updateEntity(entity) - request.testContext.req.Equal(http.StatusOK, httpStatus) + resp := request.updateEntity(entity) + standardJsonResponseTests(resp, http.StatusOK, request.testContext.testing) } func (request *authenticatedRequests) requireList(url string) []string { @@ -409,14 +415,14 @@ func (request *authenticatedRequests) requireRemoveAssociation(url string, ids . request.testContext.req.Equal(http.StatusOK, httpStatus) } -func (request *authenticatedRequests) createEntityOfType(entityType string, body string) (int, []byte) { +func (request *authenticatedRequests) createEntityOfType(entityType string, body string) *resty.Response { resp, err := request.newAuthenticatedRequest(). SetBody(body). Post("/" + entityType) request.testContext.req.NoError(err) request.testContext.logJson(resp.Body()) - return resp.StatusCode(), resp.Body() + return resp } type serviceConfig struct { @@ -490,24 +496,24 @@ func (request *authenticatedRequests) updateIdentityServiceConfigs(method string return resp.StatusCode(), resp.Body() } -func (request *authenticatedRequests) createEntity(entity entity) (int, []byte) { +func (request *authenticatedRequests) createEntity(entity entity) *resty.Response { return request.createEntityOfType(entity.getEntityType(), entity.toJson(true, request.testContext)) } -func (request *authenticatedRequests) deleteEntityOfType(entityType string, id string) (int, []byte) { +func (request *authenticatedRequests) deleteEntityOfType(entityType string, id string) *resty.Response { resp, err := request.newAuthenticatedRequest().Delete("/" + entityType + "/" + id) request.testContext.req.NoError(err) request.testContext.logJson(resp.Body()) - return resp.StatusCode(), resp.Body() + return resp } -func (request *authenticatedRequests) updateEntity(entity entity) (int, []byte) { +func (request *authenticatedRequests) updateEntity(entity entity) *resty.Response { return request.updateEntityOfType(entity.getId(), entity.getEntityType(), entity.toJson(false, request.testContext), false) } -func (request *authenticatedRequests) updateEntityOfType(id string, entityType string, body string, patch bool) (int, []byte) { +func (request *authenticatedRequests) updateEntityOfType(id string, entityType string, body string, patch bool) *resty.Response { if request.testContext.enabledJsonLogging { fmt.Printf("update body:\n%v\n", body) } @@ -528,7 +534,7 @@ func (request *authenticatedRequests) updateEntityOfType(id string, entityType s request.testContext.req.NoError(err) request.testContext.logJson(resp.Body()) - return resp.StatusCode(), resp.Body() + return resp } func (request *authenticatedRequests) query(url string) (int, []byte) { @@ -660,11 +666,11 @@ func (request *authenticatedRequests) requireCreateNewConfigType() *configType { } func (request *authenticatedRequests) requirePatchEntity(entity entity, fields ...string) { - httpStatus, _ := request.patchEntity(entity, fields...) - request.testContext.req.Equal(http.StatusOK, httpStatus) + resp := request.patchEntity(entity, fields...) + standardJsonResponseTests(resp, http.StatusOK, request.testContext.testing) } -func (request *authenticatedRequests) patchEntity(entity entity, fields ...string) (int, []byte) { +func (request *authenticatedRequests) patchEntity(entity entity, fields ...string) *resty.Response { return request.updateEntityOfType(entity.getId(), entity.getEntityType(), entity.toJson(false, request.testContext, fields...), true) } @@ -674,6 +680,12 @@ func (request *authenticatedRequests) getEdgeRouterJwt(edgeRouterId string) stri return data.Data().(string) } +func (request *authenticatedRequests) getTransitRouterJwt(transitRouterId string) string { + jsonBody := request.requireQuery("transit-routers/" + transitRouterId) + data := request.testContext.requirePath(jsonBody, "data", "enrollmentJwt") + return data.Data().(string) +} + func (request *authenticatedRequests) getIdentityJwt(identityId string) string { request.testContext.enabledJsonLogging = true jsonBody := request.requireQuery("identities/" + identityId) diff --git a/tests/config_test.go b/tests/config_test.go index 939e23a99..0a101784c 100644 --- a/tests/config_test.go +++ b/tests/config_test.go @@ -42,30 +42,30 @@ func Test_Configs(t *testing.T) { configType := ctx.AdminSession.requireCreateNewConfigType() config := ctx.newConfig(configType.id, map[string]interface{}{"port": 22}) config.name = "" - httpCode, body := ctx.AdminSession.createEntity(config) - ctx.requireFieldError(httpCode, body, apierror.CouldNotValidateCode, "name") + resp := ctx.AdminSession.createEntity(config) + ctx.requireFieldError(resp.StatusCode(), resp.Body(), apierror.CouldNotValidateCode, "name") }) t.Run("create without data should fail", func(t *testing.T) { ctx.testContextChanged(t) configType := ctx.AdminSession.requireCreateNewConfigType() config := ctx.newConfig(configType.id, nil) - httpCode, body := ctx.AdminSession.createEntity(config) - ctx.requireFieldError(httpCode, body, apierror.CouldNotValidateCode, "data") + resp := ctx.AdminSession.createEntity(config) + ctx.requireFieldError(resp.StatusCode(), resp.Body(), apierror.CouldNotValidateCode, "data") }) t.Run("create without type should fail", func(t *testing.T) { ctx.testContextChanged(t) config := ctx.newConfig("", map[string]interface{}{"port": 22}) - httpCode, body := ctx.AdminSession.createEntity(config) - ctx.requireFieldError(httpCode, body, apierror.CouldNotValidateCode, "type") + resp := ctx.AdminSession.createEntity(config) + ctx.requireFieldError(resp.StatusCode(), resp.Body(), apierror.CouldNotValidateCode, "type") }) t.Run("create with invalid config type should fail", func(t *testing.T) { ctx.testContextChanged(t) config := ctx.newConfig(uuid.New().String(), map[string]interface{}{"port": 22}) - httpCode, body := ctx.AdminSession.createEntity(config) - ctx.requireFieldError(httpCode, body, apierror.InvalidFieldCode, "type") + resp := ctx.AdminSession.createEntity(config) + ctx.requireFieldError(resp.StatusCode(), resp.Body(), apierror.InvalidFieldCode, "type") }) t.Run("create should pass", func(t *testing.T) { @@ -248,8 +248,8 @@ func Test_Configs(t *testing.T) { configType.id = ctx.AdminSession.requireCreateEntity(configType) config := ctx.newConfig(configType.id, map[string]interface{}{"port": 22}) - httpCode, body := ctx.AdminSession.createEntity(config) - ctx.requireFieldError(httpCode, body, apierror.CouldNotValidateCode, "(root)") + resp := ctx.AdminSession.createEntity(config) + ctx.requireFieldError(resp.StatusCode(), resp.Body(), apierror.CouldNotValidateCode, "(root)") now := time.Now() config = ctx.newConfig(configType.id, map[string]interface{}{ diff --git a/tests/config_type_test.go b/tests/config_type_test.go index 28bcda24e..6ac90b93f 100644 --- a/tests/config_type_test.go +++ b/tests/config_type_test.go @@ -41,8 +41,8 @@ func Test_ConfigTypes(t *testing.T) { ctx.testContextChanged(t) configType := ctx.newConfigType() configType.name = "" - httpCode, body := ctx.AdminSession.createEntity(configType) - ctx.requireFieldError(httpCode, body, apierror.CouldNotValidateCode, "name") + resp := ctx.AdminSession.createEntity(configType) + ctx.requireFieldError(resp.StatusCode(), resp.Body(), apierror.CouldNotValidateCode, "name") }) t.Run("create config type without schema should pass", func(t *testing.T) { @@ -74,8 +74,8 @@ func Test_ConfigTypes(t *testing.T) { }, }, } - httpCode, body := ctx.AdminSession.createEntity(configType) - ctx.requireFieldError(httpCode, body, apierror.InvalidFieldCode, "schema") + resp := ctx.AdminSession.createEntity(configType) + ctx.requireFieldError(resp.StatusCode(), resp.Body(), apierror.InvalidFieldCode, "schema") }) t.Run("create config type with schema should pass", func(t *testing.T) { diff --git a/tests/context.go b/tests/context.go index 3f65773f2..1d84d8fec 100644 --- a/tests/context.go +++ b/tests/context.go @@ -60,8 +60,9 @@ import ( ) const ( - ControllerConfFile = "ats-ctrl.yml" - EdgeRouterConfFile = "ats-edge.router.yml" + ControllerConfFile = "ats-ctrl.yml" + EdgeRouterConfFile = "ats-edge.router.yml" + TransitRouterConfFile = "ats-transit.router.yml" ) func init() { @@ -84,8 +85,10 @@ type TestContext struct { client *resty.Client enabledJsonLogging bool - edgeRouterEntity *edgeRouter - router *router.Router + edgeRouterEntity *edgeRouter + transitRouterEntity *transitRouter + router *router.Router + testing *testing.T } var defaultTestContext = &TestContext{ @@ -96,7 +99,7 @@ var defaultTestContext = &TestContext{ } func NewTestContext(t *testing.T) *TestContext { - return &TestContext{ + ret := &TestContext{ ApiHost: "127.0.0.1:1281", AdminAuthenticator: &updbAuthenticator{ Username: uuid.New().String(), @@ -104,6 +107,9 @@ func NewTestContext(t *testing.T) *TestContext { }, req: require.New(t), } + ret.testContextChanged(t) + + return ret } func GetTestContext() *TestContext { @@ -111,6 +117,7 @@ func GetTestContext() *TestContext { } func (ctx *TestContext) testContextChanged(t *testing.T) { + ctx.testing = t ctx.req = require.New(t) } @@ -238,6 +245,28 @@ func (ctx *TestContext) createAndEnrollEdgeRouter(roleAttributes ...string) *edg return ctx.edgeRouterEntity } +func (ctx *TestContext) createAndEnrollTransitRouter() *transitRouter { + // If a tx router has already been created, delete it and create a new one + if ctx.transitRouterEntity != nil { + ctx.AdminSession.requireDeleteEntity(ctx.transitRouterEntity) + ctx.transitRouterEntity = nil + } + + _ = os.MkdirAll("testdata/transit-router", os.FileMode(0755)) + + ctx.transitRouterEntity = ctx.AdminSession.requireNewTransitRouter() + jwt := ctx.AdminSession.getTransitRouterJwt(ctx.transitRouterEntity.id) + + cfgmap, err := router.LoadConfigMap(TransitRouterConfFile) + ctx.req.NoError(err) + + enroller := enroll.NewRestEnroller() + ctx.req.NoError(enroller.LoadConfig(cfgmap)) + ctx.req.NoError(enroller.Enroll([]byte(jwt), true, "")) + + return ctx.transitRouterEntity +} + func (ctx *TestContext) createEnrollAndStartEdgeRouter(roleAttributes ...string) { ctx.createAndEnrollEdgeRouter(roleAttributes...) diff --git a/tests/edge_router_test.go b/tests/edge_router_test.go index 7d505ae29..0ce6b3181 100644 --- a/tests/edge_router_test.go +++ b/tests/edge_router_test.go @@ -90,4 +90,12 @@ func Test_EdgeRouter(t *testing.T) { ctx.req.True(stringz.ContainsAll(list, role1, role2, role3, role4)) ctx.req.False(stringz.Contains(list, role5)) }) + + t.Run("newly created edge routers that is deleted", func(t *testing.T) { + ctx.testContextChanged(t) + + edgeRouter := ctx.AdminSession.requireNewEdgeRouter() + + ctx.AdminSession.requireDeleteEntity(edgeRouter) + }) } diff --git a/tests/entities.go b/tests/entities.go index b17e9c4aa..3e2fd5032 100644 --- a/tests/entities.go +++ b/tests/entities.go @@ -570,6 +570,45 @@ func (entity *configValidatingService) validate(ctx *TestContext, c *gabs.Contai } } +func newTestTransitRouter() *transitRouter { + return &transitRouter{ + name: uuid.New().String(), + } +} + +type transitRouter struct { + id string + name string + tags map[string]interface{} +} + +func (entity *transitRouter) getId() string { + return entity.id +} + +func (entity *transitRouter) setId(id string) { + entity.id = id +} + +func (entity *transitRouter) getEntityType() string { + return "transit-routers" +} + +func (entity *transitRouter) toJson(_ bool, ctx *TestContext, _ ...string) string { + entityData := gabs.New() + ctx.setJsonValue(entityData, entity.name, "name") + ctx.setJsonValue(entityData, entity.tags, "tags") + + return entityData.String() +} + +func (entity *transitRouter) validate(ctx *TestContext, c *gabs.Container) { + if entity.tags == nil { + entity.tags = map[string]interface{}{} + } + ctx.pathEquals(c, entity.name, path("name")) + ctx.pathEquals(c, entity.tags, path("tags")) +} type ca struct { id string name string `json:"name"` diff --git a/tests/service_test.go b/tests/service_test.go index 83ab6947c..d3e61004c 100644 --- a/tests/service_test.go +++ b/tests/service_test.go @@ -44,8 +44,8 @@ func Test_Services(t *testing.T) { ctx.testContextChanged(t) service := ctx.newService(nil, nil) service.name = "" - httpCode, body := ctx.AdminSession.createEntity(service) - ctx.requireFieldError(httpCode, body, apierror.CouldNotValidateCode, "name") + resp := ctx.AdminSession.createEntity(service) + ctx.requireFieldError(resp.StatusCode(), resp.Body(), apierror.CouldNotValidateCode, "name") }) t.Run("create should pass", func(t *testing.T) { @@ -353,8 +353,8 @@ func Test_ServiceListWithConfigDuplicate(t *testing.T) { }) service := ctx.newService(nil, s(config1.id, config2.id)) - httpCode, body := ctx.AdminSession.createEntity(service) - ctx.requireFieldError(httpCode, body, apierror.InvalidFieldCode, "configs") + resp := ctx.AdminSession.createEntity(service) + ctx.requireFieldError(resp.StatusCode(), resp.Body(), apierror.InvalidFieldCode, "configs") } func Test_ServiceRoleAttributes(t *testing.T) { diff --git a/tests/testdata/.gitignore b/tests/testdata/.gitignore index deeae7c95..6b221bf2f 100644 --- a/tests/testdata/.gitignore +++ b/tests/testdata/.gitignore @@ -1,2 +1,3 @@ ctrl.db* edge-router/ +transit-router/ \ No newline at end of file diff --git a/tests/transit_router_test.go b/tests/transit_router_test.go new file mode 100644 index 000000000..b7d823494 --- /dev/null +++ b/tests/transit_router_test.go @@ -0,0 +1,47 @@ +// +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 ( + "testing" +) + +func Test_TransitRouters(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.teardown() + ctx.startServer() + ctx.requireAdminLogin() + + t.Run("transit routers can be created and enrolled", func(t *testing.T) { + ctx.testContextChanged(t) + ctx.createAndEnrollTransitRouter() + }) + + t.Run("create transit router, then delete", func(t *testing.T) { + ctx.testContextChanged(t) + router := ctx.AdminSession.requireNewTransitRouter() + ctx.AdminSession.requireDeleteEntity(router) + }) + + t.Run("create & enroll transit router, then delete", func(t *testing.T) { + ctx.testContextChanged(t) + router := ctx.createAndEnrollTransitRouter() + ctx.AdminSession.requireDeleteEntity(router) + }) +}