Skip to content

Commit

Permalink
rough exec implementation of presence detection
Browse files Browse the repository at this point in the history
  • Loading branch information
James-Pickett committed Sep 17, 2024
1 parent 97f4287 commit 566d5bd
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 11 deletions.
48 changes: 48 additions & 0 deletions cmd/launcher/detect_presence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main

import (
"encoding/base64"
"encoding/json"
"errors"
"os"
"runtime"

"github.com/kolide/launcher/ee/presencedetection"
"github.com/kolide/launcher/pkg/log/multislogger"
)

func runDetectPresence(_ *multislogger.MultiSlogger, args []string) error {
reason := ""

if len(args) != 0 {
reason = args[0]
}

if reason == "" && runtime.GOOS == "darwin" {
return errors.New("reason is required on darwin")
}

success, err := presencedetection.Detect(reason)
response := presencedetection.PresenceDetectionResponse{
Success: success,
}
if err != nil {
response.Error = err.Error()
}

// serialize response to JSON
responseJSON, err := json.Marshal(response)
if err != nil {
return err
}

// b64 enode response
responseB64 := base64.StdEncoding.EncodeToString(responseJSON)

// write response to stdout
if _, err := os.Stdout.Write([]byte(responseB64)); err != nil {
return err
}

return nil
}
2 changes: 2 additions & 0 deletions cmd/launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ func runSubcommands(systemMultiSlogger *multislogger.MultiSlogger) error {
run = runSecureEnclave
case "watchdog": // note: this is currently only implemented for windows
run = watchdog.RunWatchdogService
case "detect-presence":
run = runDetectPresence
default:
return fmt.Errorf("unknown subcommand %s", os.Args[1])
}
Expand Down
33 changes: 30 additions & 3 deletions ee/localserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/kolide/krypto/pkg/echelper"
"github.com/kolide/launcher/ee/agent"
"github.com/kolide/launcher/ee/agent/types"
"github.com/kolide/launcher/ee/presencedetection"
"github.com/kolide/launcher/pkg/osquery"
"github.com/kolide/launcher/pkg/traces"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
Expand Down Expand Up @@ -56,6 +57,8 @@ type localServer struct {

serverKey *rsa.PublicKey
serverEcKey *ecdsa.PublicKey

presenceDetector presencedetection.PresenceDetector
}

const (
Expand Down Expand Up @@ -121,9 +124,15 @@ func New(ctx context.Context, k types.Knapsack) (*localServer, error) {
// mux.Handle("/acceleratecontrol", ls.requestAccelerateControlHandler())

srv := &http.Server{
Handler: otelhttp.NewHandler(ls.requestLoggingHandler(ls.preflightCorsHandler(ls.rateLimitHandler(mux))), "localserver", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return r.URL.Path
})),
Handler: otelhttp.NewHandler(
ls.requestLoggingHandler(
ls.preflightCorsHandler(
ls.rateLimitHandler(
ls.presenceDetectionHandler(mux),
),
)), "localserver", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return r.URL.Path
})),
ReadTimeout: 500 * time.Millisecond,
ReadHeaderTimeout: 50 * time.Millisecond,
// WriteTimeout very high due to retry logic in the scheduledquery endpoint
Expand Down Expand Up @@ -393,3 +402,21 @@ func (ls *localServer) rateLimitHandler(next http.Handler) http.Handler {
next.ServeHTTP(w, r)
})
}

func (ls *localServer) presenceDetectionHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

success, err := ls.presenceDetector.DetectForConsoleUser("sign you into a thing...", 1*time.Minute)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

if !success {
http.Error(w, "presence detection failed", http.StatusInternalServerError)
return
}

next.ServeHTTP(w, r)
})
}
116 changes: 116 additions & 0 deletions ee/presencedetection/presencedetection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package presencedetection

import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"strings"
"sync"
"time"

"github.com/kolide/launcher/ee/consoleuser"
)

type PresenceDetectionResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}

type PresenceDetector struct {
lastDetectionUTC time.Time
mutext sync.Mutex
}

func (pd *PresenceDetector) DetectForConsoleUser(reason string, detectionInterval time.Duration) (bool, error) {
pd.mutext.Lock()
defer pd.mutext.Unlock()

// Check if the last detection was within the detection interval
if time.Since(pd.lastDetectionUTC) < detectionInterval {
return true, nil
}

executablePath, err := os.Executable()
if err != nil {
return false, fmt.Errorf("could not get executable path: %w", err)
}

consoleUserUids, err := consoleuser.CurrentUids(context.TODO())
if err != nil {
return false, fmt.Errorf("could not get console user: %w", err)
}

if len(consoleUserUids) == 0 {
return false, errors.New("no console user found")
}

runningUserUid := consoleUserUids[0]

// Ensure that we handle a non-root current user appropriately
currentUser, err := user.Current()
if err != nil {
return false, fmt.Errorf("getting current user: %w", err)
}

runningUser, err := user.LookupId(runningUserUid)
if err != nil || runningUser == nil {
return false, fmt.Errorf("looking up user with uid %s: %w", runningUserUid, err)
}

cmd := exec.Command(executablePath, "detect-presence", reason) //nolint:forbidigo // We trust that the launcher executable path is correct, so we don't need to use allowedcmd

// Update command so that we're prepending `launchctl asuser $UID sudo --preserve-env -u $runningUser` to the launcher desktop command.
// We need to run with `launchctl asuser` in order to get the user context, which is required to be able to send notifications.
// We need `sudo -u $runningUser` to set the UID on the command correctly -- necessary for, among other things, correctly observing
// light vs dark mode.
// We need --preserve-env for sudo in order to avoid clearing SOCKET_PATH, AUTHTOKEN, etc that are necessary for the desktop
// process to run.
cmd.Path = "/bin/launchctl"
updatedCmdArgs := append([]string{"/bin/launchctl", "asuser", runningUserUid, "sudo", "--preserve-env", "-u", runningUser.Username}, cmd.Args...)
cmd.Args = updatedCmdArgs

if currentUser.Uid != "0" && currentUser.Uid != runningUserUid {
// if the user is running for another user, we have an error because we can't set credentials
return false, fmt.Errorf("current user %s is not root and does not match running user, can't start process for other user %s", currentUser.Uid, runningUserUid)
}

out, err := cmd.CombinedOutput()
if err != nil {
return false, fmt.Errorf("could not run command: %w", err)
}

outStr := string(out)

// get last line of outstr
lastLine := ""
for _, line := range strings.Split(outStr, "\n") {
if line != "" {
lastLine = line
}
}

outDecoded, err := base64.StdEncoding.DecodeString(lastLine)
if err != nil {
return false, fmt.Errorf("could not decode output: %w", err)
}

response := PresenceDetectionResponse{}
if err := json.Unmarshal(outDecoded, &response); err != nil {
return false, fmt.Errorf("could not unmarshal response: %w", err)
}

if response.Success {
pd.lastDetectionUTC = time.Now().UTC()
}

if response.Error != "" {
return response.Success, errors.New(response.Error)
}

return response.Success, nil
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package presence
//go:build darwin
// +build darwin

package presencedetection

/*
#cgo CFLAGS: -x objective-c -fmodules -fblocks
Expand Down Expand Up @@ -81,7 +84,7 @@ import (
"unsafe"
)

func detect(reason string) (bool, error) {
func Detect(reason string) (bool, error) {
reasonStr := C.CString(reason)
defer C.free(unsafe.Pointer(reasonStr))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package presence
package presencedetection

import (
"os"
Expand All @@ -15,24 +15,24 @@ const testPresenceEnvVar = "launcher_test_presence"

// To test this run
//
// launcher_test_presence=true go test ./ee/presence/ -run Test_detectSuccess
// launcher_test_presence=true go test ./ee/presencedetection/ -run Test_detectSuccess
//
// then successfully auth with the pop up
func Test_detectSuccess(t *testing.T) {
t.Parallel()

if os.Getenv(testPresenceEnvVar) == "" {
t.Skip("Skipping test_biometricDetectSuccess")
t.Skip("Skipping Test_detectSuccess")
}

success, err := detect("IS TRYING TO TEST SUCCESS, PLEASE AUTHENTICATE")
success, err := Detect("IS TRYING TO TEST SUCCESS, PLEASE AUTHENTICATE")
require.NoError(t, err, "should not get an error on successful detect")
assert.True(t, success, "should be successful")
}

// To test this run
//
// launcher_test_presence=true go test ./ee/presence/ -run Test_detectCancel
// launcher_test_presence=true go test ./ee/presencedetection/ -run Test_detectCancel
//
// then cancel the biometric auth that pops up
func Test_detectCancel(t *testing.T) {
Expand All @@ -42,7 +42,7 @@ func Test_detectCancel(t *testing.T) {
t.Skip("Skipping test_biometricDetectCancel")
}

success, err := detect("IS TRYING TO TEST CANCEL, PLEASE PRESS CANCEL")
success, err := Detect("IS TRYING TO TEST CANCEL, PLEASE PRESS CANCEL")
require.Error(t, err, "should get an error on failed detect")
assert.False(t, success, "should not be successful")
}
11 changes: 11 additions & 0 deletions ee/presencedetection/presencedetection_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build !darwin
// +build !darwin

package presencedetection

import "errors"

func Detect(reason string) (bool, error) {
// Implement detection logic for non-Darwin platforms
return false, errors.New("detection not implemented for this platform")
}

0 comments on commit 566d5bd

Please sign in to comment.