mydev/internal/checks/auth.go
Victor Abrell 7140397a13 Initial commit: MyDev dev-environment TUI
CLI + live dashboard wrapping aws/docker/git/awsmfa for local dev:
AWS MFA auth status + expiry, dev compose stack control, container
drift vs the dev repo, git repo status, prod-data and DB-access tools.
Config-driven (~/.config/mydev/config.yaml). Dashboard runs commands
via a command palette with captured/interactive modes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:31:58 +02:00

84 lines
2.3 KiB
Go

package checks
import (
"context"
"encoding/json"
"os/exec"
"strings"
"time"
)
// AuthStatus holds the result of an AWS auth check via the awsmfa tool.
type AuthStatus struct {
Authed bool
Account string
Expires time.Time // zero if unknown; session expiry from awsmfa
Detail string // human summary line
Err string // populated when awsmfa itself failed to run
}
// notAuthedMarker is the exact string awsmfa prints when the session is gone.
const notAuthedMarker = "Not authenticated."
// AWSAuth runs `<awsmfa> status` and decides auth state from its output.
// This mirrors the shell `myd auth` check — awsmfa is the real MFA session,
// not `aws sts`. awsmfaPath is the full path to the awsmfa command.
//
// Authed output looks like:
//
// Success.
// { "UserId": ..., "Account": "0482...", "Arn": ... }
// Expire: 2026-06-12T19:25:19+00:00
func AWSAuth(ctx context.Context, awsmfaPath string) AuthStatus {
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, awsmfaPath, "status").CombinedOutput()
text := strings.TrimSpace(string(out))
if err != nil && text == "" {
return AuthStatus{Authed: false, Err: err.Error()}
}
if strings.Contains(text, notAuthedMarker) {
return AuthStatus{Authed: false, Detail: notAuthedMarker}
}
s := AuthStatus{Authed: true}
// Account: decode the JSON identity block. Use a Decoder so the trailing
// "Expire:" line after the closing brace doesn't fail parsing.
if i := strings.IndexByte(text, '{'); i >= 0 {
var id struct {
Account string `json:"Account"`
}
if json.NewDecoder(strings.NewReader(text[i:])).Decode(&id) == nil {
s.Account = id.Account
}
}
// Expiry: the "Expire: <RFC3339>" line.
for _, line := range strings.Split(text, "\n") {
if rest, ok := strings.CutPrefix(strings.TrimSpace(line), "Expire:"); ok {
if t, perr := time.Parse(time.RFC3339, strings.TrimSpace(rest)); perr == nil {
s.Expires = t
}
}
}
s.Detail = summarize(s.Account, s.Expires)
return s
}
func summarize(account string, expires time.Time) string {
var parts []string
if account != "" {
parts = append(parts, "account "+account)
}
if !expires.IsZero() {
parts = append(parts, "expires "+expires.Local().Format("15:04"))
}
if len(parts) == 0 {
return "authed"
}
return strings.Join(parts, " · ")
}