From 2f07f7ce738a67cc813264e1a69135820a738545 Mon Sep 17 00:00:00 2001 From: cabbage Date: Sun, 6 Sep 2026 05:21:10 +0000 Subject: [PATCH] feat: select Windows execution contexts --- .../client/supervisor/windows/selection.go | 200 ++++++++++++++++++ .../supervisor/windows/selection_test.go | 115 ++++++++++ test/coverage.toml | 11 +- 3 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 internal/client/supervisor/windows/selection.go create mode 100644 internal/client/supervisor/windows/selection_test.go diff --git a/internal/client/supervisor/windows/selection.go b/internal/client/supervisor/windows/selection.go new file mode 100644 index 0000000..9903226 --- /dev/null +++ b/internal/client/supervisor/windows/selection.go @@ -0,0 +1,200 @@ +// Package windows contains Windows-supervisor policy that is safe to unit test +// without loading Win32. Narrow build-tagged adapters obtain and verify the +// real token/session facts before they reach this selector. +package windows + +import ( + "fmt" + + "github.com/rvbox/rvbox/internal/domain" +) + +// ExecutionContext is the only context vocabulary exposed by the Windows v1 +// policy. The caller expresses merely Elevated; all fallback ordering remains +// local to the client daemon. +type ExecutionContext string + +const ( + ContextActiveUser ExecutionContext = "ACTIVE_USER" + ContextActiveUserElevated ExecutionContext = "ACTIVE_USER_ELEVATED" + ContextActiveSystem ExecutionContext = "ACTIVE_SYSTEM" + ContextLocalService ExecutionContext = "LOCAL_SERVICE" + ContextLocalSystem ExecutionContext = "LOCAL_SYSTEM" +) + +type AttemptReason string + +const ( + ReasonSelected AttemptReason = "SELECTED" + ReasonNoUsableActiveSession AttemptReason = "NO_USABLE_ACTIVE_SESSION" + ReasonAmbiguousActiveSessions AttemptReason = "AMBIGUOUS_ACTIVE_SESSIONS" + ReasonStandardToken AttemptReason = "STANDARD_OR_FILTERED_TOKEN" + ReasonRestrictedToken AttemptReason = "RESTRICTED_MEDIUM_TOKEN" + ReasonRestrictedTokenUnavailable AttemptReason = "RESTRICTED_TOKEN_UNAVAILABLE" + ReasonElevationUnavailable AttemptReason = "ELEVATION_UNAVAILABLE" + ReasonApprovalPolicy AttemptReason = "APPROVAL_POLICY" + ReasonActiveSystemUnavailable AttemptReason = "ACTIVE_SYSTEM_UNAVAILABLE" + ReasonLocalServiceUnavailable AttemptReason = "LOCAL_SERVICE_UNAVAILABLE" + ReasonLocalSystemUnavailable AttemptReason = "LOCAL_SYSTEM_UNAVAILABLE" +) + +// TokenFacts are verified observations, not token handles. The native adapter +// must set these only after it has checked SID, session, type, elevation, and +// integrity properties. +type TokenFacts struct { + Usable bool + StandardOrFiltered bool + FullAdministrator bool + LinkedFullAvailable bool + RestrictedMediumAllowed bool + ApprovalPolicyRequired bool +} + +// SessionCandidate represents an active WTS session after native enumeration. +// It deliberately contains no handles or credentials. +type SessionCandidate struct { + SessionID uint32 + Console bool + UserSID string + LogonSID string + Token TokenFacts +} + +type SelectionInput struct { + Elevated bool + ActiveSessions []SessionCandidate + ActiveSystemAvailable bool + LocalServiceAvailable bool + LocalSystemAvailable bool +} + +type Attempt struct { + Context ExecutionContext + Reason AttemptReason + Success bool +} + +type Identity struct { + Context ExecutionContext + SessionID *uint32 + UserSID string + LogonSID string +} + +type Selection struct { + Elevated bool + Attempts []Attempt + Effective *Identity + NoActiveReason AttemptReason + Error *domain.Error +} + +// Select applies the v1 hierarchy. It never chooses an arbitrary active +// session and never substitutes a service identity for a failed normal active +// user selection. +func Select(input SelectionInput) Selection { + selected, absentReason := chooseActiveSession(input.ActiveSessions) + result := Selection{Elevated: input.Elevated, NoActiveReason: absentReason} + if selected != nil { + if !input.Elevated { + return selectActiveNormal(result, *selected) + } + return selectActiveElevated(result, *selected, input) + } + if input.Elevated { + return selectNoUserElevated(result, input) + } + return selectNoUserNormal(result, input) +} + +func chooseActiveSession(candidates []SessionCandidate) (*SessionCandidate, AttemptReason) { + var usable []SessionCandidate + for _, candidate := range candidates { + if candidate.Token.Usable && candidate.UserSID != "" && candidate.LogonSID != "" { + if candidate.Console { + selected := candidate + return &selected, "" + } + usable = append(usable, candidate) + } + } + if len(usable) == 1 { + return &usable[0], "" + } + if len(usable) > 1 { + return nil, ReasonAmbiguousActiveSessions + } + return nil, ReasonNoUsableActiveSession +} + +func selectActiveNormal(result Selection, candidate SessionCandidate) Selection { + if candidate.Token.StandardOrFiltered { + return success(result, ContextActiveUser, ReasonStandardToken, candidate) + } + if candidate.Token.FullAdministrator && candidate.Token.RestrictedMediumAllowed { + return success(result, ContextActiveUser, ReasonRestrictedToken, candidate) + } + result.Attempts = append(result.Attempts, Attempt{Context: ContextActiveUser, Reason: ReasonRestrictedTokenUnavailable}) + result.Error = domain.NewExecutionContextUnavailable("a non-elevated active-user token could not be prepared", "") + return result +} + +func selectActiveElevated(result Selection, candidate SessionCandidate, input SelectionInput) Selection { + if candidate.Token.ApprovalPolicyRequired { + result.Attempts = append(result.Attempts, Attempt{Context: ContextActiveUserElevated, Reason: ReasonApprovalPolicy}) + } else if candidate.Token.FullAdministrator || candidate.Token.LinkedFullAvailable { + return success(result, ContextActiveUserElevated, ReasonSelected, candidate) + } else { + result.Attempts = append(result.Attempts, Attempt{Context: ContextActiveUserElevated, Reason: ReasonElevationUnavailable}) + } + if input.ActiveSystemAvailable { + return success(result, ContextActiveSystem, ReasonSelected, candidate) + } + result.Attempts = append(result.Attempts, Attempt{Context: ContextActiveSystem, Reason: ReasonActiveSystemUnavailable}) + if input.LocalSystemAvailable { + return success(result, ContextLocalSystem, ReasonSelected, SessionCandidate{}) + } + result.Attempts = append(result.Attempts, Attempt{Context: ContextLocalSystem, Reason: ReasonLocalSystemUnavailable}) + result.Error = domain.NewElevationUnavailable("no elevated Windows execution context could be prepared", "") + return result +} + +func selectNoUserNormal(result Selection, input SelectionInput) Selection { + if input.LocalServiceAvailable { + return success(result, ContextLocalService, ReasonSelected, SessionCandidate{}) + } + result.Attempts = append(result.Attempts, Attempt{Context: ContextLocalService, Reason: ReasonLocalServiceUnavailable}) + result.Error = domain.NewExecutionContextUnavailable("LocalService execution context could not be prepared", "") + return result +} + +func selectNoUserElevated(result Selection, input SelectionInput) Selection { + if input.LocalSystemAvailable { + return success(result, ContextLocalSystem, ReasonSelected, SessionCandidate{}) + } + result.Attempts = append(result.Attempts, Attempt{Context: ContextLocalSystem, Reason: ReasonLocalSystemUnavailable}) + result.Error = domain.NewElevationUnavailable("LocalSystem execution context could not be prepared", "") + return result +} + +func success(result Selection, context ExecutionContext, reason AttemptReason, candidate SessionCandidate) Selection { + result.Attempts = append(result.Attempts, Attempt{Context: context, Reason: reason, Success: true}) + identity := &Identity{Context: context} + if context == ContextActiveUser || context == ContextActiveUserElevated || context == ContextActiveSystem { + identity.SessionID = &candidate.SessionID + identity.UserSID = candidate.UserSID + identity.LogonSID = candidate.LogonSID + } + result.Effective = identity + return result +} + +func (selection Selection) Validate() error { + if selection.Effective != nil && selection.Error != nil { + return fmt.Errorf("effective context and error cannot coexist") + } + if selection.Effective == nil && selection.Error == nil { + return fmt.Errorf("selection has neither context nor error") + } + return nil +} diff --git a/internal/client/supervisor/windows/selection_test.go b/internal/client/supervisor/windows/selection_test.go new file mode 100644 index 0000000..8bf77b6 --- /dev/null +++ b/internal/client/supervisor/windows/selection_test.go @@ -0,0 +1,115 @@ +package windows + +import ( + "testing" + + rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1" +) + +func TestSelectExecutionContext_HP_WINCTX_02(t *testing.T) { + t.Parallel() + standard := active(TokenFacts{Usable: true, StandardOrFiltered: true}) + full := active(TokenFacts{Usable: true, FullAdministrator: true}) + cases := []struct { + name string + input SelectionInput + want ExecutionContext + attempts []ExecutionContext + }{ + {"active standard normal", SelectionInput{ActiveSessions: []SessionCandidate{standard}}, ContextActiveUser, []ExecutionContext{ContextActiveUser}}, + {"active full normal restricted", SelectionInput{ActiveSessions: []SessionCandidate{active(TokenFacts{Usable: true, FullAdministrator: true, RestrictedMediumAllowed: true})}}, ContextActiveUser, []ExecutionContext{ContextActiveUser}}, + {"active linked admin elevated", SelectionInput{Elevated: true, ActiveSessions: []SessionCandidate{active(TokenFacts{Usable: true, LinkedFullAvailable: true})}}, ContextActiveUserElevated, []ExecutionContext{ContextActiveUserElevated}}, + {"active full admin elevated", SelectionInput{Elevated: true, ActiveSessions: []SessionCandidate{full}}, ContextActiveUserElevated, []ExecutionContext{ContextActiveUserElevated}}, + {"active elevation fallback", SelectionInput{Elevated: true, ActiveSessions: []SessionCandidate{standard}, ActiveSystemAvailable: true}, ContextActiveSystem, []ExecutionContext{ContextActiveUserElevated, ContextActiveSystem}}, + {"active local system final fallback", SelectionInput{Elevated: true, ActiveSessions: []SessionCandidate{standard}, LocalSystemAvailable: true}, ContextLocalSystem, []ExecutionContext{ContextActiveUserElevated, ContextActiveSystem, ContextLocalSystem}}, + {"no user normal", SelectionInput{LocalServiceAvailable: true}, ContextLocalService, []ExecutionContext{ContextLocalService}}, + {"no user elevated", SelectionInput{Elevated: true, LocalSystemAvailable: true}, ContextLocalSystem, []ExecutionContext{ContextLocalSystem}}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + got := Select(test.input) + if err := got.Validate(); err != nil { + t.Fatal(err) + } + if got.Effective == nil || got.Effective.Context != test.want { + t.Fatalf("effective = %+v, want %s", got.Effective, test.want) + } + if len(got.Attempts) != len(test.attempts) { + t.Fatalf("attempt count = %d, want %d", len(got.Attempts), len(test.attempts)) + } + for index, want := range test.attempts { + if got.Attempts[index].Context != want { + t.Fatalf("attempt %d = %s, want %s", index, got.Attempts[index].Context, want) + } + } + if got.Effective.Context == ContextLocalSystem || got.Effective.Context == ContextLocalService { + if got.Effective.SessionID != nil || got.Effective.UserSID != "" || got.Effective.LogonSID != "" { + t.Fatalf("Session 0 identity leaked active-session fields: %+v", got.Effective) + } + } + }) + } + + _ = rvboxv1.ControlError_CODE_ELEVATION_UNAVAILABLE +} + +func TestSelectExecutionContextBoundaries_BH_WINCTX_02(t *testing.T) { + t.Parallel() + standard := active(TokenFacts{Usable: true, StandardOrFiltered: true}) + cases := []struct { + name string + input SelectionInput + wantCode rvboxv1.ControlError_Code + wantReason AttemptReason + wantTry []ExecutionContext + }{ + {"normal active cannot downgrade full", SelectionInput{ActiveSessions: []SessionCandidate{active(TokenFacts{Usable: true, FullAdministrator: true})}, LocalServiceAvailable: true}, rvboxv1.ControlError_CODE_EXECUTION_CONTEXT_UNAVAILABLE, "", []ExecutionContext{ContextActiveUser}}, + {"approval falls back", SelectionInput{Elevated: true, ActiveSessions: []SessionCandidate{active(TokenFacts{Usable: true, ApprovalPolicyRequired: true})}, ActiveSystemAvailable: false, LocalSystemAvailable: true}, 0, "", []ExecutionContext{ContextActiveUserElevated, ContextActiveSystem, ContextLocalSystem}}, + {"ambiguous sessions use no user row", SelectionInput{Elevated: false, ActiveSessions: ambiguousSessions(standard.Token), LocalServiceAvailable: true}, 0, ReasonAmbiguousActiveSessions, []ExecutionContext{ContextLocalService}}, + {"no user service unavailable", SelectionInput{}, rvboxv1.ControlError_CODE_EXECUTION_CONTEXT_UNAVAILABLE, ReasonNoUsableActiveSession, []ExecutionContext{ContextLocalService}}, + {"no user elevated unavailable", SelectionInput{Elevated: true}, rvboxv1.ControlError_CODE_ELEVATION_UNAVAILABLE, ReasonNoUsableActiveSession, []ExecutionContext{ContextLocalSystem}}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + got := Select(test.input) + if err := got.Validate(); err != nil { + t.Fatal(err) + } + if test.wantCode != 0 { + if got.Error == nil || got.Error.Code != test.wantCode { + t.Fatalf("error = %+v, want %s", got.Error, test.wantCode) + } + } else if got.Effective == nil { + t.Fatalf("selection failed: %+v", got.Error) + } + if got.NoActiveReason != test.wantReason { + t.Fatalf("no-active reason = %q, want %q", got.NoActiveReason, test.wantReason) + } + if len(got.Attempts) != len(test.wantTry) { + t.Fatalf("attempts = %+v", got.Attempts) + } + for index, want := range test.wantTry { + if got.Attempts[index].Context != want { + t.Fatalf("attempt %d = %s, want %s", index, got.Attempts[index].Context, want) + } + } + }) + } +} + +func active(token TokenFacts) SessionCandidate { + return SessionCandidate{SessionID: 1, Console: true, UserSID: "S-1-5-21-1", LogonSID: "S-1-5-5-1-2", Token: token} +} + +func ambiguousSessions(token TokenFacts) []SessionCandidate { + first := active(token) + first.Console = false + second := active(token) + second.Console = false + second.SessionID = 2 + second.UserSID = "S-1-5-21-2" + second.LogonSID = "S-1-5-5-2-3" + return []SessionCandidate{first, second} +} diff --git a/test/coverage.toml b/test/coverage.toml index c0e071b..3145b6e 100644 --- a/test/coverage.toml +++ b/test/coverage.toml @@ -104,7 +104,16 @@ tests = ["internal/domain/sequence_test.go:TestEventSequenceAndDuplicateEquivale id = "HP-WINCTX-01" layer = "unit" status = "implemented" -tests = ["internal/domain/errors_test.go:TestWindowsPrelaunchErrorCodes_HP_WINCTX_01"] +tests = [ + "internal/domain/errors_test.go:TestWindowsPrelaunchErrorCodes_HP_WINCTX_01", + "internal/client/supervisor/windows/selection_test.go:TestSelectExecutionContext_HP_WINCTX_02", +] + +[[requirements]] +id = "BH-WINCTX-02" +layer = "unit" +status = "implemented" +tests = ["internal/client/supervisor/windows/selection_test.go:TestSelectExecutionContextBoundaries_BH_WINCTX_02"] [[requirements]] id = "BH-SES-01"