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.

Fix #2512 /api/forgejo/v1/version auth check (#2582)

Add the same auth check and middlewares as the /v1/ API.
It require to export some variable from /v1 API, i am not sure if is the correct way to do

Co-authored-by: oliverpool <git@olivier.pfad.fr>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2582
Reviewed-by: oliverpool <oliverpool@noreply.codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Ada <ada@gnous.eu>
Co-committed-by: Ada <ada@gnous.eu>

authored by

Ada
oliverpool
Ada
and committed by
Earl Warren
41676a86 1e292e90

+238 -144
+4
routers/api/forgejo/v1/api.go
··· 5 5 6 6 import ( 7 7 "code.gitea.io/gitea/modules/web" 8 + "code.gitea.io/gitea/routers/api/shared" 8 9 ) 9 10 10 11 func Routes() *web.Route { 11 12 m := web.NewRoute() 13 + 14 + m.Use(shared.Middlewares()...) 15 + 12 16 forgejo := NewForgejo() 13 17 m.Get("", Root) 14 18 m.Get("/version", forgejo.GetVersion)
+152
routers/api/shared/middleware.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package shared 5 + 6 + import ( 7 + "net/http" 8 + 9 + auth_model "code.gitea.io/gitea/models/auth" 10 + "code.gitea.io/gitea/models/db" 11 + "code.gitea.io/gitea/modules/log" 12 + "code.gitea.io/gitea/modules/setting" 13 + "code.gitea.io/gitea/routers/common" 14 + "code.gitea.io/gitea/services/auth" 15 + "code.gitea.io/gitea/services/context" 16 + 17 + "github.com/go-chi/cors" 18 + ) 19 + 20 + func Middlewares() (stack []any) { 21 + stack = append(stack, securityHeaders()) 22 + 23 + if setting.CORSConfig.Enabled { 24 + stack = append(stack, cors.Handler(cors.Options{ 25 + AllowedOrigins: setting.CORSConfig.AllowDomain, 26 + AllowedMethods: setting.CORSConfig.Methods, 27 + AllowCredentials: setting.CORSConfig.AllowCredentials, 28 + AllowedHeaders: append([]string{"Authorization", "X-Gitea-OTP", "X-Forgejo-OTP"}, setting.CORSConfig.Headers...), 29 + MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), 30 + })) 31 + } 32 + return append(stack, 33 + context.APIContexter(), 34 + 35 + checkDeprecatedAuthMethods, 36 + // Get user from session if logged in. 37 + apiAuth(buildAuthGroup()), 38 + verifyAuthWithOptions(&common.VerifyOptions{ 39 + SignInRequired: setting.Service.RequireSignInView, 40 + }), 41 + ) 42 + } 43 + 44 + func buildAuthGroup() *auth.Group { 45 + group := auth.NewGroup( 46 + &auth.OAuth2{}, 47 + &auth.HTTPSign{}, 48 + &auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API 49 + ) 50 + if setting.Service.EnableReverseProxyAuthAPI { 51 + group.Add(&auth.ReverseProxy{}) 52 + } 53 + 54 + if setting.IsWindows && auth_model.IsSSPIEnabled(db.DefaultContext) { 55 + group.Add(&auth.SSPI{}) // it MUST be the last, see the comment of SSPI 56 + } 57 + 58 + return group 59 + } 60 + 61 + func apiAuth(authMethod auth.Method) func(*context.APIContext) { 62 + return func(ctx *context.APIContext) { 63 + ar, err := common.AuthShared(ctx.Base, nil, authMethod) 64 + if err != nil { 65 + ctx.Error(http.StatusUnauthorized, "APIAuth", err) 66 + return 67 + } 68 + ctx.Doer = ar.Doer 69 + ctx.IsSigned = ar.Doer != nil 70 + ctx.IsBasicAuth = ar.IsBasicAuth 71 + } 72 + } 73 + 74 + // verifyAuthWithOptions checks authentication according to options 75 + func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIContext) { 76 + return func(ctx *context.APIContext) { 77 + // Check prohibit login users. 78 + if ctx.IsSigned { 79 + if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm { 80 + ctx.Data["Title"] = ctx.Tr("auth.active_your_account") 81 + ctx.JSON(http.StatusForbidden, map[string]string{ 82 + "message": "This account is not activated.", 83 + }) 84 + return 85 + } 86 + if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin { 87 + log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr()) 88 + ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") 89 + ctx.JSON(http.StatusForbidden, map[string]string{ 90 + "message": "This account is prohibited from signing in, please contact your site administrator.", 91 + }) 92 + return 93 + } 94 + 95 + if ctx.Doer.MustChangePassword { 96 + ctx.JSON(http.StatusForbidden, map[string]string{ 97 + "message": "You must change your password. Change it at: " + setting.AppURL + "/user/change_password", 98 + }) 99 + return 100 + } 101 + } 102 + 103 + // Redirect to dashboard if user tries to visit any non-login page. 104 + if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" { 105 + ctx.Redirect(setting.AppSubURL + "/") 106 + return 107 + } 108 + 109 + if options.SignInRequired { 110 + if !ctx.IsSigned { 111 + // Restrict API calls with error message. 112 + ctx.JSON(http.StatusForbidden, map[string]string{ 113 + "message": "Only signed in user is allowed to call APIs.", 114 + }) 115 + return 116 + } else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm { 117 + ctx.Data["Title"] = ctx.Tr("auth.active_your_account") 118 + ctx.JSON(http.StatusForbidden, map[string]string{ 119 + "message": "This account is not activated.", 120 + }) 121 + return 122 + } 123 + } 124 + 125 + if options.AdminRequired { 126 + if !ctx.Doer.IsAdmin { 127 + ctx.JSON(http.StatusForbidden, map[string]string{ 128 + "message": "You have no permission to request for this.", 129 + }) 130 + return 131 + } 132 + } 133 + } 134 + } 135 + 136 + // check for and warn against deprecated authentication options 137 + func checkDeprecatedAuthMethods(ctx *context.APIContext) { 138 + if ctx.FormString("token") != "" || ctx.FormString("access_token") != "" { 139 + ctx.Resp.Header().Set("Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.") 140 + } 141 + } 142 + 143 + func securityHeaders() func(http.Handler) http.Handler { 144 + return func(next http.Handler) http.Handler { 145 + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 146 + // CORB: https://www.chromium.org/Home/chromium-security/corb-for-developers 147 + // http://stackoverflow.com/a/3146618/244009 148 + resp.Header().Set("x-content-type-options", "nosniff") 149 + next.ServeHTTP(resp, req) 150 + }) 151 + } 152 + }
+2 -133
routers/api/v1/api.go
··· 72 72 73 73 actions_model "code.gitea.io/gitea/models/actions" 74 74 auth_model "code.gitea.io/gitea/models/auth" 75 - "code.gitea.io/gitea/models/db" 76 75 issues_model "code.gitea.io/gitea/models/issues" 77 76 "code.gitea.io/gitea/models/organization" 78 77 "code.gitea.io/gitea/models/perm" ··· 84 83 "code.gitea.io/gitea/modules/setting" 85 84 api "code.gitea.io/gitea/modules/structs" 86 85 "code.gitea.io/gitea/modules/web" 86 + "code.gitea.io/gitea/routers/api/shared" 87 87 "code.gitea.io/gitea/routers/api/v1/activitypub" 88 88 "code.gitea.io/gitea/routers/api/v1/admin" 89 89 "code.gitea.io/gitea/routers/api/v1/misc" ··· 93 93 "code.gitea.io/gitea/routers/api/v1/repo" 94 94 "code.gitea.io/gitea/routers/api/v1/settings" 95 95 "code.gitea.io/gitea/routers/api/v1/user" 96 - "code.gitea.io/gitea/routers/common" 97 96 "code.gitea.io/gitea/services/auth" 98 97 "code.gitea.io/gitea/services/context" 99 98 "code.gitea.io/gitea/services/forms" ··· 101 100 _ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation 102 101 103 102 "gitea.com/go-chi/binding" 104 - "github.com/go-chi/cors" 105 103 ) 106 104 107 105 func sudo() func(ctx *context.APIContext) { ··· 731 729 } 732 730 } 733 731 734 - func buildAuthGroup() *auth.Group { 735 - group := auth.NewGroup( 736 - &auth.OAuth2{}, 737 - &auth.HTTPSign{}, 738 - &auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API 739 - ) 740 - if setting.Service.EnableReverseProxyAuthAPI { 741 - group.Add(&auth.ReverseProxy{}) 742 - } 743 - 744 - if setting.IsWindows && auth_model.IsSSPIEnabled(db.DefaultContext) { 745 - group.Add(&auth.SSPI{}) // it MUST be the last, see the comment of SSPI 746 - } 747 - 748 - return group 749 - } 750 - 751 - func apiAuth(authMethod auth.Method) func(*context.APIContext) { 752 - return func(ctx *context.APIContext) { 753 - ar, err := common.AuthShared(ctx.Base, nil, authMethod) 754 - if err != nil { 755 - ctx.Error(http.StatusUnauthorized, "APIAuth", err) 756 - return 757 - } 758 - ctx.Doer = ar.Doer 759 - ctx.IsSigned = ar.Doer != nil 760 - ctx.IsBasicAuth = ar.IsBasicAuth 761 - } 762 - } 763 - 764 - // verifyAuthWithOptions checks authentication according to options 765 - func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIContext) { 766 - return func(ctx *context.APIContext) { 767 - // Check prohibit login users. 768 - if ctx.IsSigned { 769 - if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm { 770 - ctx.Data["Title"] = ctx.Tr("auth.active_your_account") 771 - ctx.JSON(http.StatusForbidden, map[string]string{ 772 - "message": "This account is not activated.", 773 - }) 774 - return 775 - } 776 - if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin { 777 - log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr()) 778 - ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") 779 - ctx.JSON(http.StatusForbidden, map[string]string{ 780 - "message": "This account is prohibited from signing in, please contact your site administrator.", 781 - }) 782 - return 783 - } 784 - 785 - if ctx.Doer.MustChangePassword { 786 - ctx.JSON(http.StatusForbidden, map[string]string{ 787 - "message": "You must change your password. Change it at: " + setting.AppURL + "/user/change_password", 788 - }) 789 - return 790 - } 791 - } 792 - 793 - // Redirect to dashboard if user tries to visit any non-login page. 794 - if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" { 795 - ctx.Redirect(setting.AppSubURL + "/") 796 - return 797 - } 798 - 799 - if options.SignInRequired { 800 - if !ctx.IsSigned { 801 - // Restrict API calls with error message. 802 - ctx.JSON(http.StatusForbidden, map[string]string{ 803 - "message": "Only signed in user is allowed to call APIs.", 804 - }) 805 - return 806 - } else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm { 807 - ctx.Data["Title"] = ctx.Tr("auth.active_your_account") 808 - ctx.JSON(http.StatusForbidden, map[string]string{ 809 - "message": "This account is not activated.", 810 - }) 811 - return 812 - } 813 - } 814 - 815 - if options.AdminRequired { 816 - if !ctx.Doer.IsAdmin { 817 - ctx.JSON(http.StatusForbidden, map[string]string{ 818 - "message": "You have no permission to request for this.", 819 - }) 820 - return 821 - } 822 - } 823 - } 824 - } 825 - 826 732 func individualPermsChecker(ctx *context.APIContext) { 827 733 // org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked. 828 734 if ctx.ContextUser.IsIndividual() { ··· 841 747 } 842 748 } 843 749 844 - // check for and warn against deprecated authentication options 845 - func checkDeprecatedAuthMethods(ctx *context.APIContext) { 846 - if ctx.FormString("token") != "" || ctx.FormString("access_token") != "" { 847 - ctx.Resp.Header().Set("Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.") 848 - } 849 - } 850 - 851 750 // Routes registers all v1 APIs routes to web application. 852 751 func Routes() *web.Route { 853 752 m := web.NewRoute() 854 753 855 - m.Use(securityHeaders()) 856 - if setting.CORSConfig.Enabled { 857 - m.Use(cors.Handler(cors.Options{ 858 - AllowedOrigins: setting.CORSConfig.AllowDomain, 859 - AllowedMethods: setting.CORSConfig.Methods, 860 - AllowCredentials: setting.CORSConfig.AllowCredentials, 861 - AllowedHeaders: append([]string{"Authorization", "X-Gitea-OTP", "X-Forgejo-OTP"}, setting.CORSConfig.Headers...), 862 - MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), 863 - })) 864 - } 865 - m.Use(context.APIContexter()) 866 - 867 - m.Use(checkDeprecatedAuthMethods) 868 - 869 - // Get user from session if logged in. 870 - m.Use(apiAuth(buildAuthGroup())) 871 - 872 - m.Use(verifyAuthWithOptions(&common.VerifyOptions{ 873 - SignInRequired: setting.Service.RequireSignInView, 874 - })) 754 + m.Use(shared.Middlewares()...) 875 755 876 756 m.Group("", func() { 877 757 // Miscellaneous (no scope required) ··· 1627 1507 1628 1508 return m 1629 1509 } 1630 - 1631 - func securityHeaders() func(http.Handler) http.Handler { 1632 - return func(next http.Handler) http.Handler { 1633 - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 1634 - // CORB: https://www.chromium.org/Home/chromium-security/corb-for-developers 1635 - // http://stackoverflow.com/a/3146618/244009 1636 - resp.Header().Set("x-content-type-options", "nosniff") 1637 - next.ServeHTTP(resp, req) 1638 - }) 1639 - } 1640 - }
+39 -5
tests/integration/api_forgejo_version_test.go
··· 7 7 "net/http" 8 8 "testing" 9 9 10 + auth_model "code.gitea.io/gitea/models/auth" 11 + "code.gitea.io/gitea/modules/setting" 12 + "code.gitea.io/gitea/modules/test" 13 + "code.gitea.io/gitea/routers" 10 14 v1 "code.gitea.io/gitea/routers/api/forgejo/v1" 11 15 "code.gitea.io/gitea/tests" 12 16 ··· 16 20 func TestAPIForgejoVersion(t *testing.T) { 17 21 defer tests.PrepareTestEnv(t)() 18 22 19 - req := NewRequest(t, "GET", "/api/forgejo/v1/version") 20 - resp := MakeRequest(t, req, http.StatusOK) 23 + t.Run("Version", func(t *testing.T) { 24 + req := NewRequest(t, "GET", "/api/forgejo/v1/version") 25 + resp := MakeRequest(t, req, http.StatusOK) 21 26 22 - var version v1.Version 23 - DecodeJSON(t, resp, &version) 24 - assert.Equal(t, "1.0.0", *version.Version) 27 + var version v1.Version 28 + DecodeJSON(t, resp, &version) 29 + assert.Equal(t, "1.0.0", *version.Version) 30 + }) 31 + 32 + t.Run("Versions with REQUIRE_SIGNIN_VIEW enabled", func(t *testing.T) { 33 + defer test.MockVariableValue(&setting.Service.RequireSignInView, true)() 34 + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() 35 + 36 + t.Run("Get forgejo version without auth", func(t *testing.T) { 37 + defer tests.PrintCurrentTest(t)() 38 + 39 + // GET api without auth 40 + req := NewRequest(t, "GET", "/api/forgejo/v1/version") 41 + MakeRequest(t, req, http.StatusForbidden) 42 + }) 43 + 44 + t.Run("Get forgejo version without auth", func(t *testing.T) { 45 + defer tests.PrintCurrentTest(t)() 46 + username := "user1" 47 + session := loginUser(t, username) 48 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) 49 + 50 + // GET api with auth 51 + req := NewRequest(t, "GET", "/api/forgejo/v1/version").AddTokenAuth(token) 52 + resp := MakeRequest(t, req, http.StatusOK) 53 + 54 + var version v1.Version 55 + DecodeJSON(t, resp, &version) 56 + assert.Equal(t, "1.0.0", *version.Version) 57 + }) 58 + }) 25 59 }
+41 -6
tests/integration/version_test.go
··· 7 7 "net/http" 8 8 "testing" 9 9 10 + auth_model "code.gitea.io/gitea/models/auth" 10 11 "code.gitea.io/gitea/modules/setting" 11 12 "code.gitea.io/gitea/modules/structs" 13 + "code.gitea.io/gitea/modules/test" 14 + "code.gitea.io/gitea/routers" 12 15 "code.gitea.io/gitea/tests" 13 16 14 17 "github.com/stretchr/testify/assert" ··· 17 20 func TestVersion(t *testing.T) { 18 21 defer tests.PrepareTestEnv(t)() 19 22 20 - setting.AppVer = "test-version-1" 21 - req := NewRequest(t, "GET", "/api/v1/version") 22 - resp := MakeRequest(t, req, http.StatusOK) 23 + t.Run("Version", func(t *testing.T) { 24 + setting.AppVer = "test-version-1" 25 + req := NewRequest(t, "GET", "/api/v1/version") 26 + resp := MakeRequest(t, req, http.StatusOK) 27 + 28 + var version structs.ServerVersion 29 + DecodeJSON(t, resp, &version) 30 + assert.Equal(t, setting.AppVer, version.Version) 31 + }) 32 + 33 + t.Run("Versions with REQUIRE_SIGNIN_VIEW enabled", func(t *testing.T) { 34 + defer test.MockVariableValue(&setting.Service.RequireSignInView, true)() 35 + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() 36 + 37 + setting.AppVer = "test-version-1" 38 + 39 + t.Run("Get version without auth", func(t *testing.T) { 40 + defer tests.PrintCurrentTest(t)() 41 + 42 + // GET api without auth 43 + req := NewRequest(t, "GET", "/api/v1/version") 44 + MakeRequest(t, req, http.StatusForbidden) 45 + }) 46 + 47 + t.Run("Get version without auth", func(t *testing.T) { 48 + defer tests.PrintCurrentTest(t)() 49 + username := "user1" 50 + session := loginUser(t, username) 51 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) 52 + 53 + // GET api with auth 54 + req := NewRequest(t, "GET", "/api/v1/version").AddTokenAuth(token) 55 + resp := MakeRequest(t, req, http.StatusOK) 23 56 24 - var version structs.ServerVersion 25 - DecodeJSON(t, resp, &version) 26 - assert.Equal(t, setting.AppVer, version.Version) 57 + var version structs.ServerVersion 58 + DecodeJSON(t, resp, &version) 59 + assert.Equal(t, setting.AppVer, version.Version) 60 + }) 61 + }) 27 62 }