loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'feat: add synchronization for SSH keys for OpenID Connect' (#6232) from Maks1mS/forgejo:feat/add-oidc-ssh-keys into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6232
Reviewed-by: Gusted <gusted@noreply.codeberg.org>

Gusted db7be1a1 d071c09b

+232 -27
+1
routers/web/admin/auths.go
··· 197 197 CustomURLMapping: customURLMapping, 198 198 IconURL: form.Oauth2IconURL, 199 199 Scopes: scopes, 200 + AttributeSSHPublicKey: form.Oauth2AttributeSSHPublicKey, 200 201 RequiredClaimName: form.Oauth2RequiredClaimName, 201 202 RequiredClaimValue: form.Oauth2RequiredClaimValue, 202 203 SkipLocalTwoFA: form.SkipLocalTwoFA,
+55
routers/web/auth/oauth.go
··· 17 17 "sort" 18 18 "strings" 19 19 20 + asymkey_model "code.gitea.io/gitea/models/asymkey" 20 21 "code.gitea.io/gitea/models/auth" 21 22 org_model "code.gitea.io/gitea/models/organization" 22 23 user_model "code.gitea.io/gitea/models/user" ··· 1183 1184 } 1184 1185 } 1185 1186 1187 + func getSSHKeys(source *oauth2.Source, gothUser *goth.User) ([]string, error) { 1188 + key := source.AttributeSSHPublicKey 1189 + value, exists := gothUser.RawData[key] 1190 + if !exists { 1191 + return []string{}, nil 1192 + } 1193 + 1194 + rawSlice, ok := value.([]any) 1195 + if !ok { 1196 + return nil, fmt.Errorf("unexpected type for SSH public key, expected []interface{} but got %T", value) 1197 + } 1198 + 1199 + sshKeys := make([]string, 0, len(rawSlice)) 1200 + for i, v := range rawSlice { 1201 + str, ok := v.(string) 1202 + if !ok { 1203 + return nil, fmt.Errorf("unexpected element type at index %d in SSH public key array, expected string but got %T", i, v) 1204 + } 1205 + sshKeys = append(sshKeys, str) 1206 + } 1207 + 1208 + return sshKeys, nil 1209 + } 1210 + 1211 + func updateSSHPubIfNeed( 1212 + ctx *context.Context, 1213 + authSource *auth.Source, 1214 + fetchedUser *goth.User, 1215 + user *user_model.User, 1216 + ) error { 1217 + oauth2Source := authSource.Cfg.(*oauth2.Source) 1218 + 1219 + if oauth2Source.ProvidesSSHKeys() { 1220 + sshKeys, err := getSSHKeys(oauth2Source, fetchedUser) 1221 + if err != nil { 1222 + return err 1223 + } 1224 + 1225 + if asymkey_model.SynchronizePublicKeys(ctx, user, authSource, sshKeys) { 1226 + err = asymkey_model.RewriteAllPublicKeys(ctx) 1227 + if err != nil { 1228 + return err 1229 + } 1230 + } 1231 + } 1232 + 1233 + return nil 1234 + } 1235 + 1186 1236 func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model.User, gothUser goth.User) { 1187 1237 updateAvatarIfNeed(ctx, gothUser.AvatarURL, u) 1238 + err := updateSSHPubIfNeed(ctx, source, &gothUser, u) 1239 + if err != nil { 1240 + ctx.ServerError("updateSSHPubIfNeed", err) 1241 + return 1242 + } 1188 1243 1189 1244 needs2FA := false 1190 1245 if !source.Cfg.(*oauth2.Source).SkipLocalTwoFA {
+4
services/auth/source/oauth2/providers_base.go
··· 48 48 return nil 49 49 } 50 50 51 + func (b *BaseProvider) CanProvideSSHKeys() bool { 52 + return false 53 + } 54 + 51 55 var _ Provider = &BaseProvider{}
+4
services/auth/source/oauth2/providers_openid.go
··· 51 51 return nil 52 52 } 53 53 54 + func (o *OpenIDProvider) CanProvideSSHKeys() bool { 55 + return true 56 + } 57 + 54 58 var _ GothProvider = &OpenIDProvider{} 55 59 56 60 func init() {
+17 -9
services/auth/source/oauth2/source.go
··· 4 4 package oauth2 5 5 6 6 import ( 7 + "strings" 8 + 7 9 "code.gitea.io/gitea/models/auth" 8 10 "code.gitea.io/gitea/modules/json" 9 11 ) ··· 17 19 CustomURLMapping *CustomURLMapping 18 20 IconURL string 19 21 20 - Scopes []string 21 - RequiredClaimName string 22 - RequiredClaimValue string 23 - GroupClaimName string 24 - AdminGroup string 25 - GroupTeamMap string 26 - GroupTeamMapRemoval bool 27 - RestrictedGroup string 28 - SkipLocalTwoFA bool `json:",omitempty"` 22 + Scopes []string 23 + AttributeSSHPublicKey string 24 + RequiredClaimName string 25 + RequiredClaimValue string 26 + GroupClaimName string 27 + AdminGroup string 28 + GroupTeamMap string 29 + GroupTeamMapRemoval bool 30 + RestrictedGroup string 31 + SkipLocalTwoFA bool `json:",omitempty"` 29 32 30 33 // reference to the authSource 31 34 authSource *auth.Source ··· 39 42 // ToDB exports an SMTPConfig to a serialized format. 40 43 func (source *Source) ToDB() ([]byte, error) { 41 44 return json.Marshal(source) 45 + } 46 + 47 + // ProvidesSSHKeys returns if this source provides SSH Keys 48 + func (source *Source) ProvidesSSHKeys() bool { 49 + return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 42 50 } 43 51 44 52 // SetAuthSource sets the related AuthSource
+1
services/forms/auth_form.go
··· 75 75 Oauth2RestrictedGroup string 76 76 Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"` 77 77 Oauth2GroupTeamMapRemoval bool 78 + Oauth2AttributeSSHPublicKey string 78 79 SkipLocalTwoFA bool 79 80 SSPIAutoCreateUsers bool 80 81 SSPIAutoActivateUsers bool
+17 -8
templates/admin/auth/edit.tmpl
··· 326 326 <input id="oauth2_tenant" name="oauth2_tenant" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.Tenant}}{{end}}"> 327 327 </div> 328 328 329 - {{range .OAuth2Providers}}{{if .CustomURLSettings}} 330 - <input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true"> 331 - <input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden"> 332 - <input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden"> 333 - <input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden"> 334 - <input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden"> 335 - <input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden"> 336 - {{end}}{{end}} 329 + {{range .OAuth2Providers}} 330 + {{if .CustomURLSettings}} 331 + <input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true"> 332 + <input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden"> 333 + <input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden"> 334 + <input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden"> 335 + <input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden"> 336 + <input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden"> 337 + {{end}} 338 + {{if .CanProvideSSHKeys}} 339 + <input id="{{.Name}}_canProvideSSHKeys" type="hidden"> 340 + {{end}} 341 + {{end}} 337 342 338 343 <div class="field"> 339 344 <label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label> 340 345 <input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{StringUtils.Join $cfg.Scopes ","}}{{end}}"> 346 + </div> 347 + <div class="oauth2_attribute_ssh_public_key field"> 348 + <label for="oauth2_attribute_ssh_public_key">{{ctx.Locale.Tr "admin.auths.attribute_ssh_public_key"}}</label> 349 + <input id="oauth2_attribute_ssh_public_key" name="oauth2_attribute_ssh_public_key" value="{{$cfg.AttributeSSHPublicKey}}" placeholder="sshpubkey"> 341 350 </div> 342 351 <div class="field"> 343 352 <label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
+17 -9
templates/admin/auth/source/oauth.tmpl
··· 63 63 <input id="oauth2_tenant" name="oauth2_tenant" value="{{.oauth2_tenant}}"> 64 64 </div> 65 65 66 - {{range .OAuth2Providers}}{{if .CustomURLSettings}} 67 - <input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true"> 68 - <input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden"> 69 - <input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden"> 70 - <input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden"> 71 - <input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden"> 72 - <input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden"> 73 - {{end}}{{end}} 74 - 66 + {{range .OAuth2Providers}} 67 + {{if .CustomURLSettings}} 68 + <input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true"> 69 + <input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden"> 70 + <input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden"> 71 + <input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden"> 72 + <input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden"> 73 + <input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden"> 74 + {{end}} 75 + {{if .CanProvideSSHKeys}} 76 + <input id="{{.Name}}_canProvideSSHKeys" type="hidden"> 77 + {{end}} 78 + {{end}} 75 79 <div class="field"> 76 80 <label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label> 77 81 <input id="oauth2_scopes" name="oauth2_scopes" value="{{.oauth2_scopes}}"> 82 + </div> 83 + <div class="oauth2_attribute_ssh_public_key field"> 84 + <label for="oauth2_attribute_ssh_public_key">{{ctx.Locale.Tr "admin.auths.attribute_ssh_public_key"}}</label> 85 + <input id="oauth2_attribute_ssh_public_key" name="oauth2_attribute_ssh_public_key" value="{{.attribute_ssh_public_key}}" placeholder="sshpubkey"> 78 86 </div> 79 87 <div class="field"> 80 88 <label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
+111
tests/integration/oauth_test.go
··· 691 691 assert.Equal(t, "/login/oauth/authorize?redirect_uri=https://translate.example.org", test.RedirectURL(resp)) 692 692 } 693 693 694 + func setupMockOIDCServer() *httptest.Server { 695 + var mockServer *httptest.Server 696 + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 697 + switch r.URL.Path { 698 + case "/.well-known/openid-configuration": 699 + w.WriteHeader(http.StatusOK) 700 + w.Write([]byte(`{ 701 + "issuer": "` + mockServer.URL + `", 702 + "authorization_endpoint": "` + mockServer.URL + `/authorize", 703 + "token_endpoint": "` + mockServer.URL + `/token", 704 + "userinfo_endpoint": "` + mockServer.URL + `/userinfo" 705 + }`)) 706 + default: 707 + http.NotFound(w, r) 708 + } 709 + })) 710 + return mockServer 711 + } 712 + 713 + func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) { 714 + defer tests.PrepareTestEnv(t)() 715 + mockServer := setupMockOIDCServer() 716 + defer mockServer.Close() 717 + 718 + sourceName := "oidc" 719 + authPayload := authSourcePayloadOpenIDConnect(sourceName, mockServer.URL+"/") 720 + authPayload["oauth2_attribute_ssh_public_key"] = "sshpubkey" 721 + authSource := addAuthSource(t, authPayload) 722 + 723 + userID := "5678" 724 + user := &user_model.User{ 725 + Name: "oidc.user", 726 + Email: "oidc.user@example.com", 727 + Passwd: "oidc.userpassword", 728 + Type: user_model.UserTypeIndividual, 729 + LoginType: auth_model.OAuth2, 730 + LoginSource: authSource.ID, 731 + LoginName: userID, 732 + IsActive: true, 733 + } 734 + defer createUser(context.Background(), t, user)() 735 + 736 + for _, tt := range []struct { 737 + name string 738 + rawData map[string]any 739 + parsedKeySets []string 740 + }{ 741 + { 742 + name: "Add keys", 743 + rawData: map[string]any{ 744 + "sshpubkey": []any{ 745 + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINDRDoephkaFELacrNNe2fqAwedhRB1MKOpLEHlPuczO nocomment", 746 + }, 747 + }, 748 + parsedKeySets: []string{ 749 + "SHA256:X/mW7JUQ8J8yhrKBbZ/pJni8qx7zPA1DTFsi8ftpDwg", 750 + }, 751 + }, 752 + { 753 + name: "Update keys", 754 + rawData: map[string]any{ 755 + "sshpubkey": []any{ 756 + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMLLMOLFMouSJmzOASKKv178d+7op4utSxcugF9tVVch nocomment", 757 + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGyDh9sg1IGQGa0U363wcGXrDlGBhZI3UHvS7we/0d+T nocomment", 758 + }, 759 + }, 760 + parsedKeySets: []string{ 761 + "SHA256:gsyG4JNmY5XoLBK5lSzuwD3EXcaDBiDKBkqDkpQTH6Q", 762 + "SHA256:bbEKB1Qpumgk6QrgiN6t/kIvtUZvIQ8rqQBz8yYPzYw", 763 + }, 764 + }, 765 + { 766 + name: "Remove keys", 767 + rawData: map[string]any{ 768 + "sshpubkey": []any{}, 769 + }, 770 + parsedKeySets: []string{}, 771 + }, 772 + } { 773 + t.Run(tt.name, func(t *testing.T) { 774 + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { 775 + return goth.User{ 776 + Provider: sourceName, 777 + UserID: userID, 778 + Email: user.Email, 779 + RawData: tt.rawData, 780 + }, nil 781 + })() 782 + 783 + session := emptyTestSession(t) 784 + 785 + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", sourceName)) 786 + resp := session.MakeRequest(t, req, http.StatusSeeOther) 787 + assert.Equal(t, "/", test.RedirectURL(resp)) 788 + 789 + req = NewRequest(t, "GET", "/user/settings/keys") 790 + resp = session.MakeRequest(t, req, http.StatusOK) 791 + 792 + htmlDoc := NewHTMLParser(t, resp.Body) 793 + divs := htmlDoc.doc.Find("#keys-ssh .flex-item .flex-item-body:not(:last-child)") 794 + 795 + syncedKeys := make([]string, divs.Length()) 796 + for i := 0; i < divs.Length(); i++ { 797 + syncedKeys[i] = strings.TrimSpace(divs.Eq(i).Text()) 798 + } 799 + 800 + assert.ElementsMatch(t, tt.parsedKeySets, syncedKeys, "Unequal number of keys") 801 + }) 802 + } 803 + } 804 + 694 805 func TestSignUpViaOAuthWithMissingFields(t *testing.T) { 695 806 defer tests.PrepareTestEnv(t)() 696 807 // enable auto-creation of accounts via OAuth2
+5 -1
web_src/js/features/admin/common.js
··· 62 62 } 63 63 64 64 function onOAuth2Change(applyDefaultValues) { 65 - hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url'); 65 + hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url, .oauth2_attribute_ssh_public_key'); 66 66 for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input[required]')) { 67 67 input.removeAttribute('required'); 68 68 } ··· 84 84 showElem('.oauth2_use_custom_url'); 85 85 } 86 86 } 87 + } 88 + const canProvideSSHKeys = document.getElementById(`${provider}_canProvideSSHKeys`); 89 + if (canProvideSSHKeys) { 90 + showElem('.oauth2_attribute_ssh_public_key'); 87 91 } 88 92 onOAuth2UseCustomURLChange(applyDefaultValues); 89 93 }