mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-01-10 23:52:16 +01:00
feat: add synchronization for SSH keys with OpenID Connect
Co-authored-by: Kirill Kolmykov <cyberk1ra@ya.ru>
This commit is contained in:
parent
4bc0abac3c
commit
4500757acd
9 changed files with 158 additions and 27 deletions
|
@ -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,
|
||||
|
|
|
@ -48,4 +48,8 @@ func (b *BaseProvider) CustomURLSettings() *CustomURLSettings {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseProvider) CanProvideSSHKeys() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var _ Provider = &BaseProvider{}
|
||||
|
|
|
@ -51,6 +51,10 @@ func (o *OpenIDProvider) CustomURLSettings() *CustomURLSettings {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenIDProvider) CanProvideSSHKeys() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var _ GothProvider = &OpenIDProvider{}
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ type AuthenticationForm struct {
|
|||
Oauth2RestrictedGroup string
|
||||
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
|
||||
Oauth2GroupTeamMapRemoval bool
|
||||
Oauth2AttributeSSHPublicKey string
|
||||
SkipLocalTwoFA bool
|
||||
SSPIAutoCreateUsers bool
|
||||
SSPIAutoActivateUsers bool
|
||||
|
|
|
@ -326,19 +326,28 @@
|
|||
<input id="oauth2_tenant" name="oauth2_tenant" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.Tenant}}{{end}}">
|
||||
</div>
|
||||
|
||||
{{range .OAuth2Providers}}{{if .CustomURLSettings}}
|
||||
<input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
|
||||
<input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
|
||||
{{end}}{{end}}
|
||||
{{range .OAuth2Providers}}
|
||||
{{if .CustomURLSettings}}
|
||||
<input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
|
||||
<input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
|
||||
{{end}}
|
||||
{{if .CanProvideSSHKeys}}
|
||||
<input id="{{.Name}}_canProvideSSHKeys" type="hidden">
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<div class="field">
|
||||
<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
|
||||
<input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{StringUtils.Join $cfg.Scopes ","}}{{end}}">
|
||||
</div>
|
||||
<div class="oauth2_attribute_ssh_public_key field">
|
||||
<label for="oauth2_attribute_ssh_public_key">{{ctx.Locale.Tr "admin.auths.attribute_ssh_public_key"}}</label>
|
||||
<input id="oauth2_attribute_ssh_public_key" name="oauth2_attribute_ssh_public_key" value="{{$cfg.AttributeSSHPublicKey}}" placeholder="sshpubkey">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
|
||||
<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{$cfg.RequiredClaimName}}">
|
||||
|
|
|
@ -63,19 +63,27 @@
|
|||
<input id="oauth2_tenant" name="oauth2_tenant" value="{{.oauth2_tenant}}">
|
||||
</div>
|
||||
|
||||
{{range .OAuth2Providers}}{{if .CustomURLSettings}}
|
||||
<input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
|
||||
<input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
|
||||
{{end}}{{end}}
|
||||
|
||||
{{range .OAuth2Providers}}
|
||||
{{if .CustomURLSettings}}
|
||||
<input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
|
||||
<input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
|
||||
<input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
|
||||
{{end}}
|
||||
{{if .CanProvideSSHKeys}}
|
||||
<input id="{{.Name}}_canProvideSSHKeys" type="hidden">
|
||||
{{end}}
|
||||
{{end}}
|
||||
<div class="field">
|
||||
<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
|
||||
<input id="oauth2_scopes" name="oauth2_scopes" value="{{.oauth2_scopes}}">
|
||||
</div>
|
||||
<div class="oauth2_attribute_ssh_public_key field">
|
||||
<label for="oauth2_attribute_ssh_public_key">{{ctx.Locale.Tr "admin.auths.attribute_ssh_public_key"}}</label>
|
||||
<input id="oauth2_attribute_ssh_public_key" name="oauth2_attribute_ssh_public_key" value="{{.attribute_ssh_public_key}}" placeholder="sshpubkey">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
|
||||
<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{.oauth2_required_claim_name}}">
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue