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( | func New( | ||||||
| 	pool *dockertest.Pool, | 	pool *dockertest.Pool, | ||||||
| 	network *dockertest.Network, | 	network *dockertest.Network, | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user