From 0b138a7baeb5927e86c32d7689872a1c9b3e812d Mon Sep 17 00:00:00 2001 From: Christopher Johnstone Date: Sun, 26 Mar 2023 10:45:32 -0400 Subject: [PATCH 1/4] Allow use of the preferred_username OIDC claim. Previously, Headscale would only use the `email` OIDC claim to set the Headscale user. In certain cases (self-hosted SSO), it may be useful to instead use the `preferred_username` to set the Headscale username. This also closes #938. This adds a config setting to use this claim instead. The OIDC docs have been updated to include this entry as well. In addition, this adds an Authelia OIDC example to the docs. --- config-example.yaml | 10 +++++++++ config.go | 2 ++ docs/oidc.md | 51 +++++++++++++++++++++++++++++++++++++++++++++ oidc.go | 21 +++++++++++++++---- 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/config-example.yaml b/config-example.yaml index 93aa797ac1..f2f997c6d4 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -303,6 +303,16 @@ unix_socket_permission: "0770" # allowed_users: # - alice@example.com # +# # By default, Headscale will use the OIDC email address claim to determine the username. +# # OIDC also returns a `preferred_username` claim. +# # +# # If `use_username_claim` is set to `true`, then the `preferred_username` claim will +# # be used instead to set the Headscale username. +# # If `use_username_claim` is set to `false`, then the `email` claim will be used +# # to derive the Headscale username (as modified by the `strip_email_domain` entry). +# +# use_username_claim: false +# # # If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed. # # This will transform `first-name.last-name@example.com` to the user `first-name.last-name` # # If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following diff --git a/config.go b/config.go index c0dd1c9864..c8b6b25bc6 100644 --- a/config.go +++ b/config.go @@ -107,6 +107,7 @@ type OIDCConfig struct { AllowedUsers []string AllowedGroups []string StripEmaildomain bool + UseUsernameClaim bool Expiry time.Duration UseExpiryFromToken bool } @@ -610,6 +611,7 @@ func GetHeadscaleConfig() (*Config, error) { AllowedDomains: viper.GetStringSlice("oidc.allowed_domains"), AllowedUsers: viper.GetStringSlice("oidc.allowed_users"), AllowedGroups: viper.GetStringSlice("oidc.allowed_groups"), + UseUsernameClaim: viper.GetBool("oidc.use_username_claim"), StripEmaildomain: viper.GetBool("oidc.strip_email_domain"), Expiry: func() time.Duration { // if set to 0, we assume no expiry diff --git a/docs/oidc.md b/docs/oidc.md index 189d7cd736..dc52c29263 100644 --- a/docs/oidc.md +++ b/docs/oidc.md @@ -43,6 +43,16 @@ oidc: allowed_users: - alice@example.com + # By default, Headscale will use the OIDC email address claim to determine the username. + # OIDC also returns a `preferred_username` claim. + # + # If `use_username_claim` is set to `true`, then the `preferred_username` claim will + # be used instead to set the Headscale username. + # If `use_username_claim` is set to `false`, then the `email` claim will be used + # to derive the Headscale username (as modified by the `strip_email_domain` entry). + + use_username_claim: false + # If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed. # This will transform `first-name.last-name@example.com` to the user `first-name.last-name` # If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following @@ -170,3 +180,44 @@ oidc: ``` You can also use `allowed_domains` and `allowed_users` to restrict the users who can authenticate. + +## Authelia Example + +In order to integrate Headscale with your Authelia instance, you need to generate a client secret add your Headscale instance as a client. + +First, generate a client secret. If you are running Authelia inside docker, prepend `docker-compose exec ` before these commands: +```shell +authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 +``` +This will return two strings, a "Random Password" which you will fill into Headscale, and a "Digest" you will fill into Authelia. + +In your Authelia configuration, add Headscale under the client section: +```yaml +clients: + - id: headscale + description: Headscale + secret: "DIGEST_STRING_FROM_ABOVE" + public: false + authorization_policy: two_factor + redirect_uris: + - https://your.headscale.domain/oidc/callback + scopes: + - openid + - profile + - email + - groups +``` + +In your Headscale `config.yaml`, edit the config under `oidc`, filling in the `client_id` to match the `id` line in the Authelia config and filling in `client_secret` from the "Random Password" output. +You may want to tune the `expiry`, `only_start_if_oidc_available`, and other entries. The following are only the required entries. +```yaml +oidc: + issuer: "https://your.authelia.domain" + client_id: "headscale" + client_secret: "RANDOM_PASSWORD_STRING_FROM_ABOVE" + scope: ["openid", "profile", "email", "groups"] + allowed_groups: + - authelia_groups_you_want_to_limit +``` + +In particular, you may want to set `use_username_claim: true` to use Authelia's `preferred_username` grant to set Headscale usernames. \ No newline at end of file diff --git a/oidc.go b/oidc.go index 5aa3ba6203..205784c352 100644 --- a/oidc.go +++ b/oidc.go @@ -239,7 +239,7 @@ func (h *Headscale) OIDCCallback( return } - userName, err := getUserName(writer, claims, h.cfg.OIDC.StripEmaildomain) + userName, err := getUserName(writer, claims, h.cfg.OIDC.UseUsernameClaim, h.cfg.OIDC.StripEmaildomain) if err != nil { return } @@ -625,17 +625,30 @@ func (h *Headscale) validateMachineForOIDCCallback( func getUserName( writer http.ResponseWriter, claims *IDTokenClaims, + useUsernameClaim bool, stripEmaildomain bool, ) (string, error) { + var claim string + if useUsernameClaim { + claim = claims.Username + } else { + claim = claims.Email + } userName, err := NormalizeToFQDNRules( - claims.Email, + claim, stripEmaildomain, ) if err != nil { - log.Error().Err(err).Caller().Msgf("couldn't normalize email") + var friendlyErrMsg string + if useUsernameClaim { + friendlyErrMsg = "couldn't normalize username (preferred_username OIDC claim)" + } else { + friendlyErrMsg = "couldn't normalize username (email OIDC claim)" + } + log.Error().Err(err).Caller().Msgf(friendlyErrMsg) writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) - _, werr := writer.Write([]byte("couldn't normalize email")) + _, werr := writer.Write([]byte(friendlyErrMsg)) if werr != nil { log.Error(). Caller(). From 64f94e8665ca1c6338831639ad657468af3fbc4c Mon Sep 17 00:00:00 2001 From: Christopher Johnstone Date: Sat, 22 Apr 2023 09:22:18 -0400 Subject: [PATCH 2/4] Added OIDC claim integration tests. Updated the MockOIDC wrapper to take an environment variable that lets you set the username/email claims to return. Added two integration tests, TestOIDCEmailGrant and TestOIDCUsernameGrant, which check the username by checking the FQDN of clients. Updated the HTML template shown after OIDC login to show whatever username is used, based on the Headscale settings. --- cmd/headscale/cli/mockoidc.go | 33 +++++- config.go | 3 +- docs/oidc.md | 8 +- integration/auth_oidc_test.go | 183 +++++++++++++++++++++++++++++++++- oidc.go | 13 ++- 5 files changed, 228 insertions(+), 12 deletions(-) diff --git a/cmd/headscale/cli/mockoidc.go b/cmd/headscale/cli/mockoidc.go index 568a2a03e8..1245d3d6f8 100644 --- a/cmd/headscale/cli/mockoidc.go +++ b/cmd/headscale/cli/mockoidc.go @@ -4,7 +4,9 @@ import ( "fmt" "net" "os" + "regexp" "strconv" + "strings" "time" "github.com/oauth2-proxy/mockoidc" @@ -63,6 +65,29 @@ func mockOIDC() error { } accessTTL = newTTL } + + mockUsers := os.Getenv("MOCKOIDC_USERS") + users := []mockoidc.User{} + if mockUsers != "" { + user_strings := strings.Split(mockUsers, ",") + user_re := regexp.MustCompile(`^\s*(?P\S+)\s*<(?P\S+@\S+)>\s*$`) + for _, v := range user_strings { + match := user_re.FindStringSubmatch(v) + if match != nil { + fmt.Println("Success!", match[1], match[2]) + // Use the default mockoidc entries for other entries + users = append(users, &mockoidc.MockUser{ + Subject: "1234567890", + Email: match[2], + PreferredUsername: match[1], + Phone: "555-987-6543", + Address: "123 Main Street", + Groups: []string{"engineering", "design"}, + EmailVerified: true, + }) + } + } + } log.Info().Msgf("Access token TTL: %s", accessTTL) @@ -71,7 +96,7 @@ func mockOIDC() error { return err } - mock, err := getMockOIDC(clientID, clientSecret) + mock, err := getMockOIDC(clientID, clientSecret, users) if err != nil { return err } @@ -93,7 +118,7 @@ func mockOIDC() error { return nil } -func getMockOIDC(clientID string, clientSecret string) (*mockoidc.MockOIDC, error) { +func getMockOIDC(clientID string, clientSecret string, users []mockoidc.User) (*mockoidc.MockOIDC, error) { keypair, err := mockoidc.NewKeypair(nil) if err != nil { return nil, err @@ -111,5 +136,9 @@ func getMockOIDC(clientID string, clientSecret string) (*mockoidc.MockOIDC, erro ErrorQueue: &mockoidc.ErrorQueue{}, } + for _, v := range users { + mock.QueueUser(v) + } + return &mock, nil } diff --git a/config.go b/config.go index c8b6b25bc6..f0e739e051 100644 --- a/config.go +++ b/config.go @@ -107,7 +107,7 @@ type OIDCConfig struct { AllowedUsers []string AllowedGroups []string StripEmaildomain bool - UseUsernameClaim bool + UseUsernameClaim bool Expiry time.Duration UseExpiryFromToken bool } @@ -188,6 +188,7 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"}) viper.SetDefault("oidc.strip_email_domain", true) + viper.SetDefault("oidc.use_username_claim", false) viper.SetDefault("oidc.only_start_if_oidc_is_available", true) viper.SetDefault("oidc.expiry", "180d") viper.SetDefault("oidc.use_expiry_from_token", false) diff --git a/docs/oidc.md b/docs/oidc.md index dc52c29263..689e50c4b7 100644 --- a/docs/oidc.md +++ b/docs/oidc.md @@ -186,12 +186,15 @@ You can also use `allowed_domains` and `allowed_users` to restrict the users who In order to integrate Headscale with your Authelia instance, you need to generate a client secret add your Headscale instance as a client. First, generate a client secret. If you are running Authelia inside docker, prepend `docker-compose exec ` before these commands: + ```shell authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 ``` + This will return two strings, a "Random Password" which you will fill into Headscale, and a "Digest" you will fill into Authelia. In your Authelia configuration, add Headscale under the client section: + ```yaml clients: - id: headscale @@ -208,8 +211,9 @@ clients: - groups ``` -In your Headscale `config.yaml`, edit the config under `oidc`, filling in the `client_id` to match the `id` line in the Authelia config and filling in `client_secret` from the "Random Password" output. +In your Headscale `config.yaml`, edit the config under `oidc`, filling in the `client_id` to match the `id` line in the Authelia config and filling in `client_secret` from the "Random Password" output. You may want to tune the `expiry`, `only_start_if_oidc_available`, and other entries. The following are only the required entries. + ```yaml oidc: issuer: "https://your.authelia.domain" @@ -220,4 +224,4 @@ oidc: - authelia_groups_you_want_to_limit ``` -In particular, you may want to set `use_username_claim: true` to use Authelia's `preferred_username` grant to set Headscale usernames. \ No newline at end of file +In particular, you may want to set `use_username_claim: true` to use Authelia's `preferred_username` grant to set Headscale usernames. diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index 3e61c197dc..78d68ab304 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/netip" "strconv" + "strings" "testing" "time" @@ -36,6 +37,181 @@ type AuthOIDCScenario struct { mockOIDC *dockertest.Resource } +func TestOIDCUsernameGrant(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + baseScenario, err := NewScenario() + if err != nil { + t.Errorf("failed to create scenario: %s", err) + } + + scenario := AuthOIDCScenario{ + Scenario: baseScenario, + } + + spec := map[string]int{ + "user1": len(TailscaleVersions), + } + + users := make([]string, len(TailscaleVersions)) + for i := range users { + users[i] = "test-user " + } + user_str := strings.Join(users, ", ") + + oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL, user_str) + 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, + "CREDENTIALS_DIRECTORY_TEST": "/tmp", + "HEADSCALE_OIDC_CLIENT_SECRET_PATH": "${CREDENTIALS_DIRECTORY_TEST}/hs_client_oidc_secret", + "HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": "false", + "HEADSCALE_OIDC_USE_USERNAME_CLAIM": "true", + } + + err = scenario.CreateHeadscaleEnv( + spec, + hsic.WithTestName("oidcauthping"), + hsic.WithConfigEnv(oidcMap), + hsic.WithHostnameAsServerURL(), + hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(oidcConfig.ClientSecret)), + ) + 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) + } + + // Check that clients are registered under the right username + for _, client := range allClients { + fqdn, err := client.FQDN() + if err != nil { + t.Errorf("Unable to get client FQDN: %s", err) + } + + if !strings.HasSuffix(fqdn, "test-user.headscale.net") { + t.Errorf("Client registered with unexpected username. Client FQDN: %s", fqdn) + } + } + + 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) + } + + allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { + return x.String() + }) + + success := pingAllHelper(t, allClients, allAddrs) + 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 TestOIDCEmailGrant(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + baseScenario, err := NewScenario() + if err != nil { + t.Errorf("failed to create scenario: %s", err) + } + + scenario := AuthOIDCScenario{ + Scenario: baseScenario, + } + + spec := map[string]int{ + "user1": len(TailscaleVersions), + } + + users := make([]string, len(TailscaleVersions)) + for i := range users { + users[i] = "test-user " + } + user_str := strings.Join(users, ", ") + + oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL, user_str) + 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, + "CREDENTIALS_DIRECTORY_TEST": "/tmp", + "HEADSCALE_OIDC_CLIENT_SECRET_PATH": "${CREDENTIALS_DIRECTORY_TEST}/hs_client_oidc_secret", + "HEADSCALE_OIDC_STRIP_EMAIL_DOMAIN": "true", + "HEADSCALE_OIDC_USE_USERNAME_CLAIM": "false", + } + + err = scenario.CreateHeadscaleEnv( + spec, + hsic.WithTestName("oidcauthping"), + hsic.WithConfigEnv(oidcMap), + hsic.WithHostnameAsServerURL(), + hsic.WithFileInContainer("/tmp/hs_client_oidc_secret", []byte(oidcConfig.ClientSecret)), + ) + 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) + } + // Check that clients are registered under the right username + for _, client := range allClients { + fqdn, err := client.FQDN() + if err != nil { + t.Errorf("Unable to get client FQDN: %s", err) + } + + if !strings.HasSuffix(fqdn, "test-email.headscale.net") { + t.Errorf("Client registered with unexpected username. Client FQDN: %s", fqdn) + } + } + + 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) + } + + allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { + return x.String() + }) + + success := pingAllHelper(t, allClients, allAddrs) + 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 TestOIDCAuthenticationPingAll(t *testing.T) { IntegrationSkip(t) t.Parallel() @@ -53,7 +229,7 @@ func TestOIDCAuthenticationPingAll(t *testing.T) { "user1": len(TailscaleVersions), } - oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL) + oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL, "") if err != nil { t.Errorf("failed to run mock OIDC server: %s", err) } @@ -124,7 +300,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) { "user1": len(TailscaleVersions), } - oidcConfig, err := scenario.runMockOIDC(shortAccessTTL) + oidcConfig, err := scenario.runMockOIDC(shortAccessTTL, "") if err != nil { t.Fatalf("failed to run mock OIDC server: %s", err) } @@ -213,7 +389,7 @@ func (s *AuthOIDCScenario) CreateHeadscaleEnv( return nil } -func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration) (*headscale.OIDCConfig, error) { +func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration, users string) (*headscale.OIDCConfig, error) { port, err := dockertestutil.RandomFreeHostPort() if err != nil { log.Fatalf("could not find an open port: %s", err) @@ -237,6 +413,7 @@ func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration) (*headscale.OIDC fmt.Sprintf("MOCKOIDC_PORT=%d", port), "MOCKOIDC_CLIENT_ID=superclient", "MOCKOIDC_CLIENT_SECRET=supersecret", + fmt.Sprintf("MOCKOIDC_USERS=%s", users), fmt.Sprintf("MOCKOIDC_ACCESS_TTL=%s", accessTTL.String()), }, } diff --git a/oidc.go b/oidc.go index 205784c352..53063a2cc0 100644 --- a/oidc.go +++ b/oidc.go @@ -256,7 +256,7 @@ func (h *Headscale) OIDCCallback( return } - content, err := renderOIDCCallbackTemplate(writer, claims) + content, err := renderOIDCCallbackTemplate(writer, userName) if err != nil { return } @@ -582,9 +582,14 @@ func (h *Headscale) validateMachineForOIDCCallback( Str("expiresAt", fmt.Sprintf("%v", expiry)). Msg("successfully refreshed machine") + userName, err := getUserName(writer, claims, h.cfg.OIDC.UseUsernameClaim, h.cfg.OIDC.StripEmaildomain) + if err != nil { + userName = "unknown" + } + var content bytes.Buffer if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{ - User: claims.Email, + User: userName, Verb: "Reauthenticated", }); err != nil { log.Error(). @@ -743,11 +748,11 @@ func (h *Headscale) registerMachineForOIDCCallback( func renderOIDCCallbackTemplate( writer http.ResponseWriter, - claims *IDTokenClaims, + user string, ) (*bytes.Buffer, error) { var content bytes.Buffer if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{ - User: claims.Email, + User: user, Verb: "Authenticated", }); err != nil { log.Error(). From c65587efc680498c5a62b04b748558c38e7e5832 Mon Sep 17 00:00:00 2001 From: Christopher Johnstone Date: Sat, 22 Apr 2023 09:26:43 -0400 Subject: [PATCH 3/4] Added new GHA workflows for OIDC grant tests --- ...est-integration-v2-TestOIDCEmailGrant.yaml | 57 +++++++++++++++++++ ...-integration-v2-TestOIDCUsernameGrant.yaml | 57 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 .github/workflows/test-integration-v2-TestOIDCEmailGrant.yaml create mode 100644 .github/workflows/test-integration-v2-TestOIDCUsernameGrant.yaml diff --git a/.github/workflows/test-integration-v2-TestOIDCEmailGrant.yaml b/.github/workflows/test-integration-v2-TestOIDCEmailGrant.yaml new file mode 100644 index 0000000000..9a55f2b9f8 --- /dev/null +++ b/.github/workflows/test-integration-v2-TestOIDCEmailGrant.yaml @@ -0,0 +1,57 @@ +# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go +# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ + +name: Integration Test v2 - TestOIDCEmailGrant + +on: [pull_request] + +concurrency: + group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v34 + with: + files: | + *.nix + go.* + **/*.go + integration_test/ + config-example.yaml + + - uses: cachix/install-nix-action@v18 + if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true' + + - name: Run general integration tests + if: steps.changed-files.outputs.any_changed == 'true' + run: | + nix develop --command -- docker run \ + --tty --rm \ + --volume ~/.cache/hs-integration-go:/go \ + --name headscale-test-suite \ + --volume $PWD:$PWD -w $PWD/integration \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --volume $PWD/control_logs:/tmp/control \ + golang:1 \ + go test ./... \ + -tags ts2019 \ + -failfast \ + -timeout 120m \ + -parallel 1 \ + -run "^TestOIDCEmailGrant$" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: logs + path: "control_logs/*.log" diff --git a/.github/workflows/test-integration-v2-TestOIDCUsernameGrant.yaml b/.github/workflows/test-integration-v2-TestOIDCUsernameGrant.yaml new file mode 100644 index 0000000000..7545b57be3 --- /dev/null +++ b/.github/workflows/test-integration-v2-TestOIDCUsernameGrant.yaml @@ -0,0 +1,57 @@ +# DO NOT EDIT, generated with cmd/gh-action-integration-generator/main.go +# To regenerate, run "go generate" in cmd/gh-action-integration-generator/ + +name: Integration Test v2 - TestOIDCUsernameGrant + +on: [pull_request] + +concurrency: + group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v34 + with: + files: | + *.nix + go.* + **/*.go + integration_test/ + config-example.yaml + + - uses: cachix/install-nix-action@v18 + if: ${{ env.ACT }} || steps.changed-files.outputs.any_changed == 'true' + + - name: Run general integration tests + if: steps.changed-files.outputs.any_changed == 'true' + run: | + nix develop --command -- docker run \ + --tty --rm \ + --volume ~/.cache/hs-integration-go:/go \ + --name headscale-test-suite \ + --volume $PWD:$PWD -w $PWD/integration \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --volume $PWD/control_logs:/tmp/control \ + golang:1 \ + go test ./... \ + -tags ts2019 \ + -failfast \ + -timeout 120m \ + -parallel 1 \ + -run "^TestOIDCUsernameGrant$" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: logs + path: "control_logs/*.log" From a6423839ef36d8e91293b906eb25013ab974cdee Mon Sep 17 00:00:00 2001 From: Christopher Johnstone Date: Mon, 24 Apr 2023 10:59:02 -0400 Subject: [PATCH 4/4] Updated gold compare configs and fixed lint styles --- cmd/headscale/cli/mockoidc.go | 13 ++++++------- integration/auth_oidc_test.go | 8 ++++---- integration_test/etc/alt-config.dump.gold.yaml | 1 + integration_test/etc/alt-env-config.dump.gold.yaml | 1 + integration_test/etc/config.dump.gold.yaml | 1 + 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cmd/headscale/cli/mockoidc.go b/cmd/headscale/cli/mockoidc.go index 1245d3d6f8..bc2d489747 100644 --- a/cmd/headscale/cli/mockoidc.go +++ b/cmd/headscale/cli/mockoidc.go @@ -65,17 +65,16 @@ func mockOIDC() error { } accessTTL = newTTL } - + mockUsers := os.Getenv("MOCKOIDC_USERS") users := []mockoidc.User{} if mockUsers != "" { - user_strings := strings.Split(mockUsers, ",") - user_re := regexp.MustCompile(`^\s*(?P\S+)\s*<(?P\S+@\S+)>\s*$`) - for _, v := range user_strings { - match := user_re.FindStringSubmatch(v) + userStrings := strings.Split(mockUsers, ",") + userRe := regexp.MustCompile(`^\s*(?P\S+)\s*<(?P\S+@\S+)>\s*$`) + for _, v := range userStrings { + match := userRe.FindStringSubmatch(v) if match != nil { - fmt.Println("Success!", match[1], match[2]) - // Use the default mockoidc entries for other entries + // Use the default mockoidc claims for other entries users = append(users, &mockoidc.MockUser{ Subject: "1234567890", Email: match[2], diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index 78d68ab304..f04e5a37a4 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -58,9 +58,9 @@ func TestOIDCUsernameGrant(t *testing.T) { for i := range users { users[i] = "test-user " } - user_str := strings.Join(users, ", ") + userStr := strings.Join(users, ", ") - oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL, user_str) + oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL, userStr) if err != nil { t.Errorf("failed to run mock OIDC server: %s", err) } @@ -146,9 +146,9 @@ func TestOIDCEmailGrant(t *testing.T) { for i := range users { users[i] = "test-user " } - user_str := strings.Join(users, ", ") + userStr := strings.Join(users, ", ") - oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL, user_str) + oidcConfig, err := scenario.runMockOIDC(defaultAccessTTL, userStr) if err != nil { t.Errorf("failed to run mock OIDC server: %s", err) } diff --git a/integration_test/etc/alt-config.dump.gold.yaml b/integration_test/etc/alt-config.dump.gold.yaml index c0665b30a1..aeb33a4390 100644 --- a/integration_test/etc/alt-config.dump.gold.yaml +++ b/integration_test/etc/alt-config.dump.gold.yaml @@ -44,6 +44,7 @@ oidc: - profile - email strip_email_domain: true + use_username_claim: false use_expiry_from_token: false private_key_path: private.key noise: diff --git a/integration_test/etc/alt-env-config.dump.gold.yaml b/integration_test/etc/alt-env-config.dump.gold.yaml index 0be5d9ed3a..b839660479 100644 --- a/integration_test/etc/alt-env-config.dump.gold.yaml +++ b/integration_test/etc/alt-env-config.dump.gold.yaml @@ -43,6 +43,7 @@ oidc: - profile - email strip_email_domain: true + use_username_claim: false use_expiry_from_token: false private_key_path: private.key noise: diff --git a/integration_test/etc/config.dump.gold.yaml b/integration_test/etc/config.dump.gold.yaml index e6a822a52e..683ed0aca9 100644 --- a/integration_test/etc/config.dump.gold.yaml +++ b/integration_test/etc/config.dump.gold.yaml @@ -44,6 +44,7 @@ oidc: - profile - email strip_email_domain: true + use_username_claim: false use_expiry_from_token: false private_key_path: private.key noise: