From d8e1d88ac8dab763770e3827010c46e52ef62c3f Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Fri, 3 May 2024 17:00:02 +0000 Subject: [PATCH 01/11] Test aks --- cli/azd/internal/repository/detect_confirm_apphost.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/internal/repository/detect_confirm_apphost.go b/cli/azd/internal/repository/detect_confirm_apphost.go index be65814cd67..e3f573e99af 100644 --- a/cli/azd/internal/repository/detect_confirm_apphost.go +++ b/cli/azd/internal/repository/detect_confirm_apphost.go @@ -85,7 +85,7 @@ func (d *detectConfirmAppHost) render(ctx context.Context) error { d.console.Message( ctx, "azd will generate the files necessary to host your app on Azure using "+color.MagentaString( - "Azure Container Apps", + "Azure Kubernetes Service", )+".\n", ) From 8d9d3210597710d7cb95184f6d0369874a8e277d Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:52:55 +0000 Subject: [PATCH 02/11] Fix msg --- cli/azd/internal/repository/detect_confirm_apphost.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/internal/repository/detect_confirm_apphost.go b/cli/azd/internal/repository/detect_confirm_apphost.go index e3f573e99af..be65814cd67 100644 --- a/cli/azd/internal/repository/detect_confirm_apphost.go +++ b/cli/azd/internal/repository/detect_confirm_apphost.go @@ -85,7 +85,7 @@ func (d *detectConfirmAppHost) render(ctx context.Context) error { d.console.Message( ctx, "azd will generate the files necessary to host your app on Azure using "+color.MagentaString( - "Azure Kubernetes Service", + "Azure Container Apps", )+".\n", ) From 3c5899aad71311c8d29cadb816d54269a77dfb95 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Tue, 4 Jun 2024 20:34:16 +0000 Subject: [PATCH 03/11] Add local-imds option --- cli/azd/cmd/auth.go | 6 ++ cli/azd/cmd/auth_local_imds.go | 143 +++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 cli/azd/cmd/auth_local_imds.go diff --git a/cli/azd/cmd/auth.go b/cli/azd/cmd/auth.go index 1ee509de863..6790b22251f 100644 --- a/cli/azd/cmd/auth.go +++ b/cli/azd/cmd/auth.go @@ -41,5 +41,11 @@ func authActions(root *actions.ActionDescriptor) *actions.ActionDescriptor { ActionResolver: newLogoutAction, }) + // Register the new `local-imds` command + group.Add("local-imds", &actions.ActionDescriptorOptions{ + Command: newLocalIMDSCmd("auth"), + ActionResolver: newLocalIMDSAction, + }) + return group } diff --git a/cli/azd/cmd/auth_local_imds.go b/cli/azd/cmd/auth_local_imds.go new file mode 100644 index 00000000000..344ac928f8b --- /dev/null +++ b/cli/azd/cmd/auth_local_imds.go @@ -0,0 +1,143 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/azure/azure-dev/cli/azd/cmd/actions" + "github.com/azure/azure-dev/cli/azd/pkg/auth" + "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/pkg/output" + "github.com/spf13/cobra" +) + +// TokenResponse defines the structure of the token response. +type TokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresOn int64 `json:"expires_on"` +} + +// tokenHandler handles token requests. +func (lia *localIMDSAction) tokenHandler(w http.ResponseWriter, r *http.Request) { + resource := r.URL.Query().Get("resource") + if resource == "" { + http.Error(w, "Missing 'resource' query parameter", http.StatusBadRequest) + return + } + + fmt.Printf("Received request for resource: %s\n", resource) + + ctx := context.Background() + var cred azcore.TokenCredential + + cred, err := lia.credentialProvider(ctx, &auth.CredentialForCurrentUserOptions{ + NoPrompt: true, + TenantID: "", + }) + if err != nil { + fmt.Printf("credentialProvider: %v", err) + return + } + + token, err := cred.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: []string{resource + "/.default"}, + }) + if err != nil { + fmt.Printf("fetching token: %v", err) + return + } + + res := TokenResponse{ + AccessToken: token.Token, + ExpiresOn: token.ExpiresOn.Unix(), + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(res); err != nil { + http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) + } +} + +// startIMDSServer starts the IMDS emulator server. +func (lia *localIMDSAction) startIMDSServer(port string) { + http.HandleFunc("/MSI/token", lia.tokenHandler) + http.HandleFunc("/metadata/identity/oauth2/token", lia.tokenHandler) + + srv := &http.Server{ + Addr: ":" + port, + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + go func() { + fmt.Printf("Server started on port %s\n", port) + fmt.Printf("MSI endpoint for local development: http://localhost:%s/MSI/token\n", port) + fmt.Printf("MSI endpoint for Docker: http://host.docker.internal:%s/MSI/token\n", port) + fmt.Println("Set the MSI_ENDPOINT environment variable to the appropriate URL above.") + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("Server stopped: %s\n", err) + } + }() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Printf("Server shutdown failed: %s\n", err) + } + + log.Println("Shutting down") + os.Exit(0) +} + +func newLocalIMDSCmd(parent string) *cobra.Command { + return &cobra.Command{ + Use: "local-imds", + Short: "Starts a local IMDS emulator", + Annotations: map[string]string{ + loginCmdParentAnnotation: parent, + }, + } +} + +type localIMDSAction struct { + console input.Console + credentialProvider CredentialProviderFn + formatter output.Formatter + writer io.Writer +} + +func newLocalIMDSAction( + console input.Console, + credentialProvider CredentialProviderFn, + formatter output.Formatter, + writer io.Writer) actions.Action { + return &localIMDSAction{ + console: console, + credentialProvider: credentialProvider, + formatter: formatter, + writer: writer, + } +} + +func (lia *localIMDSAction) Run(ctx context.Context) (*actions.ActionResult, error) { + port := os.Getenv("IMDS_PORT") + if port == "" { + port = "53028" + } + lia.startIMDSServer(port) + return nil, nil +} From da079b73523d93c22140650fa51fd4d78d672717 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Tue, 4 Jun 2024 20:59:32 +0000 Subject: [PATCH 04/11] Fix cspell --- cli/azd/.vscode/cspell-azd-dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index 2b3a0b5e321..2388dac5fc9 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -128,6 +128,7 @@ grpcserver hotspot ignorefile iidfile +imds ineffassign jaegertracing javac From 1f80477ad85ef250094871c11dee5f028b410a44 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Tue, 4 Jun 2024 21:13:53 +0000 Subject: [PATCH 05/11] Default to management scope --- cli/azd/cmd/auth_local_imds.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/azd/cmd/auth_local_imds.go b/cli/azd/cmd/auth_local_imds.go index 344ac928f8b..1a46a42c2ce 100644 --- a/cli/azd/cmd/auth_local_imds.go +++ b/cli/azd/cmd/auth_local_imds.go @@ -31,8 +31,7 @@ type TokenResponse struct { func (lia *localIMDSAction) tokenHandler(w http.ResponseWriter, r *http.Request) { resource := r.URL.Query().Get("resource") if resource == "" { - http.Error(w, "Missing 'resource' query parameter", http.StatusBadRequest) - return + resource = "https://management.azure.com/" } fmt.Printf("Received request for resource: %s\n", resource) From 95b08a40a29bd1159757eb41d0e7fee5757e5594 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Tue, 4 Jun 2024 21:39:51 +0000 Subject: [PATCH 06/11] Change to azd auth serve --- cli/azd/.vscode/cspell-azd-dictionary.txt | 1 - cli/azd/cmd/auth.go | 7 ++--- .../cmd/{auth_local_imds.go => auth_serve.go} | 29 +++++++++---------- .../testdata/TestUsage-azd-auth-serve.snap | 18 ++++++++++++ cli/azd/cmd/testdata/TestUsage-azd-auth.snap | 1 + 5 files changed, 36 insertions(+), 20 deletions(-) rename cli/azd/cmd/{auth_local_imds.go => auth_serve.go} (80%) create mode 100644 cli/azd/cmd/testdata/TestUsage-azd-auth-serve.snap diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index 2388dac5fc9..2b3a0b5e321 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -128,7 +128,6 @@ grpcserver hotspot ignorefile iidfile -imds ineffassign jaegertracing javac diff --git a/cli/azd/cmd/auth.go b/cli/azd/cmd/auth.go index 6790b22251f..27d78ba182b 100644 --- a/cli/azd/cmd/auth.go +++ b/cli/azd/cmd/auth.go @@ -41,10 +41,9 @@ func authActions(root *actions.ActionDescriptor) *actions.ActionDescriptor { ActionResolver: newLogoutAction, }) - // Register the new `local-imds` command - group.Add("local-imds", &actions.ActionDescriptorOptions{ - Command: newLocalIMDSCmd("auth"), - ActionResolver: newLocalIMDSAction, + group.Add("serve", &actions.ActionDescriptorOptions{ + Command: newServeCmd("auth"), + ActionResolver: newServeAction, }) return group diff --git a/cli/azd/cmd/auth_local_imds.go b/cli/azd/cmd/auth_serve.go similarity index 80% rename from cli/azd/cmd/auth_local_imds.go rename to cli/azd/cmd/auth_serve.go index 1a46a42c2ce..8f53bc5e93a 100644 --- a/cli/azd/cmd/auth_local_imds.go +++ b/cli/azd/cmd/auth_serve.go @@ -28,7 +28,7 @@ type TokenResponse struct { } // tokenHandler handles token requests. -func (lia *localIMDSAction) tokenHandler(w http.ResponseWriter, r *http.Request) { +func (serve *serveAction) tokenHandler(w http.ResponseWriter, r *http.Request) { resource := r.URL.Query().Get("resource") if resource == "" { resource = "https://management.azure.com/" @@ -39,7 +39,7 @@ func (lia *localIMDSAction) tokenHandler(w http.ResponseWriter, r *http.Request) ctx := context.Background() var cred azcore.TokenCredential - cred, err := lia.credentialProvider(ctx, &auth.CredentialForCurrentUserOptions{ + cred, err := serve.credentialProvider(ctx, &auth.CredentialForCurrentUserOptions{ NoPrompt: true, TenantID: "", }) @@ -67,10 +67,9 @@ func (lia *localIMDSAction) tokenHandler(w http.ResponseWriter, r *http.Request) } } -// startIMDSServer starts the IMDS emulator server. -func (lia *localIMDSAction) startIMDSServer(port string) { - http.HandleFunc("/MSI/token", lia.tokenHandler) - http.HandleFunc("/metadata/identity/oauth2/token", lia.tokenHandler) +func (serve *serveAction) start(port string) { + http.HandleFunc("/MSI/token", serve.tokenHandler) + http.HandleFunc("/metadata/identity/oauth2/token", serve.tokenHandler) srv := &http.Server{ Addr: ":" + port, @@ -102,29 +101,29 @@ func (lia *localIMDSAction) startIMDSServer(port string) { os.Exit(0) } -func newLocalIMDSCmd(parent string) *cobra.Command { +func newServeCmd(parent string) *cobra.Command { return &cobra.Command{ - Use: "local-imds", - Short: "Starts a local IMDS emulator", + Use: "serve", + Short: "Starts a local Managed Identity endpoint for development purposes.", Annotations: map[string]string{ loginCmdParentAnnotation: parent, }, } } -type localIMDSAction struct { +type serveAction struct { console input.Console credentialProvider CredentialProviderFn formatter output.Formatter writer io.Writer } -func newLocalIMDSAction( +func newServeAction( console input.Console, credentialProvider CredentialProviderFn, formatter output.Formatter, writer io.Writer) actions.Action { - return &localIMDSAction{ + return &serveAction{ console: console, credentialProvider: credentialProvider, formatter: formatter, @@ -132,11 +131,11 @@ func newLocalIMDSAction( } } -func (lia *localIMDSAction) Run(ctx context.Context) (*actions.ActionResult, error) { - port := os.Getenv("IMDS_PORT") +func (serve *serveAction) Run(ctx context.Context) (*actions.ActionResult, error) { + port := os.Getenv("AZD_AUTH_SERVER_PORT") if port == "" { port = "53028" } - lia.startIMDSServer(port) + serve.start(port) return nil, nil } diff --git a/cli/azd/cmd/testdata/TestUsage-azd-auth-serve.snap b/cli/azd/cmd/testdata/TestUsage-azd-auth-serve.snap new file mode 100644 index 00000000000..f6273bd20d5 --- /dev/null +++ b/cli/azd/cmd/testdata/TestUsage-azd-auth-serve.snap @@ -0,0 +1,18 @@ + +Starts a local Managed Identity endpoint for development purposes. + +Usage + azd auth serve [flags] + +Flags + --docs : Opens the documentation for azd auth serve in your web browser. + -h, --help : Gets help for serve. + +Global Flags + -C, --cwd string : Sets the current working directory. + --debug : Enables debugging and diagnostics logging. + --no-prompt : Accepts the default value instead of prompting, or it fails if there is no default. + +Find a bug? Want to let us know how we're doing? Fill out this brief survey: https://aka.ms/azure-dev/hats. + + diff --git a/cli/azd/cmd/testdata/TestUsage-azd-auth.snap b/cli/azd/cmd/testdata/TestUsage-azd-auth.snap index 21a262409d3..bbe3c47bea3 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-auth.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-auth.snap @@ -7,6 +7,7 @@ Usage Available Commands login : Log in to Azure. logout : Log out of Azure. + serve : Starts a local Managed Identity endpoint for development purposes. Global Flags -C, --cwd string : Sets the current working directory. From 7c75840b17b028457b121da1cb1d40afafdbb688 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:45:51 +0000 Subject: [PATCH 07/11] Restrict calls from localhost --- cli/azd/cmd/auth_serve.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/cli/azd/cmd/auth_serve.go b/cli/azd/cmd/auth_serve.go index 8f53bc5e93a..81c16b4078f 100644 --- a/cli/azd/cmd/auth_serve.go +++ b/cli/azd/cmd/auth_serve.go @@ -29,12 +29,32 @@ type TokenResponse struct { // tokenHandler handles token requests. func (serve *serveAction) tokenHandler(w http.ResponseWriter, r *http.Request) { + // Extract the IP address from the request's RemoteAddr field + clientIP := r.RemoteAddr + + // Only allow requests from 127.0.0.1 or host.docker.internal + allowedIPs := []string{"127.0.0.1", "host.docker.internal"} + + // Check if the request comes from an allowed IP address + ipAllowed := false + for _, allowedIP := range allowedIPs { + if clientIP == allowedIP { + ipAllowed = true + break + } + } + + if !ipAllowed { + http.Error(w, "Forbidden: Requests are only allowed from 127.0.0.1 or host.docker.internal", http.StatusForbidden) + return + } + resource := r.URL.Query().Get("resource") if resource == "" { resource = "https://management.azure.com/" } - fmt.Printf("Received request for resource: %s\n", resource) + fmt.Printf("Received request for resource: %s from IP: %s\n", resource, clientIP) ctx := context.Background() var cred azcore.TokenCredential @@ -45,6 +65,7 @@ func (serve *serveAction) tokenHandler(w http.ResponseWriter, r *http.Request) { }) if err != nil { fmt.Printf("credentialProvider: %v", err) + http.Error(w, "Failed to get credentials: "+err.Error(), http.StatusInternalServerError) return } @@ -53,6 +74,7 @@ func (serve *serveAction) tokenHandler(w http.ResponseWriter, r *http.Request) { }) if err != nil { fmt.Printf("fetching token: %v", err) + http.Error(w, "Failed to fetch token: "+err.Error(), http.StatusInternalServerError) return } From 25702f8d10b8ea1038be41e29ffdbcd8e702df1e Mon Sep 17 00:00:00 2001 From: Jon Gallant Date: Thu, 9 Jan 2025 09:08:10 -0800 Subject: [PATCH 08/11] Enhance tokenHandler to support X-Forwarded-For and improve IP address validation --- cli/azd/cmd/auth_serve.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cli/azd/cmd/auth_serve.go b/cli/azd/cmd/auth_serve.go index 81c16b4078f..718f7bd8852 100644 --- a/cli/azd/cmd/auth_serve.go +++ b/cli/azd/cmd/auth_serve.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log" + "net" "net/http" "os" "os/signal" @@ -29,11 +30,13 @@ type TokenResponse struct { // tokenHandler handles token requests. func (serve *serveAction) tokenHandler(w http.ResponseWriter, r *http.Request) { - // Extract the IP address from the request's RemoteAddr field - clientIP := r.RemoteAddr + clientIP := r.Header.Get("X-Forwarded-For") + if clientIP == "" { + clientIP, _, _ = net.SplitHostPort(r.RemoteAddr) + } // Only allow requests from 127.0.0.1 or host.docker.internal - allowedIPs := []string{"127.0.0.1", "host.docker.internal"} + allowedIPs := []string{"::1", "127.0.0.1", "host.docker.internal"} // Check if the request comes from an allowed IP address ipAllowed := false @@ -50,6 +53,9 @@ func (serve *serveAction) tokenHandler(w http.ResponseWriter, r *http.Request) { } resource := r.URL.Query().Get("resource") + if resource == "" { + resource = r.FormValue("resource") + } if resource == "" { resource = "https://management.azure.com/" } From 11d2d4f7717680db3c49c753a798c86f0dedce24 Mon Sep 17 00:00:00 2001 From: Jon Gallant Date: Thu, 9 Jan 2025 15:51:56 -0800 Subject: [PATCH 09/11] Update documentation for azd auth serve command flags --- cli/azd/cmd/testdata/TestUsage-azd-auth-serve.snap | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cli/azd/cmd/testdata/TestUsage-azd-auth-serve.snap b/cli/azd/cmd/testdata/TestUsage-azd-auth-serve.snap index f6273bd20d5..43a108194fa 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-auth-serve.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-auth-serve.snap @@ -4,13 +4,11 @@ Starts a local Managed Identity endpoint for development purposes. Usage azd auth serve [flags] -Flags - --docs : Opens the documentation for azd auth serve in your web browser. - -h, --help : Gets help for serve. - Global Flags -C, --cwd string : Sets the current working directory. --debug : Enables debugging and diagnostics logging. + --docs : Opens the documentation for azd auth serve in your web browser. + -h, --help : Gets help for serve. --no-prompt : Accepts the default value instead of prompting, or it fails if there is no default. Find a bug? Want to let us know how we're doing? Fill out this brief survey: https://aka.ms/azure-dev/hats. From 5b841737017f06e807db62f177a5ce00de87af2d Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:28:36 -0800 Subject: [PATCH 10/11] Add copyright notice and license information to auth_serve.go --- cli/azd/cmd/auth_serve.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/azd/cmd/auth_serve.go b/cli/azd/cmd/auth_serve.go index 718f7bd8852..9a1289c2743 100644 --- a/cli/azd/cmd/auth_serve.go +++ b/cli/azd/cmd/auth_serve.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package cmd import ( From 9a1abd5cdda156d18d1bcea33abd6a2f0a0d9afd Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:34:14 -0800 Subject: [PATCH 11/11] Log client IP address in tokenHandler for debugging purposes --- cli/azd/cmd/auth_serve.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/azd/cmd/auth_serve.go b/cli/azd/cmd/auth_serve.go index 9a1289c2743..eff8f387dd3 100644 --- a/cli/azd/cmd/auth_serve.go +++ b/cli/azd/cmd/auth_serve.go @@ -38,6 +38,8 @@ func (serve *serveAction) tokenHandler(w http.ResponseWriter, r *http.Request) { clientIP, _, _ = net.SplitHostPort(r.RemoteAddr) } + fmt.Printf("Client IP: %s\n", clientIP) + // Only allow requests from 127.0.0.1 or host.docker.internal allowedIPs := []string{"::1", "127.0.0.1", "host.docker.internal"}