Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions internal/appui/setup_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,18 @@ func RunSetup(c *cli.Context) error {
return setupError(slog, err)
}

fmt.Printf(" %s%sStep 1/2%s 🔑 Authenticate with Hexmos\n", clr(cBold), clr(cBlue), clr(cReset))
fmt.Println()
slog.write("phase 1: starting hexmos login flow")

result, err = runHexmosLoginFlow(slog, selectedAPIURL)
if setuptpl.IsCloudAPIURL(selectedAPIURL) {
fmt.Printf(" %s%sStep 1/2%s 🔑 Authenticate with Hexmos\n", clr(cBold), clr(cBlue), clr(cReset))
fmt.Println()
slog.write("phase 1: starting hexmos login flow")
result, err = runHexmosLoginFlow(slog, selectedAPIURL)
} else {
fmt.Printf(" %s%sStep 1/2%s 🔑 Sign in to LiveReview\n", clr(cBold), clr(cBlue), clr(cReset))
fmt.Printf(" %sUsing self-hosted authentication at %s%s\n", clr(cDim), selectedAPIURL, clr(cReset))
fmt.Println()
slog.write("phase 1: starting self-hosted login flow for api_url=%s", selectedAPIURL)
result, err = promptSelfHostedLoginFlow(selectedAPIURL, slog)
}
if err != nil {
return setupError(slog, fmt.Errorf("authentication failed: %w", err))
}
Expand Down
136 changes: 136 additions & 0 deletions internal/appui/setup_selfhosted.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package appui

import (
"bufio"
"fmt"
"net/mail"
"os"
"strings"
"syscall"

setuptpl "github.com/HexmosTech/git-lrc/setup"
"golang.org/x/term"
)

func promptSelfHostedLoginFlow(apiURL string, slog *setupLog) (*setupResult, error) {
setupRequired, err := setuptpl.CheckSelfHostedSetupRequired(apiURL)
if err != nil {
return nil, fmt.Errorf("failed to inspect self-hosted setup status: %w", err)
}

reader := bufio.NewReader(os.Stdin)


if setupRequired {
fmt.Printf(" %sNo LiveReview users found on this server.%s\n", clr(cYellow), clr(cReset))
fmt.Printf(" %sCreate the first admin account to continue.%s\n", clr(cDim), clr(cReset))
fmt.Println()

admin, err := promptSelfHostedInitialAdmin(reader)
if err != nil {
return nil, err
}
slog.write("initial self-hosted admin setup for email=%s org=%s", admin.Email, admin.OrgName)
return setuptpl.ProvisionSelfHostedUser(apiURL, setuptpl.SelfHostedLoginRequest{}, admin, slog.write)
}

creds, err := promptSelfHostedCredentials(reader)
if err != nil {
return nil, err
}
slog.write("self-hosted login for email=%s", creds.Email)
return setuptpl.ProvisionSelfHostedUser(apiURL, creds, nil, slog.write)
}

func promptSelfHostedCredentials(reader *bufio.Reader) (setuptpl.SelfHostedLoginRequest, error) {
email, err := promptEmail(reader, "Email")
if err != nil {
return setuptpl.SelfHostedLoginRequest{}, err
}
password, err := promptSetupPassword(reader, "Password")
if err != nil {
return setuptpl.SelfHostedLoginRequest{}, err
}
return setuptpl.SelfHostedLoginRequest{
Email: email,
Password: password,
}, nil
}

func promptSelfHostedInitialAdmin(reader *bufio.Reader) (*setuptpl.SelfHostedSetupAdminRequest, error) {
email, err := promptEmail(reader, "Admin email")
if err != nil {
return nil, err
}
password, err := promptSetupPassword(reader, "Admin password")
if err != nil {
return nil, err
}
if len(password) < 8 {
return nil, fmt.Errorf("password must be at least 8 characters")
}
orgName, err := promptSetupLine(reader, "Organization name")
if err != nil {
return nil, err
}
return &setuptpl.SelfHostedSetupAdminRequest{
Email: email,
Password: password,
OrgName: orgName,
}, nil
}

func promptSetupLine(reader *bufio.Reader, label string) (string, error) {
fmt.Printf(" %s%s:%s ", clr(cBold), label, clr(cReset))
line, err := reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("failed to read %s: %w", strings.ToLower(label), err)
}
value := strings.TrimSpace(line)
if value == "" {
return "", fmt.Errorf("%s cannot be empty", strings.ToLower(label))
}
return value, nil
}

func promptSetupPassword(reader *bufio.Reader, label string) (string, error) {
fmt.Printf(" %s%s:%s ", clr(cBold), label, clr(cReset))
if !term.IsTerminal(int(syscall.Stdin)) {
line, err := reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("failed to read %s: %w", strings.ToLower(label), err)
}
value := strings.TrimSpace(line)
if value == "" {
return "", fmt.Errorf("%s cannot be empty", strings.ToLower(label))
}
return value, nil
}

password, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()
if err != nil {
return "", fmt.Errorf("failed to read %s: %w", strings.ToLower(label), err)
}
value := strings.TrimSpace(string(password))
if value == "" {
return "", fmt.Errorf("%s cannot be empty", strings.ToLower(label))
}
return value, nil
}


func promptEmail(reader *bufio.Reader, label string) (string, error) {
email, err := promptSetupLine(reader, label)
if err != nil {
return "", err
}

if _, err := mail.ParseAddress(email); err != nil {
return "", fmt.Errorf("email must be a valid email address")
}

return email, nil
}


9 changes: 8 additions & 1 deletion internal/appui/ui_connectors_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/HexmosTech/git-lrc/internal/reviewopts"
"github.com/HexmosTech/git-lrc/internal/staticserve"
"github.com/HexmosTech/git-lrc/network"
setuptpl "github.com/HexmosTech/git-lrc/setup"
"github.com/HexmosTech/git-lrc/storage"
uicfg "github.com/HexmosTech/git-lrc/ui"
)
Expand Down Expand Up @@ -182,7 +183,13 @@ func (s *connectorManagerServer) handleReauthenticate(w http.ResponseWriter, r *
}

slog := newSetupLog()
result, err := runHexmosLoginFlow(slog, apiURL)
var result *setupResult
var err error
if setuptpl.IsCloudAPIURL(apiURL) {
result, err = runHexmosLoginFlow(slog, apiURL)
} else {
result, err = promptSelfHostedLoginFlow(apiURL, slog)
}
if err != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("reauthentication failed: %v", err))
return
Expand Down
16 changes: 16 additions & 0 deletions network/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ func SetupEnsureCloudUserURL(baseURL string) string {
return strings.TrimSuffix(baseURL, "/") + "/api/v1/auth/ensure-cloud-user"
}

func SetupAuthLoginURL(baseURL string) string {
return strings.TrimSuffix(baseURL, "/") + "/api/v1/auth/login"
}

func SetupUIConfigURL(baseURL string) string {
return strings.TrimSuffix(baseURL, "/") + "/api/v1/ui-config"
}

func SetupAuthSetupStatusURL(baseURL string) string {
return strings.TrimSuffix(baseURL, "/") + "/api/v1/auth/setup-status"
}

func SetupAuthSetupURL(baseURL string) string {
return strings.TrimSuffix(baseURL, "/") + "/api/v1/auth/setup"
}

func SetupAuthRefreshURL(baseURL string) string {
return strings.TrimSuffix(baseURL, "/") + "/api/v1/auth/refresh"
}
Expand Down
22 changes: 15 additions & 7 deletions network/network_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ This document tracks network-side operations in git-lrc as an auditable inventor

- Network boundary: outbound HTTP API operations and response handling in network package.
- Modes represented: api.
- Operation count tracked: 19 operations.
- Operation count tracked: 22 operations.
- Severity distribution: High 10, Medium 7, Low 2.
- Current diff note: worktree hook-runtime and local hook-management fixes changed hook/appcore/git-path helper logic only; no network package operation surface change.
- Current diff note: self-hosted setup now uses LiveReview email/password auth endpoints (`/api/v1/auth/login`, `/api/v1/auth/setup-status`, `/api/v1/auth/setup`) in addition to existing cloud ensure-cloud-user setup path.
- Current diff note: internal reviewapi helper evidence links were revalidated after git path helper additions; network inventory scope is unchanged.
- Primary sensitive data in scope: API keys, bearer tokens, org-context headers, diff content, connector validation payloads, update manifest metadata, binary download stream.
- Highest-risk operation classes: review submission/polling, setup credential operations, proxy forwarding paths, binary download operations.
Expand Down Expand Up @@ -44,11 +44,15 @@ This document tracks network-side operations in git-lrc as an auditable inventor
| Operation | Mode | Data Handled | Purpose | Severity | Risk Acknowledgement | Compensation Status | Evidence |
| --- | --- | --- | --- | --- | --- | --- | --- |
| SetupEnsureCloudUser | api | Access-token-authenticated identity payload | Ensure cloud user record exists during setup | High | Authentication-context misuse risk | Compensated by bearer token boundary and scoped setup endpoint; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L6) |
| SetupRefreshTokens | api | Refresh token request payload and returned token set | Refresh auth session tokens during setup and re-auth flows | High | Token handling and session integrity risk | Compensated by scoped auth-refresh endpoint and typed request path in setup operations; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L11) |
| SetupCreateAPIKey | api | API key label and returned plaintext API key | Create connector/review API key | High | High sensitivity secret material exposure risk | Compensated by explicit auth flow and setup caller no-log/no-echo handling for create-key error paths; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L16) |
| SetupValidateConnectorKey | api | Provider key and validation request body | Validate AI connector key before persistence | High | Third-party key exposure/handling risk | Compensated by authenticated request path plus connector key redaction in setup error surfaces; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L21) |
| SetupCreateConnector | api | Connector configuration payload | Persist connector configuration via LiveReview API | High | Misconfiguration and sensitive metadata transmission risk | Compensated by bearer auth plus org context boundary; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L26) |
| SetupListConnectors | api | Existing connector inventory for current org | Inspect minimum AI readiness during setup and re-auth preflight | High | Sensitive connector metadata and auth-context exposure risk | Compensated by bearer auth plus org context boundary; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L31) |
| SetupAuthLogin | api | Email/password credentials and returned session tokens | Authenticate against self-hosted LiveReview during setup | High | Credential and token handling risk | Compensated by HTTPS/TLS deployment assumptions and setup caller no-log handling for secrets; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L11) |
| SetupUIConfig | api | Base URL | Check if the target server is cloud hosted | Low | Low sensitivity configuration info | Compensated by public endpoint without secrets; acceptable risk | [network/setup_operations.go](setup_operations.go#L16) |
| SetupAuthSetupStatus | api | Setup readiness metadata | Detect whether initial self-hosted admin setup is required | Medium | Low sensitivity operational metadata exposure risk | Compensated by public setup-status endpoint contract; acceptable risk | [network/setup_operations.go](setup_operations.go#L21) |
| SetupInitialAdmin | api | Initial admin credentials and returned session tokens | Create first self-hosted admin user during setup | High | Credential and token handling risk | Compensated by one-time setup gate on backend and setup caller secret-handling policy; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L26) |
| SetupRefreshTokens | api | Refresh token request payload and returned token set | Refresh auth session tokens during setup and re-auth flows | High | Token handling and session integrity risk | Compensated by scoped auth-refresh endpoint and typed request path in setup operations; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L31) |
| SetupCreateAPIKey | api | API key label and returned plaintext API key | Create connector/review API key | High | High sensitivity secret material exposure risk | Compensated by explicit auth flow and setup caller no-log/no-echo handling for create-key error paths; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L36) |
| SetupValidateConnectorKey | api | Provider key and validation request body | Validate AI connector key before persistence | High | Third-party key exposure/handling risk | Compensated by authenticated request path plus connector key redaction in setup error surfaces; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L41) |
| SetupCreateConnector | api | Connector configuration payload | Persist connector configuration via LiveReview API | High | Misconfiguration and sensitive metadata transmission risk | Compensated by bearer auth plus org context boundary; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L46) |
| SetupListConnectors | api | Existing connector inventory for current org | Inspect minimum AI readiness during setup and re-auth preflight | High | Sensitive connector metadata and auth-context exposure risk | Compensated by bearer auth plus org context boundary; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L51) |

