mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Add OIDC integration tests
* Port OIDC integration tests to v2 * Move Tailscale old versions to TS2019 list * Remove Alpine Linux container * Updated changelog * Releases: use flavor to set the tag suffix * Added more debug messages in OIDC registration * Added more logging * Do not strip nodekey prefix on handle expired * Updated changelog * Add WithHostnameAsServerURL option func * Reduce the number of namespaces and use hsic.WithHostnameAsServerURL * Linting fix * Fix linting issues * Wait for ready outside the up goroutine * Minor change in log message * Add prefix to env var * Remove unused env var Co-authored-by: Juan Font <juan.font@esa.int> Co-authored-by: Steven Honson <steven@honson.id.au> Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
This commit is contained in:
		
							parent
							
								
									4ccc528d96
								
							
						
					
					
						commit
						1b0e80bb10
					
				
							
								
								
									
										301
									
								
								integration/auth_oidc_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								integration/auth_oidc_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,301 @@ | ||||
| package integration | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/juanfont/headscale" | ||||
| 	"github.com/juanfont/headscale/integration/dockertestutil" | ||||
| 	"github.com/juanfont/headscale/integration/hsic" | ||||
| 	"github.com/ory/dockertest/v3" | ||||
| 	"github.com/ory/dockertest/v3/docker" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	dockerContextPath      = "../." | ||||
| 	hsicOIDCMockHashLength = 6 | ||||
| 	oidcServerPort         = 10000 | ||||
| ) | ||||
| 
 | ||||
| var errStatusCodeNotOK = errors.New("status code not OK") | ||||
| 
 | ||||
| type AuthOIDCScenario struct { | ||||
| 	*Scenario | ||||
| 
 | ||||
| 	mockOIDC *dockertest.Resource | ||||
| } | ||||
| 
 | ||||
