From 4500757acd3fb3c5f4fea94ee71247eedc4013d6 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Mon, 9 Dec 2024 18:59:11 +0300 Subject: [PATCH] feat: add synchronization for SSH keys with OpenID Connect Co-authored-by: Kirill Kolmykov --- routers/web/admin/auths.go | 1 + services/auth/source/oauth2/providers_base.go | 4 + .../auth/source/oauth2/providers_openid.go | 4 + services/auth/source/oauth2/source.go | 26 ++++-- services/auth/source/oauth2/source_sync.go | 92 +++++++++++++++++++ services/forms/auth_form.go | 1 + templates/admin/auth/edit.tmpl | 25 +++-- templates/admin/auth/source/oauth.tmpl | 26 ++++-- web_src/js/features/admin/common.js | 6 +- 9 files changed, 158 insertions(+), 27 deletions(-) diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 799b7e8a84..dcdc8e6a2a 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -197,6 +197,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source { CustomURLMapping: customURLMapping, IconURL: form.Oauth2IconURL, Scopes: scopes, + AttributeSSHPublicKey: form.Oauth2AttributeSSHPublicKey, RequiredClaimName: form.Oauth2RequiredClaimName, RequiredClaimValue: form.Oauth2RequiredClaimValue, SkipLocalTwoFA: form.SkipLocalTwoFA, diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go index 9d4ab106e5..63318b84ef 100644 --- a/services/auth/source/oauth2/providers_base.go +++ b/services/auth/source/oauth2/providers_base.go @@ -48,4 +48,8 @@ func (b *BaseProvider) CustomURLSettings() *CustomURLSettings { return nil } +func (b *BaseProvider) CanProvideSSHKeys() bool { + return false +} + var _ Provider = &BaseProvider{} diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go index 285876d5ac..f606581271 100644 --- a/services/auth/source/oauth2/providers_openid.go +++ b/services/auth/source/oauth2/providers_openid.go @@ -51,6 +51,10 @@ func (o *OpenIDProvider) CustomURLSettings() *CustomURLSettings { return nil } +func (o *OpenIDProvider) CanProvideSSHKeys() bool { + return true +} + var _ GothProvider = &OpenIDProvider{} func init() { diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go index 3454c9ad55..fe4823e778 100644 --- a/services/auth/source/oauth2/source.go +++ b/services/auth/source/oauth2/source.go @@ -4,6 +4,8 @@ package oauth2 import ( + "strings" + "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/modules/json" ) @@ -17,15 +19,16 @@ type Source struct { CustomURLMapping *CustomURLMapping IconURL string - Scopes []string - RequiredClaimName string - RequiredClaimValue string - GroupClaimName string - AdminGroup string - GroupTeamMap string - GroupTeamMapRemoval bool - RestrictedGroup string - SkipLocalTwoFA bool `json:",omitempty"` + Scopes []string + AttributeSSHPublicKey string + RequiredClaimName string + RequiredClaimValue string + GroupClaimName string + AdminGroup string + GroupTeamMap string + GroupTeamMapRemoval bool + RestrictedGroup string + SkipLocalTwoFA bool `json:",omitempty"` // reference to the authSource authSource *auth.Source @@ -41,6 +44,11 @@ func (source *Source) ToDB() ([]byte, error) { return json.Marshal(source) } +// ProvidesSSHKeys returns if this source provides SSH Keys +func (source *Source) ProvidesSSHKeys() bool { + return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 +} + // SetAuthSource sets the related AuthSource func (source *Source) SetAuthSource(authSource *auth.Source) { source.authSource = authSource diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go index 5e30313c8f..667c0957fc 100644 --- a/services/auth/source/oauth2/source_sync.go +++ b/services/auth/source/oauth2/source_sync.go @@ -5,15 +5,20 @@ package oauth2 import ( "context" + "fmt" "time" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" "github.com/markbates/goth" + "github.com/markbates/goth/providers/openidConnect" "golang.org/x/oauth2" + + asymkey_model "code.gitea.io/gitea/models/asymkey" ) // Sync causes this OAuth2 source to synchronize its users with the db. @@ -108,7 +113,94 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us u.RefreshToken = token.RefreshToken } + needUserFetch := source.ProvidesSSHKeys() + + if needUserFetch { + fetchedUser, err := fetchUser(provider, token) + if err != nil { + log.Error("fetchUser: %v", err) + } else { + err = updateSSHKeys(ctx, source, user, &fetchedUser) + if err != nil { + log.Error("updateSshKeys: %v", err) + } + } + } + err = user_model.UpdateExternalUserByExternalID(ctx, u) return err } + +func fetchUser(provider goth.Provider, token *oauth2.Token) (goth.User, error) { + state, err := util.CryptoRandomString(40) + if err != nil { + return goth.User{}, err + } + + session, err := provider.BeginAuth(state) + if err != nil { + return goth.User{}, err + } + + if s, ok := session.(*openidConnect.Session); ok { + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + s.IDToken = token.Extra("id_token").(string) + } + + gothUser, err := provider.FetchUser(session) + if err != nil { + return goth.User{}, err + } + + return gothUser, nil +} + +func updateSSHKeys( + ctx context.Context, + source *Source, + user *user_model.User, + fetchedUser *goth.User, +) error { + if source.ProvidesSSHKeys() { + sshKeys, err := getSSHKeys(source, fetchedUser) + if err != nil { + return err + } + + if asymkey_model.SynchronizePublicKeys(ctx, user, source.authSource, sshKeys) { + err = asymkey_model.RewriteAllPublicKeys(ctx) + if err != nil { + return err + } + } + } + + return nil +} + +func getSSHKeys(source *Source, gothUser *goth.User) ([]string, error) { + key := source.AttributeSSHPublicKey + value, exists := gothUser.RawData[key] + if !exists { + return nil, fmt.Errorf("attribute '%s' not found in user data", key) + } + + rawSlice, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf("unexpected type for SSH public key, expected []interface{} but got %T", value) + } + + sshKeys := make([]string, 0, len(rawSlice)) + for i, v := range rawSlice { + str, ok := v.(string) + if !ok { + return nil, fmt.Errorf("unexpected element type at index %d in SSH public key array, expected string but got %T", i, v) + } + sshKeys = append(sshKeys, str) + } + + return sshKeys, nil +} diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go index f0da63155a..39aae51756 100644 --- a/services/forms/auth_form.go +++ b/services/forms/auth_form.go @@ -75,6 +75,7 @@ type AuthenticationForm struct { Oauth2RestrictedGroup string Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"` Oauth2GroupTeamMapRemoval bool + Oauth2AttributeSSHPublicKey string SkipLocalTwoFA bool SSPIAutoCreateUsers bool SSPIAutoActivateUsers bool diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index a8b2049f92..84fefc0484 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -326,19 +326,28 @@ - {{range .OAuth2Providers}}{{if .CustomURLSettings}} - - - - - - - {{end}}{{end}} + {{range .OAuth2Providers}} + {{if .CustomURLSettings}} + + + + + + + {{end}} + {{if .CanProvideSSHKeys}} + + {{end}} + {{end}}
+
+ + +
diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl index 0560cc8256..7d0a64d269 100644 --- a/templates/admin/auth/source/oauth.tmpl +++ b/templates/admin/auth/source/oauth.tmpl @@ -63,19 +63,27 @@
- {{range .OAuth2Providers}}{{if .CustomURLSettings}} - - - - - - - {{end}}{{end}} - + {{range .OAuth2Providers}} + {{if .CustomURLSettings}} + + + + + + + {{end}} + {{if .CanProvideSSHKeys}} + + {{end}} + {{end}}
+
+ + +
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js index 1a5bd6e490..a42d8261f1 100644 --- a/web_src/js/features/admin/common.js +++ b/web_src/js/features/admin/common.js @@ -62,7 +62,7 @@ export function initAdminCommon() { } function onOAuth2Change(applyDefaultValues) { - hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url'); + hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url, .oauth2_attribute_ssh_public_key'); for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input[required]')) { input.removeAttribute('required'); } @@ -85,6 +85,10 @@ export function initAdminCommon() { } } } + const canProvideSSHKeys = document.getElementById(`${provider}_canProvideSSHKeys`); + if (canProvideSSHKeys) { + showElem('.oauth2_attribute_ssh_public_key'); + } onOAuth2UseCustomURLChange(applyDefaultValues); }