feat: add synchronization for SSH keys with OpenID Connect

Co-authored-by:  Kirill Kolmykov <cyberk1ra@ya.ru>
This commit is contained in:
Maxim Slipenko 2024-12-09 18:59:11 +03:00
parent 4bc0abac3c
commit 4500757acd
9 changed files with 158 additions and 27 deletions

View file

@ -48,4 +48,8 @@ func (b *BaseProvider) CustomURLSettings() *CustomURLSettings {
return nil
}
func (b *BaseProvider) CanProvideSSHKeys() bool {
return false
}
var _ Provider = &BaseProvider{}

View file

@ -51,6 +51,10 @@ func (o *OpenIDProvider) CustomURLSettings() *CustomURLSettings {
return nil
}
func (o *OpenIDProvider) CanProvideSSHKeys() bool {
return true
}
var _ GothProvider = &OpenIDProvider{}
func init() {

View file

@ -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

View file

@ -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
}

View file

@ -75,6 +75,7 @@ type AuthenticationForm struct {
Oauth2RestrictedGroup string
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
Oauth2GroupTeamMapRemoval bool
Oauth2AttributeSSHPublicKey string
SkipLocalTwoFA bool
SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool