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
38 changes: 21 additions & 17 deletions internal/cmd/auth_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func runAuthSetup(cmd *cobra.Command, _ []string) error {
// The --config path is for scripting (MDM, CI): the caller supplies the whole
// configuration as one document, so we don't pre-flight or prompt.
if setupConfigInput != "" {
return runConfigFileSetup(ctx, client)
return runConfigFileSetup(ctx, client, detectedTenant)
}
if !cli.IsInteractive() {
return fmt.Errorf("no configuration provided: pass --config (a file, '-' for stdin, or inline JSON), or run in an interactive terminal")
Expand Down Expand Up @@ -160,7 +160,7 @@ func withRequestTimeout(ctx context.Context) (context.Context, context.CancelFun

// runConfigFileSetup handles the non-interactive --config path: load the full
// configuration from JSON and create it (or update it wholesale with --update).
func runConfigFileSetup(ctx context.Context, client *auth.IdpConfigClient) error {
func runConfigFileSetup(ctx context.Context, client *auth.IdpConfigClient, detectedTenant string) error {
reqCfg, err := loadIdpConfigFromJSON(setupConfigInput)
if err != nil {
return err
Expand Down Expand Up @@ -190,9 +190,9 @@ func runConfigFileSetup(ctx context.Context, client *auth.IdpConfigClient) error
}

if setupUpdate {
return sendUpdate(ctx, client, reqCfg)
return sendUpdate(ctx, client, reqCfg, detectedTenant)
}
return sendCreate(ctx, client, reqCfg)
return sendCreate(ctx, client, reqCfg, detectedTenant)
}

// runInteractiveSetup guides an admin through setup. It first resolves the tenant
Expand Down Expand Up @@ -221,14 +221,14 @@ func runInteractiveSetup(ctx context.Context, client *auth.IdpConfigClient, dete
}

if existing != nil {
return runInteractiveUpdate(ctx, client, existing)
return runInteractiveUpdate(ctx, client, existing, detectedTenant)
}
return runInteractiveCreate(ctx, client, tenant)
return runInteractiveCreate(ctx, client, tenant, detectedTenant)
}

// runInteractiveCreate collects a full configuration for a tenant that has none
// yet and creates it.
func runInteractiveCreate(ctx context.Context, client *auth.IdpConfigClient, tenant string) error {
func runInteractiveCreate(ctx context.Context, client *auth.IdpConfigClient, tenant, detectedTenant string) error {
reqCfg, err := promptIdpConfig(tenant)
if err != nil {
return err
Expand All @@ -250,12 +250,12 @@ func runInteractiveCreate(ctx context.Context, client *auth.IdpConfigClient, ten
return nil
}
}
return sendCreate(ctx, client, reqCfg)
return sendCreate(ctx, client, reqCfg, detectedTenant)
}

// runInteractiveUpdate shows a form pre-filled from the existing configuration and
// PUTs only the fields the admin changed, relying on the backend to merge them.
func runInteractiveUpdate(ctx context.Context, client *auth.IdpConfigClient, existing *auth.IdpConfigResponse) error {
func runInteractiveUpdate(ctx context.Context, client *auth.IdpConfigClient, existing *auth.IdpConfigResponse, detectedTenant string) error {
fmt.Fprintf(os.Stderr, "%s Found an existing IdP configuration for tenant %q — editing it.\n",
output.IconPointer, existing.TenantID)
fmt.Fprintln(os.Stderr, " Leave a field unchanged to keep its current value; only edits are sent.")
Expand Down Expand Up @@ -287,7 +287,7 @@ func runInteractiveUpdate(ctx context.Context, client *auth.IdpConfigClient, exi
return describeIdpConfigError(err)
}
fmt.Fprintf(os.Stderr, "%s IdP configuration updated for tenant %q.\n", output.IconSuccess, existing.TenantID)
printPostSetupHint(existing.TenantID)
printPostSetupHint(detectedTenant)
return nil
}

Expand All @@ -311,13 +311,13 @@ func fetchExistingConfig(ctx context.Context, client *auth.IdpConfigClient, tena
// sendCreate POSTs a new configuration. On 409 it offers to switch to the update
// (PUT) path interactively; when non-interactive or --yes is set it errors and
// points at --update, so a scripted create never silently overwrites.
func sendCreate(ctx context.Context, client *auth.IdpConfigClient, reqCfg *auth.IdpConfigCreateRequest) error {
func sendCreate(ctx context.Context, client *auth.IdpConfigClient, reqCfg *auth.IdpConfigCreateRequest, detectedTenant string) error {
reqCtx, cancel := withRequestTimeout(ctx)
defer cancel()
_, err := client.Create(reqCtx, reqCfg)
if err == nil {
fmt.Fprintf(os.Stderr, "%s IdP configuration created for tenant %q.\n", output.IconSuccess, reqCfg.TenantID)
printPostSetupHint(reqCfg.TenantID)
printPostSetupHint(detectedTenant)
return nil
}

Expand All @@ -333,7 +333,7 @@ func sendCreate(ctx context.Context, client *auth.IdpConfigClient, reqCfg *auth.
return cerr
}
if ok {
return sendUpdate(ctx, client, reqCfg)
return sendUpdate(ctx, client, reqCfg, detectedTenant)
}
fmt.Fprintln(os.Stderr, " Cancelled — the existing configuration was left unchanged.")
return nil
Expand All @@ -346,7 +346,7 @@ func sendCreate(ctx context.Context, client *auth.IdpConfigClient, reqCfg *auth.

// sendUpdate PUTs the configuration for the tenant. Every field is sent, so an
// update fully replaces the stored values (and rotates the secret).
func sendUpdate(ctx context.Context, client *auth.IdpConfigClient, reqCfg *auth.IdpConfigCreateRequest) error {
func sendUpdate(ctx context.Context, client *auth.IdpConfigClient, reqCfg *auth.IdpConfigCreateRequest, detectedTenant string) error {
upd := &auth.IdpConfigUpdateRequest{
IdpType: setupStrPtr(reqCfg.IdpType),
Issuer: setupStrPtr(reqCfg.Issuer),
Expand All @@ -363,7 +363,7 @@ func sendUpdate(ctx context.Context, client *auth.IdpConfigClient, reqCfg *auth.
return describeIdpConfigError(err)
}
fmt.Fprintf(os.Stderr, "%s IdP configuration updated for tenant %q.\n", output.IconSuccess, reqCfg.TenantID)
printPostSetupHint(reqCfg.TenantID)
printPostSetupHint(detectedTenant)
return nil
}

Expand Down Expand Up @@ -398,9 +398,13 @@ func detailOr(ce *auth.IdpConfigError, fallback string) string {
// printPostSetupHint points the admin at the next step once a config exists:
// the environment variables to deploy to developer machines (via MDM). It prints
// ARMIS_REGION only for a non-default region, matching the deployment guide.
func printPostSetupHint(tenantID string) {
//
// detectedTenantID must be the credential-derived tenant ID (provider.GetTenantID(),
// e.g. from the JWT customer_id claim), not the IdP config's tenant slug — the two
// often differ, and ARMIS_TENANT_ID / `auth login` require the former.
func printPostSetupHint(detectedTenantID string) {
fmt.Fprintln(os.Stderr, " Deploy these environment variables to developer machines (e.g. via MDM):")
fmt.Fprintf(os.Stderr, " ARMIS_TENANT_ID=%s\n", tenantID)
fmt.Fprintf(os.Stderr, " ARMIS_TENANT_ID=%s\n", detectedTenantID)
fmt.Fprintln(os.Stderr, " ARMIS_DEFAULT_AUTH_METHOD=SSO")
if isNonDefaultRegion(setupDetectedRegion) {
fmt.Fprintf(os.Stderr, " ARMIS_REGION=%s\n", setupDetectedRegion)
Expand Down
42 changes: 42 additions & 0 deletions internal/cmd/auth_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,48 @@ func TestAuthSetupConfigFromFile(t *testing.T) {
}
}

// Regression test for PPSC-1230: the post-setup hint must print the
// credential-derived tenant ID (from the admin's own credentials), not the IdP
// config's tenant slug — the two can differ (e.g. a human-readable slug like
// "acme-slug" typed into the form vs. the real tenant ID tied to the admin's
// credentials).
func TestAuthSetupPostSetupHintUsesDetectedTenant(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]any{
"tenant_id": "acme-slug", "idp_type": "okta", "issuer": "https://acme.okta.com",
"oidc_client_id": "client-abc", "group_claim": "groups",
"group_mapping": map[string]string{}, "ema_enabled": false, "enabled": true,
"created_at": "t", "updated_at": "t",
})
}))
defer srv.Close()

setupSetupTest(t, srv.URL) // credential tenant (Basic auth) is "acme"
setupConfigInput = validConfigJSON("acme-slug") // IdP config's tenant slug differs
setupYes = true

old := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
err := runAuthSetup(newCmdWithCtx(), nil)
_ = w.Close()
os.Stderr = old
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
out := buf.String()

if err != nil {
t.Fatalf("runAuthSetup: %v", err)
}
if !strings.Contains(out, "ARMIS_TENANT_ID=acme\n") {
t.Errorf("hint should print the credential-derived tenant ID: %q", out)
}
if strings.Contains(out, "ARMIS_TENANT_ID=acme-slug") {
t.Errorf("hint must not print the IdP config's tenant slug: %q", out)
}
}

// detectIdentity should pull the tenant (customer_id) and region claim straight
// from the client-credentials JWT, so `auth setup` can seed the tenant prompt
// and the post-setup hint without the admin supplying either.
Expand Down
Loading