| func TestOIDCAuthenticationPingAll(t *testing.T) { | ||||
| 	IntegrationSkip(t) | ||||
| 
 | ||||
| 	baseScenario, err := NewScenario() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("failed to create scenario: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	scenario := AuthOIDCScenario{ | ||||
| 		Scenario: baseScenario, | ||||
| 	} | ||||
| 
 | ||||
| 	spec := map[string]int{ | ||||
| 		"namespace1": len(TailscaleVersions), | ||||
| 	} | ||||
| 
 | ||||
| 	oidcConfig, err := scenario.runMockOIDC() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("failed to run mock OIDC server: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	oidcMap := map[string]string{ | ||||
| 		"HEADSCALE_OIDC_ISSUER":             oidcConfig.Issuer, | ||||
| 		"HEADSCALE_OIDC_CLIENT_ID":          oidcConfig.ClientID, | ||||
| 		"HEADSCALE_OIDC_CLIENT_SECRET":      oidcConfig.ClientSecret, | ||||
| 		"HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": fmt.Sprintf("%t", oidcConfig.StripEmaildomain), | ||||
| 	} | ||||
| 
 | ||||
| 	err = scenario.CreateHeadscaleEnv( | ||||
| 		spec, | ||||
| 		hsic.WithTestName("oidcauthping"), | ||||
| 		hsic.WithConfigEnv(oidcMap), | ||||
| 		hsic.WithHostnameAsServerURL(), | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("failed to create headscale environment: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	allClients, err := scenario.ListTailscaleClients() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("failed to get clients: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	allIps, err := scenario.ListTailscaleClientsIPs() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("failed to get clients: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	err = scenario.WaitForTailscaleSync() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("failed wait for tailscale clients to be in sync: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	success := 0 | ||||
| 
 | ||||
| 	for _, client := range allClients { | ||||
| 		for _, ip := range allIps { | ||||
| 			err := client.Ping(ip.String()) | ||||
| 			if err != nil { | ||||
| 				t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err) | ||||
| 			} else { | ||||
| 				success++ | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) | ||||
| 
 | ||||
| 	err = scenario.Shutdown() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("failed to tear down scenario: %s", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *AuthOIDCScenario) CreateHeadscaleEnv( | ||||
| 	namespaces map[string]int, | ||||
| 	opts ...hsic.Option, | ||||
| ) error { | ||||
| 	headscale, err := s.Headscale(opts...) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	err = headscale.WaitForReady() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	for namespaceName, clientCount := range namespaces { | ||||
| 		log.Printf("creating namespace %s with %d clients", namespaceName, clientCount) | ||||
| 		err = s.CreateNamespace(namespaceName) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		err = s.CreateTailscaleNodesInNamespace(namespaceName, "all", clientCount) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		err = s.runTailscaleUp(namespaceName, headscale.GetEndpoint()) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (s *AuthOIDCScenario) runMockOIDC() (*headscale.OIDCConfig, error) { | ||||
| 	hash, _ := headscale.GenerateRandomStringDNSSafe(hsicOIDCMockHashLength) | ||||
| 
 | ||||
| 	hostname := fmt.Sprintf("hs-oidcmock-%s", hash) | ||||
| 
 | ||||
| 	mockOidcOptions := &dockertest.RunOptions{ | ||||
| 		Name:         hostname, | ||||
| 		Cmd:          []string{"headscale", "mockoidc"}, | ||||
| 		ExposedPorts: []string{"10000/tcp"}, | ||||
| 		PortBindings: map[docker.Port][]docker.PortBinding{ | ||||
| 			"10000/tcp": {{HostPort: "10000"}}, | ||||
| 		}, | ||||
| 		Networks: []*dockertest.Network{s.Scenario.network}, | ||||
| 		Env: []string{ | ||||
| 			fmt.Sprintf("MOCKOIDC_ADDR=%s", hostname), | ||||
| 			"MOCKOIDC_PORT=10000", | ||||
| 			"MOCKOIDC_CLIENT_ID=superclient", | ||||
| 			"MOCKOIDC_CLIENT_SECRET=supersecret", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	headscaleBuildOptions := &dockertest.BuildOptions{ | ||||
| 		Dockerfile: "Dockerfile.debug", | ||||
| 		ContextDir: dockerContextPath, | ||||
| 	} | ||||
| 
 | ||||
| 	err := s.pool.RemoveContainerByName(hostname) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if pmockoidc, err := s.pool.BuildAndRunWithBuildOptions( | ||||
| 		headscaleBuildOptions, | ||||
| 		mockOidcOptions, | ||||
| 		dockertestutil.DockerRestartPolicy); err == nil { | ||||
| 		s.mockOIDC = pmockoidc | ||||
| 	} else { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	log.Println("Waiting for headscale mock oidc to be ready for tests") | ||||
| 	hostEndpoint := fmt.Sprintf( | ||||
| 		"%s:%s", | ||||
| 		s.mockOIDC.GetIPInNetwork(s.network), | ||||
| 		s.mockOIDC.GetPort(fmt.Sprintf("%d/tcp", oidcServerPort)), | ||||
| 	) | ||||
| 
 | ||||
| 	if err := s.pool.Retry(func() error { | ||||
| 		oidcConfigURL := fmt.Sprintf("http://%s/oidc/.well-known/openid-configuration", hostEndpoint) | ||||
| 		httpClient := &http.Client{} | ||||
| 		ctx := context.Background() | ||||
| 		req, _ := http.NewRequestWithContext(ctx, http.MethodGet, oidcConfigURL, nil) | ||||
| 		resp, err := httpClient.Do(req) | ||||
| 		if err != nil { | ||||
| 			log.Printf("headscale mock OIDC tests is not ready: %s\n", err) | ||||
| 
 | ||||
| 			return err | ||||
| 		} | ||||
| 		defer resp.Body.Close() | ||||
| 
 | ||||
| 		if resp.StatusCode != http.StatusOK { | ||||
| 			return errStatusCodeNotOK | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	}); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	log.Printf("headscale mock oidc is ready for tests at %s", hostEndpoint) | ||||
| 
 | ||||
| 	return &headscale.OIDCConfig{ | ||||
| 		Issuer: fmt.Sprintf("http://%s/oidc", | ||||
| 			net.JoinHostPort(s.mockOIDC.GetIPInNetwork(s.network), strconv.Itoa(oidcServerPort))), | ||||
| 		ClientID:         "superclient", | ||||
| 		ClientSecret:     "supersecret", | ||||
| 		StripEmaildomain: true, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (s *AuthOIDCScenario) runTailscaleUp( | ||||
| 	namespaceStr, loginServer string, | ||||
| ) error { | ||||
| 	headscale, err := s.Headscale() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	log.Printf("running tailscale up for namespace %s", namespaceStr) | ||||
| 	if namespace, ok := s.namespaces[namespaceStr]; ok { | ||||
| 		for _, client := range namespace.Clients { | ||||
| 			namespace.joinWaitGroup.Add(1) | ||||
| 
 | ||||
| 			go func(c TailscaleClient) { | ||||
| 				defer namespace.joinWaitGroup.Done() | ||||
| 
 | ||||
| 				// TODO(juanfont): error handle this
 | ||||
| 				loginURL, err := c.UpWithLoginURL(loginServer) | ||||
| 				if err != nil { | ||||
| 					log.Printf("failed to run tailscale up: %s", err) | ||||
| 				} | ||||
| 
 | ||||
| 				loginURL.Host = fmt.Sprintf("%s:8080", headscale.GetIP()) | ||||
| 				loginURL.Scheme = "http" | ||||
| 
 | ||||
| 				insecureTransport := &http.Transport{ | ||||
| 					TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint
 | ||||
| 				} | ||||
| 
 | ||||
| 				log.Printf("%s login url: %s\n", c.Hostname(), loginURL.String()) | ||||
| 
 | ||||
| 				httpClient := &http.Client{Transport: insecureTransport} | ||||
| 				ctx := context.Background() | ||||
| 				req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil) | ||||
| 				resp, err := httpClient.Do(req) | ||||
| 				if err != nil { | ||||
| 					log.Printf("%s failed to get login url %s: %s", c.Hostname(), loginURL, err) | ||||
| 
 | ||||
| 					return | ||||
| 				} | ||||
| 
 | ||||
| 				defer resp.Body.Close() | ||||
| 
 | ||||
| 				_, err = io.ReadAll(resp.Body) | ||||
| 				if err != nil { | ||||
| 					log.Printf("%s failed to read response body: %s", c.Hostname(), err) | ||||
| 
 | ||||
| 					return | ||||
| 				} | ||||
| 
 | ||||
| 				log.Printf("Finished request for %s to join tailnet", c.Hostname()) | ||||
| 			}(client) | ||||
| 
 | ||||
| 			err = client.WaitForReady() | ||||
| 			if err != nil { | ||||
| 				log.Printf("error waiting for client %s to be ready: %s", client.Hostname(), err) | ||||
| 			} | ||||
| 
 | ||||
| 			log.Printf("client %s is ready", client.Hostname()) | ||||
| 		} | ||||
| 
 | ||||
| 		namespace.joinWaitGroup.Wait() | ||||
| 
 | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	return fmt.Errorf("failed to up tailscale node: %w", errNoNamespaceAvailable) | ||||
| } | ||||
| 
 | ||||
| func (s *AuthOIDCScenario) Shutdown() error { | ||||
| 	err := s.pool.Purge(s.mockOIDC) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return s.Scenario.Shutdown() | ||||
| } | ||||
| @ -104,6 +104,17 @@ func WithTestName(testName string) Option { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func WithHostnameAsServerURL() Option { | ||||
| 	return func(hsic *HeadscaleInContainer) { | ||||
| 		hsic.env = append( | ||||
| 			hsic.env, | ||||
| 			fmt.Sprintf("HEADSCALE_SERVER_URL=http://%s:%d", | ||||
| 				hsic.GetHostname(), | ||||
| 				hsic.port, | ||||
| 			)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func New( | ||||
| 	pool *dockertest.Pool, | ||||
| 	network *dockertest.Network, | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user