From 7e2b97c4255a9bd1b75c563ad221de573cac5182 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Thu, 12 Aug 2021 14:58:28 -0400 Subject: [PATCH 1/6] fixes posture check timeouts, tests, service updates - fixes timeout calculation bugs - adds timeout calculation unit/api tests - adds posture response service events - fixes 0 to -1 values for timeout settings - fixes abnormally high service updates --- controller/handler_edge_ctrl/common_tunnel.go | 1 + .../routes/posture_response_responder.go | 42 +++ .../routes/posture_response_router.go | 59 +++- .../internal/routes/session_api_model.go | 4 +- controller/internal/routes/session_router.go | 2 +- controller/model/posture_check_model.go | 2 + controller/model/posture_check_model_mfa.go | 36 ++- .../model/posture_check_model_mfa_test.go | 79 +++-- controller/model/posture_check_type_model.go | 6 +- controller/model/posture_response_handlers.go | 87 +++++ controller/model/posture_response_model.go | 305 +++++++++--------- controller/model/service_handlers.go | 2 + controller/model/session_handlers.go | 193 ++++++----- controller/model/session_model.go | 4 + .../persistence/migration_initialize.go | 2 +- controller/persistence/migration_v23.go | 2 +- controller/persistence/posture_check_mfa.go | 23 +- controller/persistence/posture_check_store.go | 2 + .../persistence/posture_check_type_store.go | 26 +- controller/persistence/session_store.go | 4 + .../create_posture_response_bulk_responses.go | 8 +- .../create_posture_response_responses.go | 8 +- rest_client_api_server/embedded_spec.go | 140 +++++++- .../create_posture_response_bulk_responses.go | 8 +- .../create_posture_response_responses.go | 8 +- rest_management_api_server/embedded_spec.go | 12 +- rest_model/posture_response.go | 139 ++++++++ rest_model/posture_response_envelope.go | 171 ++++++++++ rest_model/posture_response_service.go | 163 ++++++++++ rest_model/session_detail.go | 25 ++ specs/client.yml | 50 ++- specs/management.yml | 3 + specs/source/client/posture-response.yml | 46 ++- specs/source/shared/sessions.yml | 3 + tests/posture_check_mfa_test.go | 43 +++ 35 files changed, 1375 insertions(+), 333 deletions(-) create mode 100644 controller/internal/routes/posture_response_responder.go create mode 100644 rest_model/posture_response.go create mode 100644 rest_model/posture_response_envelope.go create mode 100644 rest_model/posture_response_service.go diff --git a/controller/handler_edge_ctrl/common_tunnel.go b/controller/handler_edge_ctrl/common_tunnel.go index a06621546..79f3e68b8 100644 --- a/controller/handler_edge_ctrl/common_tunnel.go +++ b/controller/handler_edge_ctrl/common_tunnel.go @@ -180,6 +180,7 @@ func (self *baseTunnelRequestContext) ensureSessionForService(sessionId, session Token: uuid.NewString(), ApiSessionId: self.apiSession.Id, ServiceId: self.service.Id, + IdentityId: self.identity.Id, Type: sessionType, } diff --git a/controller/internal/routes/posture_response_responder.go b/controller/internal/routes/posture_response_responder.go new file mode 100644 index 000000000..c088f1a1d --- /dev/null +++ b/controller/internal/routes/posture_response_responder.go @@ -0,0 +1,42 @@ +/* + 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/openziti/edge/controller/env" + "github.com/openziti/edge/controller/response" + "github.com/openziti/edge/rest_model" + "net/http" +) + +type PostureResponseResponder struct { + response.Responder + ae *env.AppEnv + services []*rest_model.PostureResponseService +} + +func (responder *PostureResponseResponder) RespondWithCreatedId(id string, _ rest_model.Link) { + data := &rest_model.PostureResponse{ + Services: responder.services, + } + newSessionEnvelope := &rest_model.PostureResponseEnvelope{ + Data: data, + Meta: &rest_model.Meta{}, + } + + responder.Respond(newSessionEnvelope, http.StatusCreated) +} diff --git a/controller/internal/routes/posture_response_router.go b/controller/internal/routes/posture_response_router.go index 44fbdb415..6036c2235 100644 --- a/controller/internal/routes/posture_response_router.go +++ b/controller/internal/routes/posture_response_router.go @@ -53,19 +53,66 @@ func (r *PostureResponseRouter) Register(ae *env.AppEnv) { } func (r *PostureResponseRouter) Create(ae *env.AppEnv, rc *response.RequestContext, params posture_checks.CreatePostureResponseParams) { - Create(rc, rc, PostureResponseLinkFactory, func() (string, error) { - apiPostureResponse := params.PostureResponse - r.handlePostureResponse(ae, rc, apiPostureResponse) - return "", nil - }) + bulkParams := posture_checks.CreatePostureResponseBulkParams{ + HTTPRequest: params.HTTPRequest, + PostureResponse: []rest_model.PostureResponseCreate{params.PostureResponse}, + } + r.CreateBulk(ae, rc, bulkParams) } func (r *PostureResponseRouter) CreateBulk(ae *env.AppEnv, rc *response.RequestContext, params posture_checks.CreatePostureResponseBulkParams) { + responder := &PostureResponseResponder{ + Responder: rc, + ae: ae, + services: []*rest_model.PostureResponseService{}, + } + + onWake := false + onUnlock := false + for _, apiPostureResponse := range params.PostureResponse { + if state, ok := apiPostureResponse.(*rest_model.PostureResponseEndpointStateCreate); ok { + if state.Woken { + onWake = true + } + + if state.Unlocked { + onUnlock = true + } + } r.handlePostureResponse(ae, rc, apiPostureResponse) } - Create(rc, rc, PostureResponseLinkFactory, func() (string, error) { + if onWake || onUnlock { + mfaType := string(rest_model.PostureCheckTypeMFA) + gracePeriod := model.MfaPromptGracePeriod * -1 + gracePeriodSeconds := int64(gracePeriod.Seconds()) + + postureData := ae.Handlers.PostureResponse.PostureData(rc.Identity.Id) + + if postureData != nil && postureData.ApiSessions != nil { + if apiPostureData, ok := postureData.ApiSessions[rc.ApiSession.Id]; ok && apiPostureData.Mfa.PassedMfaAt != nil { + //if the last time Mfa was passed at is outside of the grace period, send timeout update + durationSinceLastMfa := time.Now().Sub(*apiPostureData.Mfa.PassedMfaAt) + + modelServicesWithTimeouts := ae.Handlers.PostureResponse.GetEndpointStateChangeAffectedServices(durationSinceLastMfa, gracePeriod, onWake, onUnlock) + + for _, modelServiceWithTimeout := range modelServicesWithTimeouts { + + responder.services = append(responder.services, &rest_model.PostureResponseService{ + ID: &modelServiceWithTimeout.Service.Id, + Name: &modelServiceWithTimeout.Service.Name, + PostureQueryType: &mfaType, + TimeoutRemaining: &gracePeriodSeconds, + Timeout: &modelServiceWithTimeout.Timeout, + }) + } + + } + } + } + + CreateWithResponder(rc, responder, PostureResponseLinkFactory, func() (string, error) { return "", nil }) } diff --git a/controller/internal/routes/session_api_model.go b/controller/internal/routes/session_api_model.go index 6df508032..1435a83ed 100644 --- a/controller/internal/routes/session_api_model.go +++ b/controller/internal/routes/session_api_model.go @@ -48,7 +48,7 @@ func (factory *SessionLinkFactoryImpl) Links(entity models.Entity) rest_model.Li return links } -func MapCreateSessionToModel(apiSessionId string, session *rest_model.SessionCreate) *model.Session { +func MapCreateSessionToModel(identityId, apiSessionId string, session *rest_model.SessionCreate) *model.Session { ret := &model.Session{ BaseEntity: models.BaseEntity{ Tags: TagsOrDefault(session.Tags), @@ -56,6 +56,7 @@ func MapCreateSessionToModel(apiSessionId string, session *rest_model.SessionCre Token: uuid.New().String(), ApiSessionId: apiSessionId, ServiceId: session.ServiceID, + IdentityId: identityId, Type: string(session.Type), SessionCerts: nil, ServicePolicies: nil, @@ -126,6 +127,7 @@ func MapSessionToRestModel(ae *env.AppEnv, sessionModel *model.Session) (*rest_m APISessionID: &apiSession.Id, Service: ToEntityRef(service.Name, service, ServiceLinkFactory), ServiceID: &service.Id, + IdentityID: &sessionModel.IdentityId, EdgeRouters: edgeRouters, Type: &dialBindType, Token: &sessionModel.Token, diff --git a/controller/internal/routes/session_router.go b/controller/internal/routes/session_router.go index cd8a1e254..6f7b97815 100644 --- a/controller/internal/routes/session_router.go +++ b/controller/internal/routes/session_router.go @@ -126,7 +126,7 @@ func (r *SessionRouter) Create(ae *env.AppEnv, rc *response.RequestContext, para start := time.Now() responder := &SessionRequestResponder{ae: ae, Responder: rc} CreateWithResponder(rc, responder, SessionLinkFactory, func() (string, error) { - return ae.Handlers.Session.Create(MapCreateSessionToModel(rc.ApiSession.Id, params.Session)) + return ae.Handlers.Session.Create(MapCreateSessionToModel(rc.Identity.Id, rc.ApiSession.Id, params.Session)) }) r.createTimer.UpdateSince(start) } diff --git a/controller/model/posture_check_model.go b/controller/model/posture_check_model.go index 32a06240f..4735283e6 100644 --- a/controller/model/posture_check_model.go +++ b/controller/model/posture_check_model.go @@ -131,6 +131,8 @@ func (entity *PostureCheck) toBoltEntityForPatch(tx *bbolt.Tx, handler Handler, func (entity *PostureCheck) Evaluate(apiSessionId string, pd *PostureData) (bool, *PostureCheckFailure) { if !entity.SubType.Evaluate(apiSessionId, pd) { + + return false, &PostureCheckFailure{ PostureCheckId: entity.Id, PostureCheckName: entity.Name, diff --git a/controller/model/posture_check_model_mfa.go b/controller/model/posture_check_model_mfa.go index 49193297d..df6643c63 100644 --- a/controller/model/posture_check_model_mfa.go +++ b/controller/model/posture_check_model_mfa.go @@ -66,6 +66,10 @@ func (p *PostureCheckMfa) GetTimeoutSeconds() int64 { } func (p *PostureCheckMfa) GetTimeoutRemainingSeconds(apiSessionId string, pd *PostureData) int64 { + return p.getTimeoutRemainingAtSeconds(apiSessionId, pd, time.Now()) +} + +func (p *PostureCheckMfa) getTimeoutRemainingAtSeconds(apiSessionId string, pd *PostureData, now time.Time) int64 { if p.TimeoutSeconds == PostureCheckNoTimeout { return PostureCheckNoTimeout } @@ -82,7 +86,7 @@ func (p *PostureCheckMfa) GetTimeoutRemainingSeconds(apiSessionId string, pd *Po return PostureCheckNoTimeout } - timeSinceLastMfa := time.Now().Sub(*apiSessionData.Mfa.PassedMfaAt) + timeSinceLastMfa := now.Sub(*apiSessionData.Mfa.PassedMfaAt) timeoutRemaining := p.TimeoutSeconds - int64(timeSinceLastMfa.Seconds()) if timeoutRemaining <= 0 { @@ -90,7 +94,7 @@ func (p *PostureCheckMfa) GetTimeoutRemainingSeconds(apiSessionId string, pd *Po } if p.PromptOnWake && apiSessionData.EndpointState != nil && apiSessionData.EndpointState.WokenAt != nil { - onWakeTimeOut := calculateTimeout(time.Now(), *apiSessionData.EndpointState.WokenAt, -1 * MfaPromptGracePeriod) + onWakeTimeOut := calculateTimeout(now, *apiSessionData.EndpointState.WokenAt, -1*MfaPromptGracePeriod) if onWakeTimeOut < timeoutRemaining { timeoutRemaining = onWakeTimeOut @@ -98,7 +102,7 @@ func (p *PostureCheckMfa) GetTimeoutRemainingSeconds(apiSessionId string, pd *Po } if p.PromptOnUnlock && apiSessionData.EndpointState != nil && apiSessionData.EndpointState.UnlockedAt != nil { - onUnlockTimeout := calculateTimeout(time.Now(), *apiSessionData.EndpointState.UnlockedAt, -1 * MfaPromptGracePeriod) + onUnlockTimeout := calculateTimeout(now, *apiSessionData.EndpointState.UnlockedAt, -1*MfaPromptGracePeriod) if onUnlockTimeout < timeoutRemaining { timeoutRemaining = onUnlockTimeout @@ -143,14 +147,19 @@ func (p *PostureCheckMfa) FailureValues(apiSessionId string, pd *PostureData) Po ret.ActualValue.TimedOutSeconds = apiSessionData.Mfa.TimedOut ret.ActualValue.PassedMfa = apiSessionData.Mfa.PassedMfaAt != nil } - ret.ActualValue.PassedOnUnlock = p.PassedOnUnlock(apiSessionData) - ret.ActualValue.PassedOnWake = p.PassedOnWake(apiSessionData) + now := time.Now().UTC() + ret.ActualValue.PassedOnUnlock = p.PassedOnUnlock(apiSessionData, now) + ret.ActualValue.PassedOnWake = p.PassedOnWake(apiSessionData, now) } return ret } func (p *PostureCheckMfa) Evaluate(apiSessionId string, pd *PostureData) bool { + return p.evaluateAt(apiSessionId, pd, time.Now()) +} + +func (p *PostureCheckMfa) evaluateAt(apiSessionId string, pd *PostureData, now time.Time) bool { apiSessionData := pd.ApiSessions[apiSessionId] if apiSessionData == nil { @@ -164,11 +173,9 @@ func (p *PostureCheckMfa) Evaluate(apiSessionId string, pd *PostureData) bool { if apiSessionData.Mfa.PassedMfaAt == nil { return false } - now := time.Now().UTC() - timeoutRemainingSeconds := p.GetTimeoutRemainingSeconds(apiSessionId, pd) - if timeoutRemainingSeconds != PostureCheckNoTimeout { - expiresAt := apiSessionData.Mfa.PassedMfaAt.Add(time.Duration(timeoutRemainingSeconds) * time.Second) + if p.TimeoutSeconds != PostureCheckNoTimeout { + expiresAt := apiSessionData.Mfa.PassedMfaAt.Add(time.Duration(p.TimeoutSeconds) * time.Second) if expiresAt.Before(now) { apiSessionData.Mfa.TimedOut = true } @@ -178,18 +185,18 @@ func (p *PostureCheckMfa) Evaluate(apiSessionId string, pd *PostureData) bool { return false } - if !p.PassedOnWake(apiSessionData) { + if !p.PassedOnWake(apiSessionData, now) { return false } - if !p.PassedOnUnlock(apiSessionData) { + if !p.PassedOnUnlock(apiSessionData, now) { return false } return true } -func (p *PostureCheckMfa) PassedOnWake(apiSessionData *ApiSessionPostureData) bool { +func (p *PostureCheckMfa) PassedOnWake(apiSessionData *ApiSessionPostureData, now time.Time) bool { if !p.PromptOnWake { return true } @@ -201,7 +208,7 @@ func (p *PostureCheckMfa) PassedOnWake(apiSessionData *ApiSessionPostureData) bo if apiSessionData.Mfa == nil || apiSessionData.Mfa.PassedMfaAt == nil { return false } - now := time.Now().UTC() + if apiSessionData.EndpointState == nil || apiSessionData.EndpointState.WokenAt == nil || apiSessionData.EndpointState.WokenAt.Before(*apiSessionData.Mfa.PassedMfaAt) || now.Add(MfaPromptGracePeriod).Before(*apiSessionData.EndpointState.WokenAt) { return true } @@ -209,7 +216,7 @@ func (p *PostureCheckMfa) PassedOnWake(apiSessionData *ApiSessionPostureData) bo return false } -func (p *PostureCheckMfa) PassedOnUnlock(apiSessionData *ApiSessionPostureData) bool { +func (p *PostureCheckMfa) PassedOnUnlock(apiSessionData *ApiSessionPostureData, now time.Time) bool { if !p.PromptOnUnlock { return true } @@ -222,7 +229,6 @@ func (p *PostureCheckMfa) PassedOnUnlock(apiSessionData *ApiSessionPostureData) return false } - now := time.Now().UTC() if apiSessionData.EndpointState == nil || apiSessionData.EndpointState.UnlockedAt == nil || apiSessionData.EndpointState.UnlockedAt.Before(*apiSessionData.Mfa.PassedMfaAt) || now.Add(MfaPromptGracePeriod).Before(*apiSessionData.EndpointState.UnlockedAt) { return true } diff --git a/controller/model/posture_check_model_mfa_test.go b/controller/model/posture_check_model_mfa_test.go index 9a2f64f7c..da1f13f94 100644 --- a/controller/model/posture_check_model_mfa_test.go +++ b/controller/model/posture_check_model_mfa_test.go @@ -28,6 +28,45 @@ const ( ) func TestPostureCheckModelMfa(t *testing.T) { + t.Run("evaluateAt", func(t *testing.T) { + mfaCheck, postureData := newMfaCheckAndPostureData() + + mfaCheck.TimeoutSeconds = 40 + + t.Run("passes at 3/4 timeout", func(t *testing.T) { + threeFourthsTimeout := 10 + + now := postureData.ApiSessions[mfaTestApiSessionId].Mfa.PassedMfaAt.Add(time.Duration(threeFourthsTimeout) * time.Second) + + result := mfaCheck.evaluateAt(mfaTestApiSessionId, postureData, now) + + req := require.New(t) + req.True(result) + }) + + t.Run("passes at 1/2 timeout", func(t *testing.T) { + halfTimeout := 20 + + now := postureData.ApiSessions[mfaTestApiSessionId].Mfa.PassedMfaAt.Add(time.Duration(halfTimeout) * time.Second) + + result := mfaCheck.evaluateAt(mfaTestApiSessionId, postureData, now) + + req := require.New(t) + req.True(result) + }) + + t.Run("passes at 1/4 timeout", func(t *testing.T) { + quarterTimeout := 30 + + now := postureData.ApiSessions[mfaTestApiSessionId].Mfa.PassedMfaAt.Add(time.Duration(quarterTimeout) * time.Second) + + result := mfaCheck.evaluateAt(mfaTestApiSessionId, postureData, now) + + req := require.New(t) + req.True(result) + }) + }) + t.Run("Evaluate", func(t *testing.T) { t.Run("returns true for valid MFA check", func(t *testing.T) { mfaCheck, postureData := newMfaCheckAndPostureData() @@ -212,7 +251,7 @@ func TestPostureCheckModelMfa(t *testing.T) { t.Run("returns true with default case", func(t *testing.T) { mfaCheck, postureData := newMfaCheckAndPostureData() - result := mfaCheck.PassedOnWake(postureData.ApiSessions[mfaTestApiSessionId]) + result := mfaCheck.PassedOnWake(postureData.ApiSessions[mfaTestApiSessionId], time.Now().UTC()) req := require.New(t) req.True(result) @@ -223,7 +262,7 @@ func TestPostureCheckModelMfa(t *testing.T) { mfaCheck.PromptOnWake = false - result := mfaCheck.PassedOnWake(postureData.ApiSessions[mfaTestApiSessionId]) + result := mfaCheck.PassedOnWake(postureData.ApiSessions[mfaTestApiSessionId], time.Now().UTC()) req := require.New(t) req.True(result) @@ -232,7 +271,7 @@ func TestPostureCheckModelMfa(t *testing.T) { t.Run("returns false when PromptOnWake is true and ApiSessionData is nil", func(t *testing.T) { mfaCheck, _ := newMfaCheckAndPostureData() - result := mfaCheck.PassedOnWake(nil) + result := mfaCheck.PassedOnWake(nil, time.Now().UTC()) req := require.New(t) req.False(result) @@ -244,7 +283,7 @@ func TestPostureCheckModelMfa(t *testing.T) { apiSessionData.Mfa = nil - result := mfaCheck.PassedOnWake(apiSessionData) + result := mfaCheck.PassedOnWake(apiSessionData, time.Now().UTC()) req := require.New(t) req.False(result) @@ -256,7 +295,7 @@ func TestPostureCheckModelMfa(t *testing.T) { apiSessionData.Mfa.PassedMfaAt = nil - result := mfaCheck.PassedOnWake(apiSessionData) + result := mfaCheck.PassedOnWake(apiSessionData, time.Now().UTC()) req := require.New(t) req.False(result) @@ -268,7 +307,7 @@ func TestPostureCheckModelMfa(t *testing.T) { apiSessionData.EndpointState = nil - result := mfaCheck.PassedOnWake(apiSessionData) + result := mfaCheck.PassedOnWake(apiSessionData, time.Now().UTC()) req := require.New(t) req.True(result) @@ -280,7 +319,7 @@ func TestPostureCheckModelMfa(t *testing.T) { apiSessionData.EndpointState.WokenAt = nil - result := mfaCheck.PassedOnWake(apiSessionData) + result := mfaCheck.PassedOnWake(apiSessionData, time.Now().UTC()) req := require.New(t) req.True(result) @@ -292,7 +331,7 @@ func TestPostureCheckModelMfa(t *testing.T) { t.Run("returns true with default case", func(t *testing.T) { mfaCheck, postureData := newMfaCheckAndPostureData() - result := mfaCheck.PassedOnUnlock(postureData.ApiSessions[mfaTestApiSessionId]) + result := mfaCheck.PassedOnUnlock(postureData.ApiSessions[mfaTestApiSessionId], time.Now().UTC()) req := require.New(t) req.True(result) @@ -303,7 +342,7 @@ func TestPostureCheckModelMfa(t *testing.T) { mfaCheck.PromptOnUnlock = false - result := mfaCheck.PassedOnUnlock(postureData.ApiSessions[mfaTestApiSessionId]) + result := mfaCheck.PassedOnUnlock(postureData.ApiSessions[mfaTestApiSessionId], time.Now().UTC()) req := require.New(t) req.True(result) @@ -312,7 +351,7 @@ func TestPostureCheckModelMfa(t *testing.T) { t.Run("returns false when PromptOnUnlock is true and ApiSessionData is nil", func(t *testing.T) { mfaCheck, _ := newMfaCheckAndPostureData() - result := mfaCheck.PassedOnUnlock(nil) + result := mfaCheck.PassedOnUnlock(nil, time.Now().UTC()) req := require.New(t) req.False(result) @@ -324,7 +363,7 @@ func TestPostureCheckModelMfa(t *testing.T) { apiSessionData.Mfa = nil - result := mfaCheck.PassedOnUnlock(apiSessionData) + result := mfaCheck.PassedOnUnlock(apiSessionData, time.Now().UTC()) req := require.New(t) req.False(result) @@ -336,7 +375,7 @@ func TestPostureCheckModelMfa(t *testing.T) { apiSessionData.Mfa.PassedMfaAt = nil - result := mfaCheck.PassedOnUnlock(apiSessionData) + result := mfaCheck.PassedOnUnlock(apiSessionData, time.Now().UTC()) req := require.New(t) req.False(result) @@ -348,7 +387,7 @@ func TestPostureCheckModelMfa(t *testing.T) { apiSessionData.EndpointState = nil - result := mfaCheck.PassedOnUnlock(apiSessionData) + result := mfaCheck.PassedOnUnlock(apiSessionData, time.Now().UTC()) req := require.New(t) req.True(result) @@ -360,7 +399,7 @@ func TestPostureCheckModelMfa(t *testing.T) { apiSessionData.EndpointState.UnlockedAt = nil - result := mfaCheck.PassedOnUnlock(apiSessionData) + result := mfaCheck.PassedOnUnlock(apiSessionData, time.Now().UTC()) req := require.New(t) req.True(result) @@ -552,9 +591,9 @@ func TestPostureCheckModelMfa(t *testing.T) { req.GreaterOrEqual(mfaCheck.GetTimeoutRemainingSeconds(mfaTestApiSessionId, postureData), mfaCheck.TimeoutSeconds-5) }) }) - + t.Run("calculateTimeout", func(t *testing.T) { - + t.Run("eventAt == now, results in 5m", func(t *testing.T) { req := require.New(t) @@ -564,7 +603,7 @@ func TestPostureCheckModelMfa(t *testing.T) { eventAt, err := time.Parse(time.RFC3339, "2020-01-01T11:30:00Z") req.NoError(err) - result := calculateTimeout(now, eventAt, -1 * MfaPromptGracePeriod) + result := calculateTimeout(now, eventAt, -1*MfaPromptGracePeriod) req.Equal(int64(300), result) }) @@ -578,7 +617,7 @@ func TestPostureCheckModelMfa(t *testing.T) { eventAt, err := time.Parse(time.RFC3339, "2020-01-01T11:27:00Z") req.NoError(err) - result := calculateTimeout(now, eventAt, -1 * MfaPromptGracePeriod) + result := calculateTimeout(now, eventAt, -1*MfaPromptGracePeriod) req.Equal(int64(120), result) }) @@ -592,7 +631,7 @@ func TestPostureCheckModelMfa(t *testing.T) { eventAt, err := time.Parse(time.RFC3339, "2020-01-01T11:25:00Z") req.NoError(err) - result := calculateTimeout(now, eventAt, -1 * MfaPromptGracePeriod) + result := calculateTimeout(now, eventAt, -1*MfaPromptGracePeriod) req.Equal(int64(0), result) }) @@ -606,7 +645,7 @@ func TestPostureCheckModelMfa(t *testing.T) { eventAt, err := time.Parse(time.RFC3339, "2020-01-01T11:20:00Z") req.NoError(err) - result := calculateTimeout(now, eventAt, -1 * MfaPromptGracePeriod) + result := calculateTimeout(now, eventAt, -1*MfaPromptGracePeriod) req.Equal(int64(0), result) }) diff --git a/controller/model/posture_check_type_model.go b/controller/model/posture_check_type_model.go index 963797628..0fd24f6d2 100644 --- a/controller/model/posture_check_type_model.go +++ b/controller/model/posture_check_type_model.go @@ -41,7 +41,7 @@ func (entity *PostureCheckType) toBoltEntity() (boltz.Entity, error) { }) } - return &persistence.PostureCheckType{ + return &persistence.PostureCheckOs{ Name: entity.Name, OperatingSystems: operatingSystems, BaseExtEntity: *boltz.NewExtEntity(entity.Id, entity.Tags), @@ -61,9 +61,9 @@ func (entity *PostureCheckType) toBoltEntityForPatch(*bbolt.Tx, Handler, boltz.F } func (entity *PostureCheckType) fillFrom(_ Handler, _ *bbolt.Tx, boltEntity boltz.Entity) error { - boltPostureCheckType, ok := boltEntity.(*persistence.PostureCheckType) + boltPostureCheckType, ok := boltEntity.(*persistence.PostureCheckOs) if !ok { - return errors.Errorf("unexpected type %v when filling model PostureCheckType", reflect.TypeOf(boltEntity)) + return errors.Errorf("unexpected type %v when filling model PostureCheckOs", reflect.TypeOf(boltEntity)) } var operatingSystems []OperatingSystem diff --git a/controller/model/posture_response_handlers.go b/controller/model/posture_response_handlers.go index a64ddefe9..9bc821900 100644 --- a/controller/model/posture_response_handlers.go +++ b/controller/model/posture_response_handlers.go @@ -19,6 +19,9 @@ package model import ( "fmt" "github.com/michaelquigley/pfxlog" + "github.com/openziti/edge/controller/persistence" + "github.com/openziti/fabric/controller/db" + "github.com/openziti/foundation/storage/ast" "go.etcd.io/bbolt" "runtime/debug" "time" @@ -247,3 +250,87 @@ func (handler *PostureResponseHandler) SetSdkInfo(identityId, apiSessionId strin return postureData }) } + +type ServiceWithTimeout struct { + Service *Service + Timeout int64 +} + +func (handler *PostureResponseHandler) GetEndpointStateChangeAffectedServices(timeSinceLastMfa, gracePeriod time.Duration, onWake bool, onUnlock bool) []*ServiceWithTimeout { + affectedChecks := map[string]int64{} //check id -> timeout + if onWake || onUnlock { + queryStr := fmt.Sprintf("%s=true or %s=true", persistence.FieldPostureCheckMfaPromptOnUnlock, persistence.FieldPostureCheckMfaPromptOnWake) + query, err := ast.Parse(handler.env.GetStores().PostureCheck, queryStr) + if err != nil { + pfxlog.Logger().Errorf("error querying for onWake/onUnlock posture checks: %v", err) + } else { + handler.env.GetDbProvider().GetDb().View(func(tx *bbolt.Tx) error { + cursor := handler.env.GetStores().PostureCheck.IterateIds(tx, query) + + for cursor.IsValid() { + if check, err := handler.env.GetStores().PostureCheck.LoadOneById(tx, string(cursor.Current())); err == nil { + if mfaCheck, ok := check.SubType.(*persistence.PostureCheckMfa); ok { + if (mfaCheck.PromptOnUnlock && onUnlock) || (mfaCheck.PromptOnWake && onWake) { + //no time out the remaining time was bigger than the grace period + timeSinceLastMfaSeconds := int64(timeSinceLastMfa.Seconds()) + gracePeriodSeconds := int64(gracePeriod.Seconds()) + + if mfaCheck.TimeoutSeconds == -1 || (mfaCheck.TimeoutSeconds-timeSinceLastMfaSeconds > gracePeriodSeconds) { + affectedChecks[check.Id] = mfaCheck.TimeoutSeconds + } + } + } + } else { + pfxlog.Logger().Errorf("error querying for onWake/onUnlock posture checks by id: %v", err) + } + + cursor.Next() + } + return nil + }) + } + } + + services := map[string]*ServiceWithTimeout{} + + if len(affectedChecks) > 0 { + _ = handler.env.GetDbProvider().GetDb().View(func(tx *bbolt.Tx) error { + for checkId, timeout := range affectedChecks { + policyCursor := handler.env.GetStores().PostureCheck.GetRelatedEntitiesCursor(tx, checkId, persistence.EntityTypeServicePolicies, true) + + for policyCursor.IsValid() { + serviceCursor := handler.env.GetStores().ServicePolicy.GetRelatedEntitiesCursor(tx, string(policyCursor.Current()), db.EntityTypeServices, true) + + for serviceCursor.IsValid() { + if _, ok := services[string(serviceCursor.Current())]; !ok { + service, err := handler.env.GetStores().EdgeService.LoadOneById(tx, string(serviceCursor.Current())) + if err == nil { + modelService := &Service{} + if err := modelService.fillFrom(handler.env.GetHandlers().EdgeService, tx, service); err == nil { + //use the lowest configured timeout (which is some timeout or no timeout) + if existingService, ok := services[service.Id]; !ok || timeout < existingService.Timeout { + services[service.Id] = &ServiceWithTimeout{ + Service: modelService, + Timeout: timeout, + } + } + } + } + } + serviceCursor.Next() + } + + policyCursor.Next() + } + } + + return nil + }) + } + + var serviceSlice []*ServiceWithTimeout + for _, service := range services { + serviceSlice = append(serviceSlice, service) + } + return serviceSlice +} diff --git a/controller/model/posture_response_model.go b/controller/model/posture_response_model.go index 77c69fdd1..402414733 100644 --- a/controller/model/posture_response_model.go +++ b/controller/model/posture_response_model.go @@ -17,11 +17,14 @@ package model import ( + "bytes" "github.com/jinzhu/copier" "github.com/kataras/go-events" "github.com/michaelquigley/pfxlog" "github.com/openziti/edge/controller/persistence" + "github.com/openziti/foundation/storage/ast" "github.com/openziti/foundation/storage/boltz" + "github.com/openziti/foundation/util/concurrenz" cmap "github.com/orcaman/concurrent-map" "go.etcd.io/bbolt" "regexp" @@ -30,14 +33,14 @@ import ( ) const ( - EventIdentityPostureDataAltered = "EventIdentityPostureDataAltered" - EventApiSessionPostureDataAltered = "EventApiSessionPostureDataAltered" + EventIdentityPostureDataAltered = "EventIdentityPostureDataAltered" ) type PostureCache struct { identityToPostureData cmap.ConcurrentMap //identityId -> PostureData apiSessionIdToIdentityId cmap.ConcurrentMap //apiSessionId -> identityId ticker *time.Ticker + isRunning concurrenz.AtomicBoolean events.EventEmmiter env Env } @@ -49,16 +52,18 @@ func newPostureCache(env Env) *PostureCache { ticker: time.NewTicker(5 * time.Second), EventEmmiter: events.New(), env: env, + isRunning: concurrenz.AtomicBoolean(0), } pc.run(env.GetHostController().GetCloseNotifyChannel()) - env.GetStores().Session.AddListener(boltz.EventCreate, pc.SessionCreated) - env.GetStores().Session.AddListener(boltz.EventDelete, pc.SessionDeleted) env.GetStores().ApiSession.AddListener(boltz.EventCreate, pc.ApiSessionCreated) env.GetStores().ApiSession.AddListener(boltz.EventDelete, pc.ApiSessionDeleted) env.GetStores().Identity.AddListener(boltz.EventDelete, pc.IdentityDeleted) + env.GetStores().PostureCheckType.AddListener(boltz.EventCreate, pc.PostureCheckChanged) + env.GetStores().PostureCheckType.AddListener(boltz.EventUpdate, pc.PostureCheckChanged) + return pc } @@ -67,27 +72,7 @@ func (pc *PostureCache) run(closeNotify <-chan struct{}) { for { select { case <-pc.ticker.C: - changedIdentityIds := map[string]struct{}{} - - for _, identityId := range pc.identityToPostureData.Keys() { - pc.identityToPostureData.Upsert(identityId, newPostureData(), func(exist bool, valueInMap interface{}, newValue interface{}) interface{} { - var postureData *PostureData - if exist { - postureData = valueInMap.(*PostureData) - } else { - postureData = newPostureData() - } - - if changed := postureData.CheckTimeouts(); changed { - changedIdentityIds[identityId] = struct{}{} - } - - return postureData - }) - } - for identityId := range changedIdentityIds { - pc.Emit(EventIdentityPostureDataAltered, identityId) - } + go pc.evaluate() case <-closeNotify: pc.ticker.Stop() return @@ -96,6 +81,94 @@ func (pc *PostureCache) run(closeNotify <-chan struct{}) { }() } +func (pc *PostureCache) evaluate() { + if !pc.isRunning.CompareAndSwap(false, true) { + return + } + + log := pfxlog.Logger() + + defer func() { + if r := recover(); r != nil { + log.Errorf("error during posture timeout enforcement: %v", r) + } + + pc.isRunning.Set(false) + }() + + var lastId []byte + const maxToDeletePerTx = 1000 + var toDeleteSessionIds []string + + newIdentityServiceUpdates := map[string]struct{}{} //tracks the current loops identityIds that have had updates + completedIdentityServiceUpdates := map[string]struct{}{} //tracks overall identityIds that have been notified of an update + + done := false + + // Chunk data in maxToDelete bunches to limit how many sessions we are deleting in a transaction. + // Requires tracking of which session was last evaluated, kept in lastId. + for !done { + _ = pc.env.GetDbProvider().GetDb().View(func(tx *bbolt.Tx) error { + cursor := pc.env.GetStores().Session.IterateIds(tx, ast.BoolNodeTrue) + + if len(lastId) != 0 { + cursor.Seek(lastId) + + if cursor.IsValid() { + if bytes.Compare(cursor.Current(), lastId) == 0 { + cursor.Next() + } + } + } + + done = !cursor.IsValid() + + for cursor.IsValid() && len(toDeleteSessionIds) < maxToDeletePerTx { + lastId = cursor.Current() + session, err := pc.env.GetStores().Session.LoadOneById(tx, string(cursor.Current())) + + if err == nil && session != nil { + result := pc.env.GetHandlers().Session.EvaluatePostureForService(session.IdentityId, session.ApiSessionId, session.Type, session.ServiceId, "") + + if !result.Passed { + log.WithFields(map[string]interface{}{ + "apiSessionId": session.ApiSessionId, + "identityId": session.IdentityId, + "sessionId": session.Id, + }).Tracef("session [%s] failed posture checks, removing", session.Id) + + toDeleteSessionIds = append(toDeleteSessionIds, session.Id) + newIdentityServiceUpdates[session.IdentityId] = struct{}{} + } + } + + cursor.Next() + } + + return nil + }) + + //delete sessions that failed pc checks, clear list + for _, sessionId := range toDeleteSessionIds { + err := pc.env.GetHandlers().Session.Delete(sessionId) + if err != nil { + log.WithError(err).Errorf("error removing session [%s] due to posture check failure, delete error: %v", sessionId, err) + } + } + toDeleteSessionIds = []string{} + + //notify endpoints that they may have new service posture queries + for identityId, _ := range newIdentityServiceUpdates { + if _, ok := completedIdentityServiceUpdates[identityId]; !ok { + completedIdentityServiceUpdates[identityId] = struct{}{} + pc.env.HandleServiceUpdatedEventForIdentityId(identityId) + } + } + + newIdentityServiceUpdates = map[string]struct{}{} + } +} + func (pc *PostureCache) Add(identityId string, postureResponses []*PostureResponse) { pc.Upsert(identityId, true, func(exist bool, valueInMap interface{}, newValue interface{}) interface{} { var postureData *PostureData @@ -187,114 +260,13 @@ func (pc *PostureCache) PostureData(identityId string) *PostureData { return result } -func (pc *PostureCache) SessionCreated(args ...interface{}) { - var session *persistence.Session - if len(args) == 1 { - session, _ = args[0].(*persistence.Session) - } - - if session == nil { - pfxlog.Logger().Error("session created event trigger with args[0] not convertible to *persistence.Session") - return - } - - mfaTimeout := int64(0) - - postureCheckLinks := pc.env.GetStores().ServicePolicy.GetLinkCollection(persistence.EntityTypePostureChecks) - - for _, policyId := range session.ServicePolicies { - pc.env.GetDbProvider().GetDb().View(func(tx *bbolt.Tx) error { - cursor := postureCheckLinks.IterateLinks(tx, []byte(policyId)) - - for cursor.IsValid() { - checkId := string(cursor.Current()) - if check, err := pc.env.GetStores().PostureCheck.LoadOneById(tx, checkId); err == nil { - if check.TypeId == PostureCheckTypeMFA { - if mfaCheck, ok := check.SubType.(*persistence.PostureCheckMfa); ok { - if mfaCheck.TimeoutSeconds == PostureCheckNoTimeout { - mfaTimeout = PostureCheckNoTimeout - } else if mfaTimeout != PostureCheckNoTimeout { - if mfaCheck.TimeoutSeconds > mfaTimeout { - mfaTimeout = mfaCheck.TimeoutSeconds - } - } - } - } - } - cursor.Next() - } - return nil - }) - } - - if identityIdVal, ok := pc.apiSessionIdToIdentityId.Get(session.ApiSessionId); ok { - identityId := identityIdVal.(string) - pc.identityToPostureData.Upsert(identityId, newPostureData(), func(exist bool, valueInMap interface{}, newValue interface{}) interface{} { - var pd *PostureData - - if exist { - pd = valueInMap.(*PostureData) - } else { - pd = newValue.(*PostureData) - } - - if pd.ApiSessions == nil { - pd.ApiSessions = map[string]*ApiSessionPostureData{} - } - - if _, ok := pd.ApiSessions[session.ApiSessionId]; !ok { - pd.ApiSessions[session.ApiSessionId] = &ApiSessionPostureData{} - } - - if pd.ApiSessions[session.ApiSessionId].Sessions == nil { - pd.ApiSessions[session.ApiSessionId].Sessions = map[string]*PostureSessionData{} - } - - pd.ApiSessions[session.ApiSessionId].Sessions[session.Id] = &PostureSessionData{ - MfaTimeout: mfaTimeout, - } - - return pd - }) - } -} - -func (pc *PostureCache) SessionDeleted(args ...interface{}) { - var session *persistence.Session - if len(args) == 1 { - session, _ = args[0].(*persistence.Session) - } - - if session == nil { - pfxlog.Logger().Error("session deleted event trigger with args[0] not convertible to *persistence.Session") - return - } - - if identityIdVal, ok := pc.apiSessionIdToIdentityId.Get(session.ApiSessionId); ok { - identityId := identityIdVal.(string) - pc.identityToPostureData.Upsert(identityId, newPostureData(), func(exist bool, valueInMap interface{}, newValue interface{}) interface{} { - var postureData *PostureData - if exist { - postureData = valueInMap.(*PostureData) - } else { - postureData = newValue.(*PostureData) - } - - if apiSessionData, ok := postureData.ApiSessions[session.ApiSessionId]; ok { - if apiSessionData.Sessions != nil { - delete(apiSessionData.Sessions, session.Id) - } - } - - return postureData - }) - } -} - func (pc *PostureCache) ApiSessionCreated(args ...interface{}) { var apiSession *persistence.ApiSession if len(args) == 1 { apiSession, _ = args[0].(*persistence.ApiSession) + } else { + pfxlog.Logger().Errorf("unexpected number of args [%d]", len(args)) + return } if apiSession == nil { @@ -309,6 +281,9 @@ func (pc *PostureCache) ApiSessionDeleted(args ...interface{}) { var apiSession *persistence.ApiSession if len(args) == 1 { apiSession, _ = args[0].(*persistence.ApiSession) + } else { + pfxlog.Logger().Errorf("unexpected number of args [%d]", len(args)) + return } if apiSession == nil { @@ -337,6 +312,9 @@ func (pc *PostureCache) IdentityDeleted(args ...interface{}) { var identity *persistence.Identity if len(args) == 1 { identity, _ = args[0].(*persistence.Identity) + } else { + pfxlog.Logger().Errorf("unexpected number of args [%d]", len(args)) + return } if identity == nil { @@ -347,6 +325,59 @@ func (pc *PostureCache) IdentityDeleted(args ...interface{}) { pc.identityToPostureData.Remove(identity.Id) } +//PostureCheckChanged notifies all associated identities that posture configuration has changed +//and that endpoints may need to reevaluate posture queries. +func (pc *PostureCache) PostureCheckChanged(args ...interface{}) { + var entity boltz.Entity + if len(args) == 1 { + var ok bool + entity, ok = args[0].(boltz.Entity) + + if !ok { + pfxlog.Logger().Errorf("unexpected type [%T] expected boltz.Entity: %v", args[0], args[0]) + } + } else { + pfxlog.Logger().Errorf("unexpected number of args [%d]", len(args)) + return + } + + servicePolicyLinks := pc.env.GetStores().PostureCheck.GetLinkCollection(persistence.EntityTypeServicePolicies) + + if servicePolicyLinks == nil { + pfxlog.Logger().Error("posture checks had no links to service policies") + return + } + + identitiesToNotify := map[string]struct{}{} + + _ = pc.env.GetDbProvider().GetDb().View(func(tx *bbolt.Tx) error { + servicePolicyCursor := servicePolicyLinks.IterateLinks(tx, []byte(entity.GetId())) + + for servicePolicyCursor.IsValid() { + identityLink := pc.env.GetStores().ServicePolicy.GetLinkCollection(persistence.EntityTypeIdentities) + + if identityLink == nil { + pfxlog.Logger().Error("service policies had no link to identities") + return nil + } + + identityCursor := identityLink.IterateLinks(tx, servicePolicyCursor.Current()) + + for identityCursor.IsValid() { + identitiesToNotify[string(identityCursor.Current())] = struct{}{} + } + + servicePolicyCursor.Next() + } + + return nil + }) + + for identityId, _ := range identitiesToNotify { + pc.env.HandleServiceUpdatedEventForIdentityId(identityId) + } +} + type PostureSessionData struct { MfaTimeout int64 } @@ -354,7 +385,6 @@ type PostureSessionData struct { type ApiSessionPostureData struct { Mfa *PostureResponseMfa `json:"mfa"` EndpointState *PostureResponseEndpointState `json:"endpointState"` - Sessions map[string]*PostureSessionData SdkInfo *SdkInfo } @@ -415,21 +445,6 @@ func (pd *PostureData) Evaluate(apiSessionId string, checks []*PostureCheck) (bo return len(failures) == 0, failures } -func (pd *PostureData) CheckTimeouts() bool { - for _, apiSessionData := range pd.ApiSessions { - for _, sessionData := range apiSessionData.Sessions { - if sessionData.MfaTimeout != PostureCheckNoTimeout && apiSessionData.Mfa != nil && apiSessionData.Mfa.PassedMfaAt != nil { - expiresAt := apiSessionData.Mfa.PassedMfaAt.Add(time.Duration(sessionData.MfaTimeout) * time.Second) - if expiresAt.Before(time.Now()) { - return true - } - } - } - } - - return false -} - func (pd *PostureData) Copy() *PostureData { dest := &PostureData{} copier.Copy(dest, pd) diff --git a/controller/model/service_handlers.go b/controller/model/service_handlers.go index 8c4f45acf..441a90562 100644 --- a/controller/model/service_handlers.go +++ b/controller/model/service_handlers.go @@ -261,6 +261,7 @@ func (handler *EdgeServiceHandler) mergeConfigs(tx *bbolt.Tx, configTypes map[st type PolicyPostureChecks struct { PostureChecks []*PostureCheck PolicyType persistence.PolicyType + PolicyName string } func (handler *EdgeServiceHandler) GetPolicyPostureChecks(identityId, serviceId string) map[string]*PolicyPostureChecks { @@ -293,6 +294,7 @@ func (handler *EdgeServiceHandler) GetPolicyPostureChecks(identityId, serviceId policyIdToChecks[policyIdStr] = &PolicyPostureChecks{ PostureChecks: []*PostureCheck{}, PolicyType: policy.PolicyType, + PolicyName: policy.Name, } cursor := postureCheckLinks.IterateLinks(tx, policyIdBytes) diff --git a/controller/model/session_handlers.go b/controller/model/session_handlers.go index a986a46e1..4b86a68c8 100644 --- a/controller/model/session_handlers.go +++ b/controller/model/session_handlers.go @@ -46,6 +46,109 @@ func (handler *SessionHandler) newModelEntity() boltEntitySink { return &Session{} } +type SessionPostureResult struct { + Passed bool + Failure *PostureSessionRequestFailure + PassingPolicyIds []string + Cause *apierror.GenericCauseError +} + +func (handler *SessionHandler) EvaluatePostureForService(identityId, apiSessionId, sessionType, serviceId, serviceName string) *SessionPostureResult { + + failureByPostureCheckId := map[string]*PostureCheckFailure{} //cache individual check status + validPosture := false + hasMatchingPolicies := false + + policyPostureCheckMap := handler.GetEnv().GetHandlers().EdgeService.GetPolicyPostureChecks(identityId, serviceId) + + failedPolicies := map[string][]*PostureCheckFailure{} + failedPoliciesIdToName := map[string]string{} + + var failedPolicyIds []string + var successPolicyIds []string + + for policyId, policyPostureCheck := range policyPostureCheckMap { + + if policyPostureCheck.PolicyType.String() != sessionType { + continue + } + hasMatchingPolicies = true + var failedChecks []*PostureCheckFailure + + for _, postureCheck := range policyPostureCheck.PostureChecks { + + found := false + + if _, found = failureByPostureCheckId[postureCheck.Id]; !found { + _, failureByPostureCheckId[postureCheck.Id] = handler.GetEnv().GetHandlers().PostureResponse.Evaluate(identityId, apiSessionId, postureCheck) + } + + if failureByPostureCheckId[postureCheck.Id] != nil { + failedChecks = append(failedChecks, failureByPostureCheckId[postureCheck.Id]) + } + } + + if len(failedChecks) == 0 { + validPosture = true + successPolicyIds = append(successPolicyIds, policyId) + } else { + //save for error output + failedPolicies[policyId] = failedChecks + failedPoliciesIdToName[policyId] = policyPostureCheck.PolicyName + failedPolicyIds = append(failedPolicyIds, policyId) + } + } + + if hasMatchingPolicies && !validPosture { + failureMap := map[string]interface{}{} + + sessionFailure := &PostureSessionRequestFailure{ + When: time.Now(), + ServiceId: serviceId, + ServiceName: serviceName, + ApiSessionId: apiSessionId, + SessionType: sessionType, + PolicyFailures: []*PosturePolicyFailure{}, + } + + for policyId, failures := range failedPolicies { + policyFailure := &PosturePolicyFailure{ + PolicyId: policyId, + PolicyName: failedPoliciesIdToName[policyId], + Checks: failures, + } + + var outFailures []interface{} + + for _, failure := range failures { + outFailures = append(outFailures, failure.ToClientErrorData()) + } + failureMap[policyId] = outFailures + + sessionFailure.PolicyFailures = append(sessionFailure.PolicyFailures, policyFailure) + } + + cause := &apierror.GenericCauseError{ + Message: fmt.Sprintf("Failed to pass posture checks for service policies: %v", failedPolicyIds), + DataMap: failureMap, + } + + return &SessionPostureResult{ + Passed: false, + Cause: cause, + PassingPolicyIds: nil, + Failure: sessionFailure, + } + } + + return &SessionPostureResult{ + Passed: true, + Cause: nil, + PassingPolicyIds: successPolicyIds, + Failure: nil, + } +} + func (handler *SessionHandler) Create(entity *Session) (string, error) { entity.Id = cuid.New() //use cuids which are longer than shortids but are monotonic @@ -74,91 +177,11 @@ func (handler *SessionHandler) Create(entity *Session) (string, error) { return "", errorz.NewFieldError("service not found", "ServiceId", entity.ServiceId) } - failureByPostureCheckId := map[string]*PostureCheckFailure{} //cache individual check status - validPosture := false - hasMatchingPolicies := false + policyResult := handler.EvaluatePostureForService(apiSession.IdentityId, apiSession.Id, entity.Type, service.Id, service.Name) - policyPostureCheckMap := handler.GetEnv().GetHandlers().EdgeService.GetPolicyPostureChecks(apiSession.IdentityId, entity.ServiceId) - - failedPolicies := map[string][]*PostureCheckFailure{} - failedPoliciesIdToName := map[string]string{} - - var failedPolicyIds []string - var successPolicyIds []string - - for policyId, policyPostureCheck := range policyPostureCheckMap { - policy, err := handler.GetEnv().GetHandlers().ServicePolicy.Read(policyId) - - if err != nil { - continue - } - - if policy.PolicyType != entity.Type { - continue - } - hasMatchingPolicies = true - var failedChecks []*PostureCheckFailure - - for _, postureCheck := range policyPostureCheck.PostureChecks { - - found := false - - if _, found = failureByPostureCheckId[postureCheck.Id]; !found { - _, failureByPostureCheckId[postureCheck.Id] = handler.GetEnv().GetHandlers().PostureResponse.Evaluate(apiSession.IdentityId, apiSession.Id, postureCheck) - } - - if failureByPostureCheckId[postureCheck.Id] != nil { - failedChecks = append(failedChecks, failureByPostureCheckId[postureCheck.Id]) - } - } - - if len(failedChecks) == 0 { - validPosture = true - successPolicyIds = append(successPolicyIds, policyId) - } else { - //save for error output - failedPolicies[policy.Id] = failedChecks - failedPoliciesIdToName[policy.Id] = policy.Name - failedPolicyIds = append(failedPolicyIds, policy.Id) - } - } - - if hasMatchingPolicies && !validPosture { - failureMap := map[string]interface{}{} - - sessionFailure := &PostureSessionRequestFailure{ - When: time.Now(), - ServiceId: service.Id, - ServiceName: service.Name, - ApiSessionId: apiSession.Id, - SessionType: entity.Type, - PolicyFailures: []*PosturePolicyFailure{}, - } - - for policyId, failures := range failedPolicies { - policyFailure := &PosturePolicyFailure{ - PolicyId: policyId, - PolicyName: failedPoliciesIdToName[policyId], - Checks: failures, - } - - var outFailures []interface{} - - for _, failure := range failures { - outFailures = append(outFailures, failure.ToClientErrorData()) - } - failureMap[policyId] = outFailures - - sessionFailure.PolicyFailures = append(sessionFailure.PolicyFailures, policyFailure) - } - - handler.env.GetHandlers().PostureResponse.postureCache.AddSessionRequestFailure(apiSession.IdentityId, sessionFailure) - - cause := apierror.GenericCauseError{ - Message: fmt.Sprintf("Failed to pass posture checks for service policies: %v", failedPolicyIds), - DataMap: failureMap, - } - return "", apierror.NewInvalidPosture(cause) + if !policyResult.Passed { + handler.env.GetHandlers().PostureResponse.postureCache.AddSessionRequestFailure(apiSession.IdentityId, policyResult.Failure) + return "", apierror.NewInvalidPosture(policyResult.Cause) } maxRows := 1 @@ -170,7 +193,7 @@ func (handler *SessionHandler) Create(entity *Session) (string, error) { return "", apierror.NewNoEdgeRoutersAvailable() } - entity.ServicePolicies = successPolicyIds + entity.ServicePolicies = policyResult.PassingPolicyIds return handler.createEntity(entity) } diff --git a/controller/model/session_model.go b/controller/model/session_model.go index 25ff6aebd..b3adbd375 100644 --- a/controller/model/session_model.go +++ b/controller/model/session_model.go @@ -31,6 +31,7 @@ import ( type Session struct { models.BaseEntity Token string + IdentityId string ApiSessionId string ServiceId string Type string @@ -58,6 +59,7 @@ func (entity *Session) toBoltEntityForCreate(tx *bbolt.Tx, handler Handler) (bol BaseExtEntity: *boltz.NewExtEntity(entity.Id, entity.Tags), Token: entity.Token, ApiSessionId: entity.ApiSessionId, + IdentityId: entity.IdentityId, ServiceId: entity.ServiceId, Type: entity.Type, ApiSession: apiSession, @@ -103,6 +105,7 @@ func (entity *Session) toBoltEntityForUpdate(*bbolt.Tx, Handler) (boltz.Entity, BaseExtEntity: *boltz.NewExtEntity(entity.Id, entity.Tags), Token: entity.Token, ApiSessionId: entity.ApiSessionId, + IdentityId: entity.IdentityId, ServiceId: entity.ServiceId, Type: entity.Type, ServicePolicies: entity.ServicePolicies, @@ -121,6 +124,7 @@ func (entity *Session) fillFrom(_ Handler, _ *bbolt.Tx, boltEntity boltz.Entity) entity.FillCommon(boltSession) entity.Token = boltSession.Token entity.ApiSessionId = boltSession.ApiSessionId + entity.IdentityId = boltSession.IdentityId entity.ServiceId = boltSession.ServiceId entity.Type = boltSession.Type entity.ServicePolicies = boltSession.ServicePolicies diff --git a/controller/persistence/migration_initialize.go b/controller/persistence/migration_initialize.go index 6dc644412..72091e48e 100644 --- a/controller/persistence/migration_initialize.go +++ b/controller/persistence/migration_initialize.go @@ -628,7 +628,7 @@ func (m *Migrations) addPostureCheckTypes(step *boltz.MigrationStep) { iOS, } - types := []*PostureCheckType{ + types := []*PostureCheckOs{ { BaseExtEntity: boltz.BaseExtEntity{ Id: "OS", diff --git a/controller/persistence/migration_v23.go b/controller/persistence/migration_v23.go index b244f46cc..89022c972 100644 --- a/controller/persistence/migration_v23.go +++ b/controller/persistence/migration_v23.go @@ -21,7 +21,7 @@ func (m *Migrations) addProcessMultiPostureCheck(step *boltz.MigrationStep) { OsVersions: []string{}, } - processCheckType := &PostureCheckType{ + processCheckType := &PostureCheckOs{ BaseExtEntity: boltz.BaseExtEntity{ Id: "PROCESS_MULTI", CreatedAt: time.Now(), diff --git a/controller/persistence/posture_check_mfa.go b/controller/persistence/posture_check_mfa.go index 84479f70c..d224ca78d 100644 --- a/controller/persistence/posture_check_mfa.go +++ b/controller/persistence/posture_check_mfa.go @@ -21,9 +21,9 @@ import ( ) const ( - FieldPostureCheckMfaTimeoutSeconds = "timeoutSeconds" - FieldPostureCheckMfaPromptOnWake = "promptOnWake" - FieldPostureCheckMfaPromptOnUnlock = "promptOnUnlock" + FieldPostureCheckMfaTimeoutSeconds = "timeoutSeconds" + FieldPostureCheckMfaPromptOnWake = "promptOnWake" + FieldPostureCheckMfaPromptOnUnlock = "promptOnUnlock" FieldPostureCheckMfaIgnoreLegacyEndpoints = "ignoreLegacyEndpoints" ) @@ -36,21 +36,30 @@ type PostureCheckMfa struct { func newPostureCheckMfa() PostureCheckSubType { return &PostureCheckMfa{ - TimeoutSeconds: 0, - PromptOnWake: false, - PromptOnUnlock: false, + TimeoutSeconds: 0, + PromptOnWake: false, + PromptOnUnlock: false, IgnoreLegacyEndpoints: false, } } func (entity *PostureCheckMfa) LoadValues(_ boltz.CrudStore, bucket *boltz.TypedBucket) { - entity.TimeoutSeconds = bucket.GetInt64WithDefault(FieldPostureCheckMfaTimeoutSeconds, 0) + entity.TimeoutSeconds = bucket.GetInt64WithDefault(FieldPostureCheckMfaTimeoutSeconds, -1) + + if entity.TimeoutSeconds == 0 { + entity.TimeoutSeconds = -1 + } + entity.PromptOnWake = bucket.GetBoolWithDefault(FieldPostureCheckMfaPromptOnWake, false) entity.PromptOnUnlock = bucket.GetBoolWithDefault(FieldPostureCheckMfaPromptOnUnlock, false) entity.IgnoreLegacyEndpoints = bucket.GetBoolWithDefault(FieldPostureCheckMfaIgnoreLegacyEndpoints, false) } func (entity *PostureCheckMfa) SetValues(ctx *boltz.PersistContext, bucket *boltz.TypedBucket) { + if entity.TimeoutSeconds == 0 { + entity.TimeoutSeconds = -1 + } + bucket.SetInt64(FieldPostureCheckMfaTimeoutSeconds, entity.TimeoutSeconds, ctx.FieldChecker) bucket.SetBool(FieldPostureCheckMfaPromptOnWake, entity.PromptOnWake, ctx.FieldChecker) bucket.SetBool(FieldPostureCheckMfaPromptOnUnlock, entity.PromptOnUnlock, ctx.FieldChecker) diff --git a/controller/persistence/posture_check_store.go b/controller/persistence/posture_check_store.go index b60857968..a15cbb5fa 100644 --- a/controller/persistence/posture_check_store.go +++ b/controller/persistence/posture_check_store.go @@ -149,6 +149,8 @@ func (store *postureCheckStoreImpl) GetRoleAttributesIndex() boltz.SetReadIndex func (store *postureCheckStoreImpl) initializeLocal() { store.AddExtEntitySymbols() store.indexName = store.addUniqueNameField() + store.AddSymbol(FieldPostureCheckMfaPromptOnUnlock, ast.NodeTypeBool, "MFA") + store.AddSymbol(FieldPostureCheckMfaPromptOnWake, ast.NodeTypeBool, "MFA") store.symbolRoleAttributes = store.AddSetSymbol(FieldRoleAttributes, ast.NodeTypeString) store.indexRoleAttributes = store.AddSetIndex(store.symbolRoleAttributes) diff --git a/controller/persistence/posture_check_type_store.go b/controller/persistence/posture_check_type_store.go index c55559cb9..33321be64 100644 --- a/controller/persistence/posture_check_type_store.go +++ b/controller/persistence/posture_check_type_store.go @@ -25,17 +25,17 @@ const ( FieldPostureCheckTypeOperatingSystems = "operatingSystems" ) -type PostureCheckType struct { +type PostureCheckOs struct { boltz.BaseExtEntity Name string OperatingSystems []OperatingSystem } -func (entity *PostureCheckType) GetName() string { +func (entity *PostureCheckOs) GetName() string { return entity.Name } -func (entity *PostureCheckType) LoadValues(_ boltz.CrudStore, bucket *boltz.TypedBucket) { +func (entity *PostureCheckOs) LoadValues(_ boltz.CrudStore, bucket *boltz.TypedBucket) { entity.LoadBaseValues(bucket) entity.Name = bucket.GetStringOrError(FieldName) @@ -60,7 +60,7 @@ func (entity *PostureCheckType) LoadValues(_ boltz.CrudStore, bucket *boltz.Type } } -func (entity *PostureCheckType) SetValues(ctx *boltz.PersistContext) { +func (entity *PostureCheckOs) SetValues(ctx *boltz.PersistContext) { entity.SetBaseValues(ctx) ctx.SetString(FieldName, entity.Name) @@ -86,14 +86,14 @@ func (entity *PostureCheckType) SetValues(ctx *boltz.PersistContext) { } } -func (entity *PostureCheckType) GetEntityType() string { +func (entity *PostureCheckOs) GetEntityType() string { return EntityTypePostureCheckTypes } type PostureCheckTypeStore interface { NameIndexedStore - LoadOneById(tx *bbolt.Tx, id string) (*PostureCheckType, error) - LoadOneByName(tx *bbolt.Tx, id string) (*PostureCheckType, error) + LoadOneById(tx *bbolt.Tx, id string) (*PostureCheckOs, error) + LoadOneByName(tx *bbolt.Tx, id string) (*PostureCheckOs, error) } func newPostureCheckTypeStore(stores *stores) *postureCheckTypeStoreImpl { @@ -110,7 +110,7 @@ type postureCheckTypeStoreImpl struct { } func (store *postureCheckTypeStoreImpl) NewStoreEntity() boltz.Entity { - return &PostureCheckType{} + return &PostureCheckOs{} } func (store *postureCheckTypeStoreImpl) initializeLocal() { @@ -126,15 +126,15 @@ func (store *postureCheckTypeStoreImpl) GetNameIndex() boltz.ReadIndex { return store.indexName } -func (store *postureCheckTypeStoreImpl) LoadOneById(tx *bbolt.Tx, id string) (*PostureCheckType, error) { - entity := &PostureCheckType{} +func (store *postureCheckTypeStoreImpl) LoadOneById(tx *bbolt.Tx, id string) (*PostureCheckOs, error) { + entity := &PostureCheckOs{} if err := store.baseLoadOneById(tx, id, entity); err != nil { return nil, err } return entity, nil } -func (store *postureCheckTypeStoreImpl) LoadOneByName(tx *bbolt.Tx, name string) (*PostureCheckType, error) { +func (store *postureCheckTypeStoreImpl) LoadOneByName(tx *bbolt.Tx, name string) (*PostureCheckOs, error) { id := store.indexName.Read(tx, []byte(name)) if id != nil { return store.LoadOneById(tx, string(id)) @@ -142,8 +142,8 @@ func (store *postureCheckTypeStoreImpl) LoadOneByName(tx *bbolt.Tx, name string) return nil, nil } -func (store *postureCheckTypeStoreImpl) LoadOneByQuery(tx *bbolt.Tx, query string) (*PostureCheckType, error) { - entity := &PostureCheckType{} +func (store *postureCheckTypeStoreImpl) LoadOneByQuery(tx *bbolt.Tx, query string) (*PostureCheckOs, error) { + entity := &PostureCheckOs{} if found, err := store.BaseLoadOneByQuery(tx, query, entity); !found || err != nil { return nil, err } diff --git a/controller/persistence/session_store.go b/controller/persistence/session_store.go index 5b593e439..0b6d9429b 100644 --- a/controller/persistence/session_store.go +++ b/controller/persistence/session_store.go @@ -30,6 +30,7 @@ const ( FieldSessionToken = "token" FieldSessionApiSession = "apiSession" FieldSessionService = "service" + FieldSessionIdentity = "identity" FieldSessionType = "type" FieldSessionServicePolicies = "servicePolicies" @@ -47,6 +48,7 @@ var validSessionTypes = []string{SessionTypeDial, SessionTypeBind} type Session struct { boltz.BaseExtEntity Token string + IdentityId string ApiSessionId string ServiceId string Type string @@ -60,6 +62,7 @@ func (entity *Session) LoadValues(_ boltz.CrudStore, bucket *boltz.TypedBucket) entity.Token = bucket.GetStringOrError(FieldSessionToken) entity.ApiSessionId = bucket.GetStringOrError(FieldSessionApiSession) entity.ServiceId = bucket.GetStringOrError(FieldSessionService) + entity.IdentityId = bucket.GetStringOrError(FieldSessionIdentity) entity.Type = bucket.GetStringWithDefault(FieldSessionType, "Dial") entity.ServicePolicies = bucket.GetStringList(FieldSessionServicePolicies) } @@ -78,6 +81,7 @@ func (entity *Session) SetValues(ctx *boltz.PersistContext) { ctx.SetString(FieldSessionToken, entity.Token) ctx.SetString(FieldSessionApiSession, entity.ApiSessionId) ctx.SetString(FieldSessionService, entity.ServiceId) + ctx.SetString(FieldSessionIdentity, entity.IdentityId) ctx.SetString(FieldSessionType, entity.Type) ctx.SetStringList(FieldSessionServicePolicies, entity.ServicePolicies) diff --git a/rest_client_api_client/posture_checks/create_posture_response_bulk_responses.go b/rest_client_api_client/posture_checks/create_posture_response_bulk_responses.go index 597c7dc7d..bf0b1125a 100644 --- a/rest_client_api_client/posture_checks/create_posture_response_bulk_responses.go +++ b/rest_client_api_client/posture_checks/create_posture_response_bulk_responses.go @@ -77,22 +77,22 @@ func NewCreatePostureResponseBulkOK() *CreatePostureResponseBulkOK { /* CreatePostureResponseBulkOK describes a response with status code 200, with default header values. -Base empty response +A list of terminators */ type CreatePostureResponseBulkOK struct { - Payload *rest_model.Empty + Payload *rest_model.PostureResponseEnvelope } func (o *CreatePostureResponseBulkOK) Error() string { return fmt.Sprintf("[POST /posture-response-bulk][%d] createPostureResponseBulkOK %+v", 200, o.Payload) } -func (o *CreatePostureResponseBulkOK) GetPayload() *rest_model.Empty { +func (o *CreatePostureResponseBulkOK) GetPayload() *rest_model.PostureResponseEnvelope { return o.Payload } func (o *CreatePostureResponseBulkOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { - o.Payload = new(rest_model.Empty) + o.Payload = new(rest_model.PostureResponseEnvelope) // response payload if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { diff --git a/rest_client_api_client/posture_checks/create_posture_response_responses.go b/rest_client_api_client/posture_checks/create_posture_response_responses.go index fb661241e..a66d800bc 100644 --- a/rest_client_api_client/posture_checks/create_posture_response_responses.go +++ b/rest_client_api_client/posture_checks/create_posture_response_responses.go @@ -77,22 +77,22 @@ func NewCreatePostureResponseCreated() *CreatePostureResponseCreated { /* CreatePostureResponseCreated describes a response with status code 201, with default header values. -The create request was successful and the resource has been added at the following location +A list of terminators */ type CreatePostureResponseCreated struct { - Payload *rest_model.CreateEnvelope + Payload *rest_model.PostureResponseEnvelope } func (o *CreatePostureResponseCreated) Error() string { return fmt.Sprintf("[POST /posture-response][%d] createPostureResponseCreated %+v", 201, o.Payload) } -func (o *CreatePostureResponseCreated) GetPayload() *rest_model.CreateEnvelope { +func (o *CreatePostureResponseCreated) GetPayload() *rest_model.PostureResponseEnvelope { return o.Payload } func (o *CreatePostureResponseCreated) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { - o.Payload = new(rest_model.CreateEnvelope) + o.Payload = new(rest_model.PostureResponseEnvelope) // response payload if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { diff --git a/rest_client_api_server/embedded_spec.go b/rest_client_api_server/embedded_spec.go index fe260e1f3..75dbef2c7 100644 --- a/rest_client_api_server/embedded_spec.go +++ b/rest_client_api_server/embedded_spec.go @@ -2238,9 +2238,9 @@ func init() { ], "responses": { "201": { - "description": "The create request was successful and the resource has been added at the following location", + "description": "A list of terminators", "schema": { - "$ref": "#/definitions/createEnvelope" + "$ref": "#/definitions/postureResponseEnvelope" } }, "400": { @@ -2336,9 +2336,9 @@ func init() { ], "responses": { "200": { - "description": "Base empty response", + "description": "A list of terminators", "schema": { - "$ref": "#/definitions/empty" + "$ref": "#/definitions/postureResponseEnvelope" } }, "400": { @@ -4952,6 +4952,20 @@ func init() { } } }, + "postureResponse": { + "type": "object", + "required": [ + "services" + ], + "properties": { + "services": { + "type": "array", + "items": { + "$ref": "#/definitions/postureResponseService" + } + } + } + }, "postureResponseCreate": { "type": "object", "required": [ @@ -5006,6 +5020,21 @@ func init() { ], "x-class": "ENDPOINT_STATE" }, + "postureResponseEnvelope": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "data": { + "$ref": "#/definitions/postureResponse" + }, + "meta": { + "$ref": "#/definitions/meta" + } + } + }, "postureResponseMacAddressCreate": { "allOf": [ { @@ -5085,6 +5114,33 @@ func init() { ], "x-class": "PROCESS" }, + "postureResponseService": { + "type": "object", + "required": [ + "id", + "name", + "postureQueryType", + "timeout", + "timeoutRemaining" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "postureQueryType": { + "type": "string" + }, + "timeout": { + "type": "integer" + }, + "timeoutRemaining": { + "type": "integer" + } + } + }, "protocol": { "type": "object", "required": [ @@ -5302,7 +5358,8 @@ func init() { "serviceId", "service", "token", - "edgeRouters" + "edgeRouters", + "identityId" ], "properties": { "apiSession": { @@ -5317,6 +5374,9 @@ func init() { "$ref": "#/definitions/sessionEdgeRouter" } }, + "identityId": { + "type": "string" + }, "service": { "$ref": "#/definitions/entityRef" }, @@ -7712,9 +7772,9 @@ func init() { ], "responses": { "201": { - "description": "The create request was successful and the resource has been added at the following location", + "description": "A list of terminators", "schema": { - "$ref": "#/definitions/createEnvelope" + "$ref": "#/definitions/postureResponseEnvelope" } }, "400": { @@ -7810,9 +7870,9 @@ func init() { ], "responses": { "200": { - "description": "Base empty response", + "description": "A list of terminators", "schema": { - "$ref": "#/definitions/empty" + "$ref": "#/definitions/postureResponseEnvelope" } }, "400": { @@ -10502,6 +10562,20 @@ func init() { } } }, + "postureResponse": { + "type": "object", + "required": [ + "services" + ], + "properties": { + "services": { + "type": "array", + "items": { + "$ref": "#/definitions/postureResponseService" + } + } + } + }, "postureResponseCreate": { "type": "object", "required": [ @@ -10556,6 +10630,21 @@ func init() { ], "x-class": "ENDPOINT_STATE" }, + "postureResponseEnvelope": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "data": { + "$ref": "#/definitions/postureResponse" + }, + "meta": { + "$ref": "#/definitions/meta" + } + } + }, "postureResponseMacAddressCreate": { "allOf": [ { @@ -10635,6 +10724,33 @@ func init() { ], "x-class": "PROCESS" }, + "postureResponseService": { + "type": "object", + "required": [ + "id", + "name", + "postureQueryType", + "timeout", + "timeoutRemaining" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "postureQueryType": { + "type": "string" + }, + "timeout": { + "type": "integer" + }, + "timeoutRemaining": { + "type": "integer" + } + } + }, "protocol": { "type": "object", "required": [ @@ -10852,7 +10968,8 @@ func init() { "serviceId", "service", "token", - "edgeRouters" + "edgeRouters", + "identityId" ], "properties": { "apiSession": { @@ -10867,6 +10984,9 @@ func init() { "$ref": "#/definitions/sessionEdgeRouter" } }, + "identityId": { + "type": "string" + }, "service": { "$ref": "#/definitions/entityRef" }, diff --git a/rest_client_api_server/operations/posture_checks/create_posture_response_bulk_responses.go b/rest_client_api_server/operations/posture_checks/create_posture_response_bulk_responses.go index 7e037bd30..914fae789 100644 --- a/rest_client_api_server/operations/posture_checks/create_posture_response_bulk_responses.go +++ b/rest_client_api_server/operations/posture_checks/create_posture_response_bulk_responses.go @@ -40,7 +40,7 @@ import ( // CreatePostureResponseBulkOKCode is the HTTP code returned for type CreatePostureResponseBulkOK const CreatePostureResponseBulkOKCode int = 200 -/*CreatePostureResponseBulkOK Base empty response +/*CreatePostureResponseBulkOK A list of terminators swagger:response createPostureResponseBulkOK */ @@ -49,7 +49,7 @@ type CreatePostureResponseBulkOK struct { /* In: Body */ - Payload *rest_model.Empty `json:"body,omitempty"` + Payload *rest_model.PostureResponseEnvelope `json:"body,omitempty"` } // NewCreatePostureResponseBulkOK creates CreatePostureResponseBulkOK with default headers values @@ -59,13 +59,13 @@ func NewCreatePostureResponseBulkOK() *CreatePostureResponseBulkOK { } // WithPayload adds the payload to the create posture response bulk o k response -func (o *CreatePostureResponseBulkOK) WithPayload(payload *rest_model.Empty) *CreatePostureResponseBulkOK { +func (o *CreatePostureResponseBulkOK) WithPayload(payload *rest_model.PostureResponseEnvelope) *CreatePostureResponseBulkOK { o.Payload = payload return o } // SetPayload sets the payload to the create posture response bulk o k response -func (o *CreatePostureResponseBulkOK) SetPayload(payload *rest_model.Empty) { +func (o *CreatePostureResponseBulkOK) SetPayload(payload *rest_model.PostureResponseEnvelope) { o.Payload = payload } diff --git a/rest_client_api_server/operations/posture_checks/create_posture_response_responses.go b/rest_client_api_server/operations/posture_checks/create_posture_response_responses.go index 3ffd92b75..2069ff22c 100644 --- a/rest_client_api_server/operations/posture_checks/create_posture_response_responses.go +++ b/rest_client_api_server/operations/posture_checks/create_posture_response_responses.go @@ -40,7 +40,7 @@ import ( // CreatePostureResponseCreatedCode is the HTTP code returned for type CreatePostureResponseCreated const CreatePostureResponseCreatedCode int = 201 -/*CreatePostureResponseCreated The create request was successful and the resource has been added at the following location +/*CreatePostureResponseCreated A list of terminators swagger:response createPostureResponseCreated */ @@ -49,7 +49,7 @@ type CreatePostureResponseCreated struct { /* In: Body */ - Payload *rest_model.CreateEnvelope `json:"body,omitempty"` + Payload *rest_model.PostureResponseEnvelope `json:"body,omitempty"` } // NewCreatePostureResponseCreated creates CreatePostureResponseCreated with default headers values @@ -59,13 +59,13 @@ func NewCreatePostureResponseCreated() *CreatePostureResponseCreated { } // WithPayload adds the payload to the create posture response created response -func (o *CreatePostureResponseCreated) WithPayload(payload *rest_model.CreateEnvelope) *CreatePostureResponseCreated { +func (o *CreatePostureResponseCreated) WithPayload(payload *rest_model.PostureResponseEnvelope) *CreatePostureResponseCreated { o.Payload = payload return o } // SetPayload sets the payload to the create posture response created response -func (o *CreatePostureResponseCreated) SetPayload(payload *rest_model.CreateEnvelope) { +func (o *CreatePostureResponseCreated) SetPayload(payload *rest_model.PostureResponseEnvelope) { o.Payload = payload } diff --git a/rest_management_api_server/embedded_spec.go b/rest_management_api_server/embedded_spec.go index 917e8896d..9b2d36c42 100644 --- a/rest_management_api_server/embedded_spec.go +++ b/rest_management_api_server/embedded_spec.go @@ -19745,7 +19745,8 @@ func init() { "serviceId", "service", "token", - "edgeRouters" + "edgeRouters", + "identityId" ], "properties": { "apiSession": { @@ -19760,6 +19761,9 @@ func init() { "$ref": "#/definitions/sessionEdgeRouter" } }, + "identityId": { + "type": "string" + }, "service": { "$ref": "#/definitions/entityRef" }, @@ -39979,7 +39983,8 @@ func init() { "serviceId", "service", "token", - "edgeRouters" + "edgeRouters", + "identityId" ], "properties": { "apiSession": { @@ -39994,6 +39999,9 @@ func init() { "$ref": "#/definitions/sessionEdgeRouter" } }, + "identityId": { + "type": "string" + }, "service": { "$ref": "#/definitions/entityRef" }, diff --git a/rest_model/posture_response.go b/rest_model/posture_response.go new file mode 100644 index 000000000..f8b568241 --- /dev/null +++ b/rest_model/posture_response.go @@ -0,0 +1,139 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// +// 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. +// +// __ __ _ +// \ \ / / (_) +// \ \ /\ / /_ _ _ __ _ __ _ _ __ __ _ +// \ \/ \/ / _` | '__| '_ \| | '_ \ / _` | +// \ /\ / (_| | | | | | | | | | | (_| | : This file is generated, do not edit it. +// \/ \/ \__,_|_| |_| |_|_|_| |_|\__, | +// __/ | +// |___/ + +package rest_model + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// PostureResponse posture response +// +// swagger:model postureResponse +type PostureResponse struct { + + // services + // Required: true + Services []*PostureResponseService `json:"services"` +} + +// Validate validates this posture response +func (m *PostureResponse) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateServices(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *PostureResponse) validateServices(formats strfmt.Registry) error { + + if err := validate.Required("services", "body", m.Services); err != nil { + return err + } + + for i := 0; i < len(m.Services); i++ { + if swag.IsZero(m.Services[i]) { // not required + continue + } + + if m.Services[i] != nil { + if err := m.Services[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("services" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this posture response based on the context it is used +func (m *PostureResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateServices(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *PostureResponse) contextValidateServices(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Services); i++ { + + if m.Services[i] != nil { + if err := m.Services[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("services" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *PostureResponse) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *PostureResponse) UnmarshalBinary(b []byte) error { + var res PostureResponse + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/rest_model/posture_response_envelope.go b/rest_model/posture_response_envelope.go new file mode 100644 index 000000000..4061137df --- /dev/null +++ b/rest_model/posture_response_envelope.go @@ -0,0 +1,171 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// +// 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. +// +// __ __ _ +// \ \ / / (_) +// \ \ /\ / /_ _ _ __ _ __ _ _ __ __ _ +// \ \/ \/ / _` | '__| '_ \| | '_ \ / _` | +// \ /\ / (_| | | | | | | | | | | (_| | : This file is generated, do not edit it. +// \/ \/ \__,_|_| |_| |_|_|_| |_|\__, | +// __/ | +// |___/ + +package rest_model + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// PostureResponseEnvelope posture response envelope +// +// swagger:model postureResponseEnvelope +type PostureResponseEnvelope struct { + + // data + // Required: true + Data *PostureResponse `json:"data"` + + // meta + // Required: true + Meta *Meta `json:"meta"` +} + +// Validate validates this posture response envelope +func (m *PostureResponseEnvelope) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateData(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMeta(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *PostureResponseEnvelope) validateData(formats strfmt.Registry) error { + + if err := validate.Required("data", "body", m.Data); err != nil { + return err + } + + if m.Data != nil { + if err := m.Data.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("data") + } + return err + } + } + + return nil +} + +func (m *PostureResponseEnvelope) validateMeta(formats strfmt.Registry) error { + + if err := validate.Required("meta", "body", m.Meta); err != nil { + return err + } + + if m.Meta != nil { + if err := m.Meta.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("meta") + } + return err + } + } + + return nil +} + +// ContextValidate validate this posture response envelope based on the context it is used +func (m *PostureResponseEnvelope) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateData(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateMeta(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *PostureResponseEnvelope) contextValidateData(ctx context.Context, formats strfmt.Registry) error { + + if m.Data != nil { + if err := m.Data.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("data") + } + return err + } + } + + return nil +} + +func (m *PostureResponseEnvelope) contextValidateMeta(ctx context.Context, formats strfmt.Registry) error { + + if m.Meta != nil { + if err := m.Meta.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("meta") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *PostureResponseEnvelope) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *PostureResponseEnvelope) UnmarshalBinary(b []byte) error { + var res PostureResponseEnvelope + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/rest_model/posture_response_service.go b/rest_model/posture_response_service.go new file mode 100644 index 000000000..f4859501f --- /dev/null +++ b/rest_model/posture_response_service.go @@ -0,0 +1,163 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// +// 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. +// +// __ __ _ +// \ \ / / (_) +// \ \ /\ / /_ _ _ __ _ __ _ _ __ __ _ +// \ \/ \/ / _` | '__| '_ \| | '_ \ / _` | +// \ /\ / (_| | | | | | | | | | | (_| | : This file is generated, do not edit it. +// \/ \/ \__,_|_| |_| |_|_|_| |_|\__, | +// __/ | +// |___/ + +package rest_model + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// PostureResponseService posture response service +// +// swagger:model postureResponseService +type PostureResponseService struct { + + // id + // Required: true + ID *string `json:"id"` + + // name + // Required: true + Name *string `json:"name"` + + // posture query type + // Required: true + PostureQueryType *string `json:"postureQueryType"` + + // timeout + // Required: true + Timeout *int64 `json:"timeout"` + + // timeout remaining + // Required: true + TimeoutRemaining *int64 `json:"timeoutRemaining"` +} + +// Validate validates this posture response service +func (m *PostureResponseService) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateName(formats); err != nil { + res = append(res, err) + } + + if err := m.validatePostureQueryType(formats); err != nil { + res = append(res, err) + } + + if err := m.validateTimeout(formats); err != nil { + res = append(res, err) + } + + if err := m.validateTimeoutRemaining(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *PostureResponseService) validateID(formats strfmt.Registry) error { + + if err := validate.Required("id", "body", m.ID); err != nil { + return err + } + + return nil +} + +func (m *PostureResponseService) validateName(formats strfmt.Registry) error { + + if err := validate.Required("name", "body", m.Name); err != nil { + return err + } + + return nil +} + +func (m *PostureResponseService) validatePostureQueryType(formats strfmt.Registry) error { + + if err := validate.Required("postureQueryType", "body", m.PostureQueryType); err != nil { + return err + } + + return nil +} + +func (m *PostureResponseService) validateTimeout(formats strfmt.Registry) error { + + if err := validate.Required("timeout", "body", m.Timeout); err != nil { + return err + } + + return nil +} + +func (m *PostureResponseService) validateTimeoutRemaining(formats strfmt.Registry) error { + + if err := validate.Required("timeoutRemaining", "body", m.TimeoutRemaining); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this posture response service based on context it is used +func (m *PostureResponseService) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *PostureResponseService) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *PostureResponseService) UnmarshalBinary(b []byte) error { + var res PostureResponseService + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/rest_model/session_detail.go b/rest_model/session_detail.go index 3d398adff..d23e53baa 100644 --- a/rest_model/session_detail.go +++ b/rest_model/session_detail.go @@ -57,6 +57,10 @@ type SessionDetail struct { // Required: true EdgeRouters []*SessionEdgeRouter `json:"edgeRouters"` + // identity Id + // Required: true + IdentityID *string `json:"identityId"` + // service // Required: true Service *EntityRef `json:"service"` @@ -91,6 +95,8 @@ func (m *SessionDetail) UnmarshalJSON(raw []byte) error { EdgeRouters []*SessionEdgeRouter `json:"edgeRouters"` + IdentityID *string `json:"identityId"` + Service *EntityRef `json:"service"` ServiceID *string `json:"serviceId"` @@ -109,6 +115,8 @@ func (m *SessionDetail) UnmarshalJSON(raw []byte) error { m.EdgeRouters = dataAO1.EdgeRouters + m.IdentityID = dataAO1.IdentityID + m.Service = dataAO1.Service m.ServiceID = dataAO1.ServiceID @@ -136,6 +144,8 @@ func (m SessionDetail) MarshalJSON() ([]byte, error) { EdgeRouters []*SessionEdgeRouter `json:"edgeRouters"` + IdentityID *string `json:"identityId"` + Service *EntityRef `json:"service"` ServiceID *string `json:"serviceId"` @@ -151,6 +161,8 @@ func (m SessionDetail) MarshalJSON() ([]byte, error) { dataAO1.EdgeRouters = m.EdgeRouters + dataAO1.IdentityID = m.IdentityID + dataAO1.Service = m.Service dataAO1.ServiceID = m.ServiceID @@ -188,6 +200,10 @@ func (m *SessionDetail) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateIdentityID(formats); err != nil { + res = append(res, err) + } + if err := m.validateService(formats); err != nil { res = append(res, err) } @@ -262,6 +278,15 @@ func (m *SessionDetail) validateEdgeRouters(formats strfmt.Registry) error { return nil } +func (m *SessionDetail) validateIdentityID(formats strfmt.Registry) error { + + if err := validate.Required("identityId", "body", m.IdentityID); err != nil { + return err + } + + return nil +} + func (m *SessionDetail) validateService(formats strfmt.Registry) error { if err := validate.Required("service", "body", m.Service); err != nil { diff --git a/specs/client.yml b/specs/client.yml index 849784521..df77019ca 100644 --- a/specs/client.yml +++ b/specs/client.yml @@ -1685,10 +1685,9 @@ paths: $ref: '#/definitions/postureResponseCreate' responses: "201": - description: The create request was successful and the resource has been - added at the following location + description: A list of terminators schema: - $ref: '#/definitions/createEnvelope' + $ref: '#/definitions/postureResponseEnvelope' "400": description: The supplied request contains invalid fields or could not be parsed (json and non-json bodies). The error's code, message, and cause @@ -1757,9 +1756,9 @@ paths: $ref: '#/definitions/postureResponseCreate' responses: "200": - description: Base empty response + description: A list of terminators schema: - $ref: '#/definitions/empty' + $ref: '#/definitions/postureResponseEnvelope' "400": description: The supplied request contains invalid fields or could not be parsed (json and non-json bodies). The error's code, message, and cause @@ -3566,6 +3565,15 @@ definitions: $ref: '#/definitions/osType' path: type: string + postureResponse: + type: object + required: + - services + properties: + services: + type: array + items: + $ref: '#/definitions/postureResponseService' postureResponseCreate: type: object required: @@ -3597,6 +3605,16 @@ definitions: woken: type: boolean x-class: ENDPOINT_STATE + postureResponseEnvelope: + type: object + required: + - meta + - data + properties: + data: + $ref: '#/definitions/postureResponse' + meta: + $ref: '#/definitions/meta' postureResponseMacAddressCreate: allOf: - $ref: '#/definitions/postureResponseCreate' @@ -3642,6 +3660,25 @@ definitions: items: type: string x-class: PROCESS + postureResponseService: + type: object + required: + - id + - name + - postureQueryType + - timeout + - timeoutRemaining + properties: + id: + type: string + name: + type: string + postureQueryType: + type: string + timeout: + type: integer + timeoutRemaining: + type: integer protocol: type: object required: @@ -3789,6 +3826,7 @@ definitions: - service - token - edgeRouters + - identityId properties: apiSession: $ref: '#/definitions/entityRef' @@ -3798,6 +3836,8 @@ definitions: type: array items: $ref: '#/definitions/sessionEdgeRouter' + identityId: + type: string service: $ref: '#/definitions/entityRef' serviceId: diff --git a/specs/management.yml b/specs/management.yml index 5ce960109..2e7328e8f 100644 --- a/specs/management.yml +++ b/specs/management.yml @@ -13995,6 +13995,7 @@ definitions: - service - token - edgeRouters + - identityId properties: apiSession: $ref: '#/definitions/entityRef' @@ -14004,6 +14005,8 @@ definitions: type: array items: $ref: '#/definitions/sessionEdgeRouter' + identityId: + type: string service: $ref: '#/definitions/entityRef' serviceId: diff --git a/specs/source/client/posture-response.yml b/specs/source/client/posture-response.yml index 839310202..ef31c9d93 100644 --- a/specs/source/client/posture-response.yml +++ b/specs/source/client/posture-response.yml @@ -18,7 +18,7 @@ paths: $ref: '#/definitions/postureResponseCreate' responses: '201': - $ref: '../shared/standard-responses.yml#/responses/createResponse' + $ref: '#/responses/postureResponse' '400': $ref: '../shared/standard-responses.yml#/responses/badRequestResponse' '401': @@ -44,14 +44,56 @@ paths: $ref: "#/definitions/postureResponseCreate" responses: '200': - $ref: '../shared/standard-responses.yml#/responses/emptyResponse' + $ref: '#/responses/postureResponse' '400': $ref: '../shared/standard-responses.yml#/responses/badRequestResponse' '401': $ref: '../shared/standard-responses.yml#/responses/unauthorizedResponse' responses: + postureResponse: + description: A list of terminators + schema: + $ref: '#/definitions/postureResponseEnvelope' definitions: + postureResponseEnvelope: + type: object + required: + - meta + - data + properties: + meta: + $ref: '../shared/standard-responses.yml#/definitions/meta' + data: + $ref: '#/definitions/postureResponse' + postureResponseService: + type: object + required: + - id + - name + - postureQueryType + - timeout + - timeoutRemaining + properties: + id: + type: string + name: + type: string + postureQueryType: + type: string + timeout: + type: integer + timeoutRemaining: + type: integer + postureResponse: + type: object + required: + - services + properties: + services: + type: array + items: + $ref: '#/definitions/postureResponseService' postureResponseCreate: type: object discriminator: typeId diff --git a/specs/source/shared/sessions.yml b/specs/source/shared/sessions.yml index 9c5f12616..cfb3177e4 100644 --- a/specs/source/shared/sessions.yml +++ b/specs/source/shared/sessions.yml @@ -50,6 +50,7 @@ definitions: - service - token - edgeRouters + - identityId properties: type: $ref: 'base-entity.yml#/definitions/dialBind' @@ -67,6 +68,8 @@ definitions: type: array items: $ref: '#/definitions/sessionEdgeRouter' + identityId: + type: string sessionEdgeRouter: allOf: - $ref: "edge-routers.yml#/definitions/commonEdgeRouterProperties" diff --git a/tests/posture_check_mfa_test.go b/tests/posture_check_mfa_test.go index df8247ffd..de0469bc0 100644 --- a/tests/posture_check_mfa_test.go +++ b/tests/posture_check_mfa_test.go @@ -448,6 +448,17 @@ func Test_PostureChecks_MFA(t *testing.T) { ctx.Req.NoError(err) standardJsonResponseTests(resp, http.StatusCreated, t) + + t.Run("woke posture response should include service timeout change event", func(t *testing.T) { + ctx.testContextChanged(t) + + responseEnvelope := &rest_model.PostureResponseEnvelope{} + + err := responseEnvelope.UnmarshalBinary(resp.Body()) + ctx.Req.NoError(err) + + ctx.Req.Len(responseEnvelope.Data.Services, 1) + }) t.Run("service has the posture check in its queries", func(t *testing.T) { ctx.testContextChanged(t) @@ -728,6 +739,19 @@ func Test_PostureChecks_MFA(t *testing.T) { ctx.Req.NoError(err) ctx.Req.Equal(http.StatusOK, resp.StatusCode()) + resp, err = newSession.NewRequest().Get("/current-api-session/service-updates") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, resp.StatusCode()) + + serviceUpdateContainer, err := gabs.ParseJSON(resp.Body()) + ctx.Req.NoError(err) + + ctx.Req.True(serviceUpdateContainer.ExistsP("data.lastChangeAt")) + + lastChangeAt, ok := serviceUpdateContainer.Path("data.lastChangeAt").Data().(string) + ctx.Req.True(ok) + ctx.Req.NotEmpty(lastChangeAt) + t.Run("after the MFA posture check timeout", func(t *testing.T) { ctx.testContextChanged(t) @@ -736,6 +760,25 @@ func Test_PostureChecks_MFA(t *testing.T) { time.Sleep(durationTillTimeout) } + t.Run("the last updated at serviced endpoint has changed", func(t *testing.T) { + ctx.testContextChanged(t) + + resp, err = newSession.NewRequest().Get("/current-api-session/service-updates") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, resp.StatusCode()) + + serviceUpdateContainer, err := gabs.ParseJSON(resp.Body()) + ctx.Req.NoError(err) + + ctx.Req.True(serviceUpdateContainer.ExistsP("data.lastChangeAt")) + + newLastChangeAt, ok := serviceUpdateContainer.Path("data.lastChangeAt").Data().(string) + ctx.Req.True(ok) + ctx.Req.NotEmpty(newLastChangeAt) + + ctx.Req.NotEqual(newLastChangeAt, lastChangeAt) + }) + t.Run("the existing session was removed", func(t *testing.T) { ctx.testContextChanged(t) From cf7c63ffa944a762b321496e7b6eed4f854784ab Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Mon, 30 Aug 2021 09:12:39 -0400 Subject: [PATCH 2/6] translate valeus less than 0 to -1 --- controller/persistence/posture_check_mfa.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/persistence/posture_check_mfa.go b/controller/persistence/posture_check_mfa.go index d224ca78d..c42cd748b 100644 --- a/controller/persistence/posture_check_mfa.go +++ b/controller/persistence/posture_check_mfa.go @@ -46,7 +46,7 @@ func newPostureCheckMfa() PostureCheckSubType { func (entity *PostureCheckMfa) LoadValues(_ boltz.CrudStore, bucket *boltz.TypedBucket) { entity.TimeoutSeconds = bucket.GetInt64WithDefault(FieldPostureCheckMfaTimeoutSeconds, -1) - if entity.TimeoutSeconds == 0 { + if entity.TimeoutSeconds <= 0 { entity.TimeoutSeconds = -1 } @@ -56,7 +56,7 @@ func (entity *PostureCheckMfa) LoadValues(_ boltz.CrudStore, bucket *boltz.Typed } func (entity *PostureCheckMfa) SetValues(ctx *boltz.PersistContext, bucket *boltz.TypedBucket) { - if entity.TimeoutSeconds == 0 { + if entity.TimeoutSeconds <= 0 { entity.TimeoutSeconds = -1 } From 4cac5518e927fdbed3f043fc48f4a74dd5b21479 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Tue, 31 Aug 2021 09:08:54 -0400 Subject: [PATCH 3/6] ensure that lastAccessedAt is set on create, emit logs if not found --- controller/env/appenv.go | 7 ++- .../internal/policy/api_session_enforcer.go | 11 +++- .../internal/routes/api_session_api_model.go | 2 +- .../internal/routes/authenticate_router.go | 12 +++-- controller/model/api_session_handlers.go | 8 ++- controller/model/api_session_heartbeats.go | 51 ++++++++++--------- controller/model/posture_check_model.go | 2 - .../create_posture_response_bulk_responses.go | 2 +- .../create_posture_response_responses.go | 2 +- rest_client_api_server/embedded_spec.go | 8 +-- .../create_posture_response_bulk_responses.go | 2 +- .../create_posture_response_responses.go | 2 +- specs/client.yml | 4 +- specs/source/client/posture-response.yml | 2 +- 14 files changed, 67 insertions(+), 48 deletions(-) diff --git a/controller/env/appenv.go b/controller/env/appenv.go index e4e990cee..cef9f9986 100644 --- a/controller/env/appenv.go +++ b/controller/env/appenv.go @@ -251,12 +251,11 @@ func (ae *AppEnv) FillRequestContext(rc *response.RequestContext) error { } } - //updates for session timeouts - if rc.ApiSession != nil { - ae.GetHandlers().ApiSession.MarkActivityById(rc.ApiSession.Id) - } if rc.ApiSession != nil { + //updates for api session timeouts + ae.GetHandlers().ApiSession.MarkActivityById(rc.ApiSession.Id) + var err error rc.Identity, err = ae.GetHandlers().Identity.Read(rc.ApiSession.IdentityId) if err != nil { diff --git a/controller/internal/policy/api_session_enforcer.go b/controller/internal/policy/api_session_enforcer.go index f16613ccb..6c171a5bb 100644 --- a/controller/internal/policy/api_session_enforcer.go +++ b/controller/internal/policy/api_session_enforcer.go @@ -57,10 +57,19 @@ func (s *ApiSessionEnforcer) Run() error { oldest := time.Now().Add(s.sessionTimeout * -1) query := fmt.Sprintf("lastActivityAt < datetime(%s) limit %d", oldest.UTC().Format(time.RFC3339), maxDeletePerIteration) + log := pfxlog.Logger() for i := 0; i < maxIterations; i++ { ids := make([]string, 0, maxDeletePerIteration) + + //iterate over API Sessions that do not have a recent enough lastAccessedAt err := s.appEnv.GetHandlers().ApiSession.StreamIds(query, func(id string, err error) error { - if lastActivityAt, hasUnflushedValue := s.appEnv.GetHandlers().ApiSession.HeartbeatCollector.LastAccessedAt(id); !hasUnflushedValue || lastActivityAt.Before(oldest) { + if lastActivityAt, ok := s.appEnv.GetHandlers().ApiSession.HeartbeatCollector.LastAccessedAt(id); ok && lastActivityAt != nil { + log.Trace("during API session enforcement lastAccessedAt check, API Session [%s] was found in the cache with time [%s]", id, lastActivityAt.String()) + if lastActivityAt.Before(oldest) { + ids = append(ids, id) + } + } else { + log.Trace("during API session enforcement lastAccessedAt check, API Session [%s] was not in the cache, using lastAccessedAt from db", id) ids = append(ids, id) } diff --git a/controller/internal/routes/api_session_api_model.go b/controller/internal/routes/api_session_api_model.go index 53f619af2..3ccbdcaa4 100644 --- a/controller/internal/routes/api_session_api_model.go +++ b/controller/internal/routes/api_session_api_model.go @@ -102,7 +102,7 @@ func MapApiSessionToRestModel(ae *env.AppEnv, apiSession *model.ApiSession) (*re } if val, ok := ae.GetHandlers().ApiSession.HeartbeatCollector.LastAccessedAt(apiSession.Id); ok { - cachedActivityAt := strfmt.DateTime(val) + cachedActivityAt := strfmt.DateTime(*val) ret.CachedLastActivityAt = cachedActivityAt } else { ret.CachedLastActivityAt = lastActivityAt diff --git a/controller/internal/routes/authenticate_router.go b/controller/internal/routes/authenticate_router.go index e79feacf9..1e0c76af1 100644 --- a/controller/internal/routes/authenticate_router.go +++ b/controller/internal/routes/authenticate_router.go @@ -53,19 +53,23 @@ func NewAuthRouter() *AuthRouter { func (ro *AuthRouter) Register(ae *env.AppEnv) { ro.createTimer = ae.GetHostController().GetNetwork().GetMetricsRegistry().Timer("api-session.create") ae.ClientApi.AuthenticationAuthenticateHandler = clientApiAuthentication.AuthenticateHandlerFunc(func(params clientApiAuthentication.AuthenticateParams) middleware.Responder { - return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) { ro.authHandler(ae, rc, params.HTTPRequest, params.Method, params.Auth) }, params.HTTPRequest, "", "", permissions.Always()) + return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) { + ro.authHandler(ae, rc, params.HTTPRequest, params.Method, params.Auth) + }, params.HTTPRequest, "", "", permissions.Always()) }) ae.ClientApi.AuthenticationAuthenticateMfaHandler = clientApiAuthentication.AuthenticateMfaHandlerFunc(func(params clientApiAuthentication.AuthenticateMfaParams, i interface{}) middleware.Responder { - return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) { ro.authMfa(ae, rc, params.MfaAuth) }, params.HTTPRequest, "", "", permissions.IsPartiallyAuthenticated()) + return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) { ro.authMfa(ae, rc, params.MfaAuth) }, params.HTTPRequest, "", "", permissions.HasOneOf(permissions.IsAuthenticated(), permissions.IsPartiallyAuthenticated())) }) ae.ManagementApi.AuthenticationAuthenticateHandler = managementApiAuthentication.AuthenticateHandlerFunc(func(params managementApiAuthentication.AuthenticateParams) middleware.Responder { - return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) { ro.authHandler(ae, rc, params.HTTPRequest, params.Method, params.Auth) }, params.HTTPRequest, "", "", permissions.Always()) + return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) { + ro.authHandler(ae, rc, params.HTTPRequest, params.Method, params.Auth) + }, params.HTTPRequest, "", "", permissions.Always()) }) ae.ManagementApi.AuthenticationAuthenticateMfaHandler = managementApiAuthentication.AuthenticateMfaHandlerFunc(func(params managementApiAuthentication.AuthenticateMfaParams, i interface{}) middleware.Responder { - return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) { ro.authMfa(ae, rc, params.MfaAuth) }, params.HTTPRequest, "", "", permissions.IsPartiallyAuthenticated()) + return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) { ro.authMfa(ae, rc, params.MfaAuth) }, params.HTTPRequest, "", "", permissions.HasOneOf(permissions.IsAuthenticated(), permissions.IsPartiallyAuthenticated())) }) } diff --git a/controller/model/api_session_handlers.go b/controller/model/api_session_handlers.go index ea78753da..df07ea2b5 100644 --- a/controller/model/api_session_handlers.go +++ b/controller/model/api_session_handlers.go @@ -52,7 +52,13 @@ func (handler *ApiSessionHandler) newModelEntity() boltEntitySink { func (handler *ApiSessionHandler) Create(entity *ApiSession) (string, error) { entity.Id = cuid.New() //use cuids which are longer than shortids but are monotonic - return handler.createEntity(entity) + id, err := handler.createEntity(entity) + + if err != nil { + handler.MarkActivityById(id) + } + + return id, err } func (handler *ApiSessionHandler) Read(id string) (*ApiSession, error) { diff --git a/controller/model/api_session_heartbeats.go b/controller/model/api_session_heartbeats.go index 7806deedb..234ce7338 100644 --- a/controller/model/api_session_heartbeats.go +++ b/controller/model/api_session_heartbeats.go @@ -26,12 +26,12 @@ import ( ) type HeartbeatCollector struct { - apiSessionLastUpdate cmap.ConcurrentMap //map[apiSessionId string] => *HeartbeatStatus - updateInterval time.Duration - closeNotify chan struct{} - isFlushing concurrenz.AtomicBoolean - flushAction func(beats []*Heartbeat) - batchSize int + apiSessionLastAccessedAtMap cmap.ConcurrentMap //map[apiSessionId string] => *HeartbeatStatus + updateInterval time.Duration + closeNotify chan struct{} + isFlushing concurrenz.AtomicBoolean + flushAction func(beats []*Heartbeat) + batchSize int } type Heartbeat struct { @@ -39,17 +39,17 @@ type Heartbeat struct { LastActivityAt time.Time } -// Creates a new HeartbeatCollector which is used to manage situations where an SDK is connecting to multiiple -// Edge Routers and making API calls that all update their last updated at and trigger a write. The heartbeat -// collector aggregates all of those calls into a single write and acts as an in memory buffer for last update -// times. +// NewHeartbeatCollector creates a HeartbeatCollector which is used to manage situations where an SDK is +// connecting to multiple Edge Routers and making API calls that all update their last updated at and trigger +// writes. The heartbeat collector aggregates all of those calls into a single write and acts as an in memory +// buffer for last update times. func NewHeartbeatCollector(env Env, batchSize int, updateInterval time.Duration, action func([]*Heartbeat)) *HeartbeatCollector { collector := &HeartbeatCollector{ - apiSessionLastUpdate: cmap.New(), - updateInterval: updateInterval, - batchSize: batchSize, - flushAction: action, - closeNotify: make(chan struct{}, 0), + apiSessionLastAccessedAtMap: cmap.New(), + updateInterval: updateInterval, + batchSize: batchSize, + flushAction: action, + closeNotify: make(chan struct{}, 0), } env.GetStores().ApiSession.AddListener(boltz.EventDelete, collector.onApiSessionDelete) @@ -69,17 +69,21 @@ func (self *HeartbeatCollector) Mark(apiSessionId string) { lastAccessedAt: time.Now().UTC(), flushed: false, } - self.apiSessionLastUpdate.Set(apiSessionId, newStatus) + self.apiSessionLastAccessedAtMap.Set(apiSessionId, newStatus) } -func (self *HeartbeatCollector) LastAccessedAt(apiSessionId string) (time.Time, bool) { - if val, ok := self.apiSessionLastUpdate.Get(apiSessionId); ok { +// LastAccessedAt will return the last time an API Sessions was either connected to an Edge Router +// or made a REST API call and true. If no such action has happened or the API Session no longer exists +// nil and false will be returned. +func (self *HeartbeatCollector) LastAccessedAt(apiSessionId string) (*time.Time, bool) { + if val, ok := self.apiSessionLastAccessedAtMap.Get(apiSessionId); ok { if status, ok := val.(*HeartbeatStatus); ok { - return status.lastAccessedAt, true + lastAccessedAt := status.lastAccessedAt + return &lastAccessedAt, true } } - return time.Time{}, false + return nil, false } func (self *HeartbeatCollector) Start() { @@ -105,11 +109,12 @@ func (self *HeartbeatCollector) Stop() { func (self *HeartbeatCollector) flush() { if self.isFlushing.CompareAndSwap(false, true) { + defer self.isFlushing.CompareAndSwap(true, false) pfxlog.Logger().Trace("flushing heartbeat collector") var beats []*Heartbeat - self.apiSessionLastUpdate.IterCb(func(key string, v interface{}) { + self.apiSessionLastAccessedAtMap.IterCb(func(key string, v interface{}) { if len(beats) >= self.batchSize { self.flushAction(beats) beats = nil @@ -130,8 +135,6 @@ func (self *HeartbeatCollector) flush() { self.flushAction(beats) } - self.isFlushing.CompareAndSwap(true, false) - } else { pfxlog.Logger().Warn("attempting to flush heartbeats from collector, a flush is already in progress, skipping") } @@ -147,5 +150,5 @@ func (self *HeartbeatCollector) onApiSessionDelete(i ...interface{}) { } func (self *HeartbeatCollector) Remove(id string) { - self.apiSessionLastUpdate.Remove(id) + self.apiSessionLastAccessedAtMap.Remove(id) } diff --git a/controller/model/posture_check_model.go b/controller/model/posture_check_model.go index 4735283e6..32a06240f 100644 --- a/controller/model/posture_check_model.go +++ b/controller/model/posture_check_model.go @@ -131,8 +131,6 @@ func (entity *PostureCheck) toBoltEntityForPatch(tx *bbolt.Tx, handler Handler, func (entity *PostureCheck) Evaluate(apiSessionId string, pd *PostureData) (bool, *PostureCheckFailure) { if !entity.SubType.Evaluate(apiSessionId, pd) { - - return false, &PostureCheckFailure{ PostureCheckId: entity.Id, PostureCheckName: entity.Name, diff --git a/rest_client_api_client/posture_checks/create_posture_response_bulk_responses.go b/rest_client_api_client/posture_checks/create_posture_response_bulk_responses.go index bf0b1125a..f09e47e76 100644 --- a/rest_client_api_client/posture_checks/create_posture_response_bulk_responses.go +++ b/rest_client_api_client/posture_checks/create_posture_response_bulk_responses.go @@ -77,7 +77,7 @@ func NewCreatePostureResponseBulkOK() *CreatePostureResponseBulkOK { /* CreatePostureResponseBulkOK describes a response with status code 200, with default header values. -A list of terminators +Contains a list of services that have had their timers altered */ type CreatePostureResponseBulkOK struct { Payload *rest_model.PostureResponseEnvelope diff --git a/rest_client_api_client/posture_checks/create_posture_response_responses.go b/rest_client_api_client/posture_checks/create_posture_response_responses.go index a66d800bc..4b3625271 100644 --- a/rest_client_api_client/posture_checks/create_posture_response_responses.go +++ b/rest_client_api_client/posture_checks/create_posture_response_responses.go @@ -77,7 +77,7 @@ func NewCreatePostureResponseCreated() *CreatePostureResponseCreated { /* CreatePostureResponseCreated describes a response with status code 201, with default header values. -A list of terminators +Contains a list of services that have had their timers altered */ type CreatePostureResponseCreated struct { Payload *rest_model.PostureResponseEnvelope diff --git a/rest_client_api_server/embedded_spec.go b/rest_client_api_server/embedded_spec.go index 75dbef2c7..08ca7cd7f 100644 --- a/rest_client_api_server/embedded_spec.go +++ b/rest_client_api_server/embedded_spec.go @@ -2238,7 +2238,7 @@ func init() { ], "responses": { "201": { - "description": "A list of terminators", + "description": "Contains a list of services that have had their timers altered", "schema": { "$ref": "#/definitions/postureResponseEnvelope" } @@ -2336,7 +2336,7 @@ func init() { ], "responses": { "200": { - "description": "A list of terminators", + "description": "Contains a list of services that have had their timers altered", "schema": { "$ref": "#/definitions/postureResponseEnvelope" } @@ -7772,7 +7772,7 @@ func init() { ], "responses": { "201": { - "description": "A list of terminators", + "description": "Contains a list of services that have had their timers altered", "schema": { "$ref": "#/definitions/postureResponseEnvelope" } @@ -7870,7 +7870,7 @@ func init() { ], "responses": { "200": { - "description": "A list of terminators", + "description": "Contains a list of services that have had their timers altered", "schema": { "$ref": "#/definitions/postureResponseEnvelope" } diff --git a/rest_client_api_server/operations/posture_checks/create_posture_response_bulk_responses.go b/rest_client_api_server/operations/posture_checks/create_posture_response_bulk_responses.go index 914fae789..7377b089a 100644 --- a/rest_client_api_server/operations/posture_checks/create_posture_response_bulk_responses.go +++ b/rest_client_api_server/operations/posture_checks/create_posture_response_bulk_responses.go @@ -40,7 +40,7 @@ import ( // CreatePostureResponseBulkOKCode is the HTTP code returned for type CreatePostureResponseBulkOK const CreatePostureResponseBulkOKCode int = 200 -/*CreatePostureResponseBulkOK A list of terminators +/*CreatePostureResponseBulkOK Contains a list of services that have had their timers altered swagger:response createPostureResponseBulkOK */ diff --git a/rest_client_api_server/operations/posture_checks/create_posture_response_responses.go b/rest_client_api_server/operations/posture_checks/create_posture_response_responses.go index 2069ff22c..3081710dc 100644 --- a/rest_client_api_server/operations/posture_checks/create_posture_response_responses.go +++ b/rest_client_api_server/operations/posture_checks/create_posture_response_responses.go @@ -40,7 +40,7 @@ import ( // CreatePostureResponseCreatedCode is the HTTP code returned for type CreatePostureResponseCreated const CreatePostureResponseCreatedCode int = 201 -/*CreatePostureResponseCreated A list of terminators +/*CreatePostureResponseCreated Contains a list of services that have had their timers altered swagger:response createPostureResponseCreated */ diff --git a/specs/client.yml b/specs/client.yml index df77019ca..f7cda06a5 100644 --- a/specs/client.yml +++ b/specs/client.yml @@ -1685,7 +1685,7 @@ paths: $ref: '#/definitions/postureResponseCreate' responses: "201": - description: A list of terminators + description: Contains a list of services that have had their timers altered schema: $ref: '#/definitions/postureResponseEnvelope' "400": @@ -1756,7 +1756,7 @@ paths: $ref: '#/definitions/postureResponseCreate' responses: "200": - description: A list of terminators + description: Contains a list of services that have had their timers altered schema: $ref: '#/definitions/postureResponseEnvelope' "400": diff --git a/specs/source/client/posture-response.yml b/specs/source/client/posture-response.yml index ef31c9d93..291bfbea5 100644 --- a/specs/source/client/posture-response.yml +++ b/specs/source/client/posture-response.yml @@ -51,7 +51,7 @@ paths: $ref: '../shared/standard-responses.yml#/responses/unauthorizedResponse' responses: postureResponse: - description: A list of terminators + description: Contains a list of services that have had their timers altered schema: $ref: '#/definitions/postureResponseEnvelope' From f095abe8dfd433e68ded81cecc44438f49f60f9c Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Thu, 2 Sep 2021 09:11:14 -0400 Subject: [PATCH 4/6] fix go test failures --- controller/internal/policy/api_session_enforcer.go | 4 ++-- tests/authenticate.go | 3 ++- tests/posture_check_mfa_test.go | 8 +++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/controller/internal/policy/api_session_enforcer.go b/controller/internal/policy/api_session_enforcer.go index 6c171a5bb..1ee3b3a0d 100644 --- a/controller/internal/policy/api_session_enforcer.go +++ b/controller/internal/policy/api_session_enforcer.go @@ -64,12 +64,12 @@ func (s *ApiSessionEnforcer) Run() error { //iterate over API Sessions that do not have a recent enough lastAccessedAt err := s.appEnv.GetHandlers().ApiSession.StreamIds(query, func(id string, err error) error { if lastActivityAt, ok := s.appEnv.GetHandlers().ApiSession.HeartbeatCollector.LastAccessedAt(id); ok && lastActivityAt != nil { - log.Trace("during API session enforcement lastAccessedAt check, API Session [%s] was found in the cache with time [%s]", id, lastActivityAt.String()) + log.Tracef("during API session enforcement lastAccessedAt check, API Session [%s] was found in the cache with time [%s]", id, lastActivityAt.String()) if lastActivityAt.Before(oldest) { ids = append(ids, id) } } else { - log.Trace("during API session enforcement lastAccessedAt check, API Session [%s] was not in the cache, using lastAccessedAt from db", id) + log.Tracef("during API session enforcement lastAccessedAt check, API Session [%s] was not in the cache, using lastAccessedAt from db", id) ids = append(ids, id) } diff --git a/tests/authenticate.go b/tests/authenticate.go index e256f52e1..02a8e412f 100644 --- a/tests/authenticate.go +++ b/tests/authenticate.go @@ -578,7 +578,8 @@ func (request *authenticatedRequests) requireNewPostureCheckProcessMulti(semanti func (request *authenticatedRequests) requireNewService(roleAttributes, configs []string) *service { service := request.testContext.newService(roleAttributes, configs) - request.requireCreateEntity(service) + id := request.requireCreateEntity(service) + service.Id = id return service } diff --git a/tests/posture_check_mfa_test.go b/tests/posture_check_mfa_test.go index de0469bc0..034714385 100644 --- a/tests/posture_check_mfa_test.go +++ b/tests/posture_check_mfa_test.go @@ -448,7 +448,7 @@ func Test_PostureChecks_MFA(t *testing.T) { ctx.Req.NoError(err) standardJsonResponseTests(resp, http.StatusCreated, t) - + t.Run("woke posture response should include service timeout change event", func(t *testing.T) { ctx.testContextChanged(t) @@ -458,6 +458,12 @@ func Test_PostureChecks_MFA(t *testing.T) { ctx.Req.NoError(err) ctx.Req.Len(responseEnvelope.Data.Services, 1) + ctx.Req.Equal(origTimeout, *responseEnvelope.Data.Services[0].Timeout) + ctx.Req.Equal(service.Name, *responseEnvelope.Data.Services[0].Name) + ctx.Req.Equal(service.Id, *responseEnvelope.Data.Services[0].ID) + ctx.Req.Equal("MFA", *responseEnvelope.Data.Services[0].PostureQueryType) + ctx.Req.Greater(*responseEnvelope.Data.Services[0].TimeoutRemaining, int64(1)) + ctx.Req.LessOrEqual(*responseEnvelope.Data.Services[0].TimeoutRemaining, int64(300)) }) t.Run("service has the posture check in its queries", func(t *testing.T) { From 76e839cfaa4aaf21ce69370c21d935a40fede5c1 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Thu, 2 Sep 2021 10:15:31 -0400 Subject: [PATCH 5/6] adds constants for posture check types --- controller/persistence/posture_check_store.go | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/controller/persistence/posture_check_store.go b/controller/persistence/posture_check_store.go index a15cbb5fa..01ff345d3 100644 --- a/controller/persistence/posture_check_store.go +++ b/controller/persistence/posture_check_store.go @@ -32,13 +32,22 @@ const ( FieldPostureCheckDialServices = "dialServices" ) +const ( + PostureCheckTypeOs = "OS" + PostureCheckTypeDomain = "DOMAIN" + PostureCheckTypeProcess = "PROCESS" + PostureCheckTypeProcessMulti = "PROCESS_MULTI" + PostureCheckTypeMAC = "MAC" + PostureCheckTypeMFA = "MFA" +) + var postureCheckSubTypeMap = map[string]newPostureCheckSubType{ - "OS": newPostureCheckOperatingSystem, - "DOMAIN": newPostureCheckWindowsDomain, - "PROCESS": newPostureCheckProcess, - "PROCESS_MULTI": newPostureCheckProcessMulti, - "MAC": newPostureCheckMacAddresses, - "MFA": newPostureCheckMfa, + PostureCheckTypeOs: newPostureCheckOperatingSystem, + PostureCheckTypeDomain: newPostureCheckWindowsDomain, + PostureCheckTypeProcess: newPostureCheckProcess, + PostureCheckTypeProcessMulti: newPostureCheckProcessMulti, + PostureCheckTypeMAC: newPostureCheckMacAddresses, + PostureCheckTypeMFA: newPostureCheckMfa, } type newPostureCheckSubType func() PostureCheckSubType @@ -149,8 +158,8 @@ func (store *postureCheckStoreImpl) GetRoleAttributesIndex() boltz.SetReadIndex func (store *postureCheckStoreImpl) initializeLocal() { store.AddExtEntitySymbols() store.indexName = store.addUniqueNameField() - store.AddSymbol(FieldPostureCheckMfaPromptOnUnlock, ast.NodeTypeBool, "MFA") - store.AddSymbol(FieldPostureCheckMfaPromptOnWake, ast.NodeTypeBool, "MFA") + store.AddSymbol(FieldPostureCheckMfaPromptOnUnlock, ast.NodeTypeBool, PostureCheckTypeMFA) + store.AddSymbol(FieldPostureCheckMfaPromptOnWake, ast.NodeTypeBool, PostureCheckTypeMFA) store.symbolRoleAttributes = store.AddSetSymbol(FieldRoleAttributes, ast.NodeTypeString) store.indexRoleAttributes = store.AddSetIndex(store.symbolRoleAttributes) From 57b6b70945bcf82ff4ec0eed10eafee9f64a2690 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Thu, 2 Sep 2021 11:00:09 -0400 Subject: [PATCH 6/6] add unit tests for timeout logic --- controller/model/posture_response_handlers.go | 29 +- .../model/posture_response_handlers_test.go | 255 ++++++++++++++++++ 2 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 controller/model/posture_response_handlers_test.go diff --git a/controller/model/posture_response_handlers.go b/controller/model/posture_response_handlers.go index 9bc821900..40c0ebdce 100644 --- a/controller/model/posture_response_handlers.go +++ b/controller/model/posture_response_handlers.go @@ -256,6 +256,25 @@ type ServiceWithTimeout struct { Timeout int64 } +func shouldPostureCheckTimeoutBeAltered(mfaCheck *persistence.PostureCheckMfa, timeSinceLastMfa, gracePeriod time.Duration, onWake, onUnlock bool) bool { + if mfaCheck == nil { + return false + } + + if (mfaCheck.PromptOnUnlock && onUnlock) || (mfaCheck.PromptOnWake && onWake) { + //no time out the remaining time was bigger than the grace period + timeSinceLastMfaSeconds := int64(timeSinceLastMfa.Seconds()) + gracePeriodSeconds := int64(gracePeriod.Seconds()) + + timeoutShouldBeAltered := mfaCheck.TimeoutSeconds == -1 || (mfaCheck.TimeoutSeconds-timeSinceLastMfaSeconds > gracePeriodSeconds) + + return timeoutShouldBeAltered + } + + + return false +} + func (handler *PostureResponseHandler) GetEndpointStateChangeAffectedServices(timeSinceLastMfa, gracePeriod time.Duration, onWake bool, onUnlock bool) []*ServiceWithTimeout { affectedChecks := map[string]int64{} //check id -> timeout if onWake || onUnlock { @@ -270,14 +289,8 @@ func (handler *PostureResponseHandler) GetEndpointStateChangeAffectedServices(ti for cursor.IsValid() { if check, err := handler.env.GetStores().PostureCheck.LoadOneById(tx, string(cursor.Current())); err == nil { if mfaCheck, ok := check.SubType.(*persistence.PostureCheckMfa); ok { - if (mfaCheck.PromptOnUnlock && onUnlock) || (mfaCheck.PromptOnWake && onWake) { - //no time out the remaining time was bigger than the grace period - timeSinceLastMfaSeconds := int64(timeSinceLastMfa.Seconds()) - gracePeriodSeconds := int64(gracePeriod.Seconds()) - - if mfaCheck.TimeoutSeconds == -1 || (mfaCheck.TimeoutSeconds-timeSinceLastMfaSeconds > gracePeriodSeconds) { - affectedChecks[check.Id] = mfaCheck.TimeoutSeconds - } + if shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, onWake, onUnlock) { + affectedChecks[check.Id] = mfaCheck.TimeoutSeconds } } } else { diff --git a/controller/model/posture_response_handlers_test.go b/controller/model/posture_response_handlers_test.go new file mode 100644 index 000000000..e627f0bb9 --- /dev/null +++ b/controller/model/posture_response_handlers_test.go @@ -0,0 +1,255 @@ +/* + 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/openziti/edge/controller/persistence" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func mustParseDuration(s string) time.Duration { + result, err := time.ParseDuration(s) + + if err != nil { + panic(err) + } + + return result +} + +func TestPostureCheckResponseHandlers_shouldPostureCheckTimeoutBeAltered(t *testing.T) { + + t.Run("returns false if the check is null", func(t *testing.T) { + result := shouldPostureCheckTimeoutBeAltered(nil, mustParseDuration("10m"), mustParseDuration("5m"), true, true) + + require.New(t).False(result) + }) + + t.Run("returns false if no prompts (all false), timeout remaining is greater than grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnWake: false, + PromptOnUnlock: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("3m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, true, true) + + require.New(t).False(result) + }) + + // promptOnWake = true, wake = true + t.Run("returns true if promptOnWake=true, wake=true, timeout remaining is greater than grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnWake: true, + PromptOnUnlock: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("3m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, true, false) + + require.New(t).True(result) + }) + + t.Run("returns false if promptOnWake=true, wake=true, timeout remaining is less than grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnWake: true, + PromptOnUnlock: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("6m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, true, false) + + require.New(t).False(result) + }) + + t.Run("returns false if promptOnWake=true, wake=true, timeout remaining equals grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnWake: true, + PromptOnUnlock: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("5m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, true, false) + + require.New(t).False(result) + }) + + // promptOnWake = true, wake = false + t.Run("returns false if promptOnWake=true, wake=false, timeout remaining is greater than grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnWake: true, + PromptOnUnlock: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("3m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, false, false) + + require.New(t).False(result) + }) + + t.Run("returns false if promptOnWake=true, wake=false, timeout remaining is less than grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnWake: true, + PromptOnUnlock: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("6m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, false, false) + + require.New(t).False(result) + }) + + t.Run("returns false if promptOnWake=true, wake=false, timeout remaining equals grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnWake: true, + PromptOnUnlock: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("5m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, false, false) + + require.New(t).False(result) + }) + + // promptOnUnlock = true, unlock = true + t.Run("returns true if promptOnUnlock=true, unlock=true, timeout remaining is greater than grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnUnlock: true, + PromptOnWake: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("3m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, false, true) + + require.New(t).True(result) + }) + + t.Run("returns false if promptOnUnlock=true, unlock=true, timeout remaining is less than grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnUnlock: true, + PromptOnWake: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("6m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, false, true) + + require.New(t).False(result) + }) + + t.Run("returns false if promptOnUnlock=true, unlock=true, timeout remaining equals grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnUnlock: true, + PromptOnWake: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("5m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, false, true) + + require.New(t).False(result) + }) + + // promptOnUnlock = true, unlock = false + t.Run("returns false if promptOnUnlock=true, unlock=false, timeout remaining is greater than grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnUnlock: true, + PromptOnWake: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("3m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, false, false) + + require.New(t).False(result) + }) + + t.Run("returns false if promptOnUnlock=true, unlock=false, timeout remaining is less than grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnUnlock: true, + PromptOnWake: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("6m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, false, false) + + require.New(t).False(result) + }) + + t.Run("returns false if promptOnUnlock=true, unlock=false, timeout remaining equals grace", func(t *testing.T) { + mfaCheck := &persistence.PostureCheckMfa{ + TimeoutSeconds: int64(mustParseDuration("10m").Seconds()), + PromptOnUnlock: true, + PromptOnWake: false, + IgnoreLegacyEndpoints: false, + } + + timeSinceLastMfa := mustParseDuration("5m") + gracePeriod := mustParseDuration("5m") + + result := shouldPostureCheckTimeoutBeAltered(mfaCheck, timeSinceLastMfa, gracePeriod, false, false) + + require.New(t).False(result) + }) +} \ No newline at end of file