## Inventory: Proxy And Forwarding APIs

Expand All @@ -72,6 +76,10 @@ This document tracks network-side operations in git-lrc as an auditable inventor
| Client.DoJSON | api | Request/response JSON payload bytes | Standard JSON HTTP call wrapper | Medium | Medium risk from broad transport usage and status-handling variance | Compensated by centralized transport wrapper with timeout controls; acceptable risk | [network/http_client.go](http_client.go#L43) |
| Client.Do | api | Raw HTTP request/response bytes | Generic HTTP call wrapper for non-JSON/raw workflows | Medium | Medium risk from raw payload handling flexibility | Partially compensated by shared client boundary; Suggestion: document callsite expectations for raw bodies | [network/http_client.go](http_client.go#L89) |
| SetupEnsureCloudUserURL | api | Base URL plus endpoint normalization inputs | Normalize endpoint composition and reduce path ambiguity | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L13) |
| SetupAuthLoginURL | api | Base URL plus endpoint normalization inputs | Build self-hosted login endpoint URL | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L17) |
| SetupUIConfigURL | api | Base URL | Build UI config URL | Low | Low risk | Compensated by public endpoint utility; acceptable risk | [network/endpoints.go](endpoints.go#L21) |
| SetupAuthSetupStatusURL | api | Base URL plus endpoint normalization inputs | Build self-hosted setup-status endpoint URL | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L25) |
| SetupAuthSetupURL | api | Base URL plus endpoint normalization inputs | Build self-hosted initial-admin setup endpoint URL | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L29) |
| PollReview | api | Review IDs, status payloads, timeout state | Timeout-bounded polling orchestration in review runtime | High | High availability/latency risk if review service is degraded | Compensated by bounded timeout and interval controls; residual risk acceptable | [internal/reviewapi/helpers.go](../internal/reviewapi/helpers.go#L201) |
| formatJSONParseError | api | Response body text for parse diagnostics | Improve operator diagnostics when endpoint/port mismatches occur | Low | Low risk diagnostic utility behavior | Compensated by safer error interpretation path; acceptable risk | [internal/reviewapi/helpers.go](../internal/reviewapi/helpers.go#L129) |

Expand Down
20 changes: 20 additions & 0 deletions network/setup_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ func SetupEnsureCloudUser(client *Client, cloudBase string, payload any, jwt str
return client.DoJSON(http.MethodPost, SetupEnsureCloudUserURL(cloudBase), payload, jwt, "", nil)
}

// SetupAuthLogin submits the self-hosted email/password login request.
func SetupAuthLogin(client *Client, baseURL string, payload any) (*Response, error) {
return client.DoJSON(http.MethodPost, SetupAuthLoginURL(baseURL), payload, "", "", nil)
}

// SetupUIConfig retrieves the deployment configuration to check the server mode.
func SetupUIConfig(client *Client, baseURL string) (*Response, error) {
return client.DoJSON(http.MethodGet, SetupUIConfigURL(baseURL), nil, "", "", nil)
}

// SetupAuthSetupStatus checks whether initial self-hosted admin setup is required.
func SetupAuthSetupStatus(client *Client, baseURL string) (*Response, error) {
return client.DoJSON(http.MethodGet, SetupAuthSetupStatusURL(baseURL), nil, "", "", nil)
}

// SetupInitialAdmin submits the first-time self-hosted admin setup request.
func SetupInitialAdmin(client *Client, baseURL string, payload any) (*Response, error) {
return client.DoJSON(http.MethodPost, SetupAuthSetupURL(baseURL), payload, "", "", nil)
}

// SetupRefreshTokens submits the auth refresh request.
func SetupRefreshTokens(client *Client, cloudBase string, payload any) (*Response, error) {
return client.DoJSON(http.MethodPost, SetupAuthRefreshURL(cloudBase), payload, "", "", nil)
Expand Down
30 changes: 30 additions & 0 deletions setup/deployment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package setup

import (
"encoding/json"
"strings"
"time"

"github.com/HexmosTech/git-lrc/network"
)

// IsCloudAPIURL reports whether apiURL targets the hosted LiveReview cloud backend.
// Empty apiURL is treated as cloud because setup defaults to CloudAPIURL.
func IsCloudAPIURL(apiURL string) bool {
normalized := strings.TrimRight(strings.TrimSpace(apiURL), "/")
if normalized == "" {
return true
}

client := network.NewSetupClient(5 * time.Second)
if resp, err := network.SetupUIConfig(client, normalized); err == nil && resp.StatusCode == 200 {
var uiConfig struct {
IsCloud bool `json:"isCloud"`
}
if err := json.Unmarshal(resp.Body, &uiConfig); err == nil {
return uiConfig.IsCloud
}
}

return strings.EqualFold(normalized, CloudAPIURL) || strings.Contains(normalized, "hexmos.com")
}
22 changes: 22 additions & 0 deletions setup/deployment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package setup

import "testing"

func TestIsCloudAPIURL(t *testing.T) {
tests := []struct {
apiURL string
want bool
}{
{apiURL: "", want: true},
{apiURL: CloudAPIURL, want: true},
{apiURL: CloudAPIURL + "/", want: true},
{apiURL: "http://localhost:8888", want: false},
{apiURL: "https://review.acme.corp", want: false},
}

for _, tc := range tests {
if got := IsCloudAPIURL(tc.apiURL); got != tc.want {
t.Fatalf("IsCloudAPIURL(%q) = %v, want %v", tc.apiURL, got, tc.want)
}
}
}
21 changes: 3 additions & 18 deletions setup/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,26 +67,11 @@ func ProvisionLiveReviewUser(cbData *HexmosCallbackData, apiURL string, logf fun
}
}

apiKeyReq := CreateAPIKeyRequest{Label: "LRC CLI Key"}
apiKeyURL := network.SetupCreateAPIKeyURL(apiURL, result.OrgID)
log("creating API key: POST %s", apiKeyURL)
resp2, err := network.SetupCreateAPIKey(client, apiURL, result.OrgID, apiKeyReq, result.AccessToken)
plainKey, err := createSetupAPIKey(client, apiURL, result.OrgID, result.AccessToken, log)
if err != nil {
return nil, fmt.Errorf("failed to create API key: %w", err)
}
if resp2.StatusCode != http.StatusCreated && resp2.StatusCode != http.StatusOK {
// Do not log/echo response bodies here because create-key responses can contain plaintext secrets.
log("create API key failed: status=%d", resp2.StatusCode)
return nil, fmt.Errorf("create API key returned %d", resp2.StatusCode)
}

log("API key created: status=%d", resp2.StatusCode)

var apiKeyResp CreateAPIKeyResponse
if err := json.Unmarshal(resp2.Body, &apiKeyResp); err != nil {
return nil, fmt.Errorf("failed to parse API key response: %w", err)
return nil, err
}

result.PlainAPIKey = apiKeyResp.PlainKey
result.PlainAPIKey = plainKey
return result, nil
}
Loading
Loading