diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2021-06-10 01:53:16 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-09 19:53:16 +0200 |
commit | fb3ffeb18df6bb94bb3f69348a93398b05259174 (patch) | |
tree | aa56433e062bc68d2a118581a715ee324f025594 /modules | |
parent | da057996d584c633524406d69b424cbc3d4473eb (diff) | |
download | gitea-fb3ffeb18df6bb94bb3f69348a93398b05259174.tar.gz gitea-fb3ffeb18df6bb94bb3f69348a93398b05259174.zip |
Add sso.Group, context.Auth, context.APIAuth to allow auth special routes (#16086)
* Add sso.Group, context.Auth, context.APIAuth to allow auth special routes
* Remove unnecessary check
* Rename sso -> auth
* remove unused method of Auth interface
Diffstat (limited to 'modules')
-rw-r--r-- | modules/auth/sso/basic.go | 132 | ||||
-rw-r--r-- | modules/auth/sso/interface.go | 40 | ||||
-rw-r--r-- | modules/auth/sso/oauth2.go | 145 | ||||
-rw-r--r-- | modules/auth/sso/reverseproxy.go | 125 | ||||
-rw-r--r-- | modules/auth/sso/session.go | 48 | ||||
-rw-r--r-- | modules/auth/sso/sso.go | 154 | ||||
-rw-r--r-- | modules/auth/sso/sso_test.go | 128 | ||||
-rw-r--r-- | modules/auth/sso/sspi_windows.go | 246 | ||||
-rw-r--r-- | modules/auth/sso/user.go | 33 | ||||
-rw-r--r-- | modules/context/api.go | 36 | ||||
-rw-r--r-- | modules/context/context.go | 42 |
11 files changed, 44 insertions, 1085 deletions
diff --git a/modules/auth/sso/basic.go b/modules/auth/sso/basic.go deleted file mode 100644 index 5551288128..0000000000 --- a/modules/auth/sso/basic.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package sso - -import ( - "net/http" - "strings" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/web/middleware" -) - -// Ensure the struct implements the interface. -var ( - _ SingleSignOn = &Basic{} -) - -// Basic implements the SingleSignOn interface and authenticates requests (API requests -// only) by looking for Basic authentication data or "x-oauth-basic" token in the "Authorization" -// header. -type Basic struct { -} - -// Init does nothing as the Basic implementation does not need to allocate any resources -func (b *Basic) Init() error { - return nil -} - -// Free does nothing as the Basic implementation does not have to release any resources -func (b *Basic) Free() error { - return nil -} - -// IsEnabled returns true as this plugin is enabled by default and its not possible to disable -// it from settings. -func (b *Basic) IsEnabled() bool { - return true -} - -// VerifyAuthData extracts and validates Basic data (username and password/token) from the -// "Authorization" header of the request and returns the corresponding user object for that -// name/token on successful validation. -// Returns nil if header is empty or validation fails. -func (b *Basic) VerifyAuthData(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User { - - // Basic authentication should only fire on API, Download or on Git or LFSPaths - if middleware.IsInternalPath(req) || !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawOrLFSPath(req) { - return nil - } - - baHead := req.Header.Get("Authorization") - if len(baHead) == 0 { - return nil - } - - auths := strings.SplitN(baHead, " ", 2) - if len(auths) != 2 || (auths[0] != "Basic" && auths[0] != "basic") { - return nil - } - - uname, passwd, _ := base.BasicAuthDecode(auths[1]) - - // Check if username or password is a token - isUsernameToken := len(passwd) == 0 || passwd == "x-oauth-basic" - // Assume username is token - authToken := uname - if !isUsernameToken { - log.Trace("Basic Authorization: Attempting login for: %s", uname) - // Assume password is token - authToken = passwd - } else { - log.Trace("Basic Authorization: Attempting login with username as token") - } - - uid := CheckOAuthAccessToken(authToken) - if uid != 0 { - log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid) - - u, err := models.GetUserByID(uid) - if err != nil { - log.Error("GetUserByID: %v", err) - return nil - } - - store.GetData()["IsApiToken"] = true - return u - } - - token, err := models.GetAccessTokenBySHA(authToken) - if err == nil { - log.Trace("Basic Authorization: Valid AccessToken for user[%d]", uid) - u, err := models.GetUserByID(token.UID) - if err != nil { - log.Error("GetUserByID: %v", err) - return nil - } - - token.UpdatedUnix = timeutil.TimeStampNow() - if err = models.UpdateAccessToken(token); err != nil { - log.Error("UpdateAccessToken: %v", err) - } - - store.GetData()["IsApiToken"] = true - return u - } else if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) { - log.Error("GetAccessTokenBySha: %v", err) - } - - if !setting.Service.EnableBasicAuth { - return nil - } - - log.Trace("Basic Authorization: Attempting SignIn for %s", uname) - u, err := models.UserSignIn(uname, passwd) - if err != nil { - if !models.IsErrUserNotExist(err) { - log.Error("UserSignIn: %v", err) - } - return nil - } - - log.Trace("Basic Authorization: Logged in user %-v", u) - - return u -} diff --git a/modules/auth/sso/interface.go b/modules/auth/sso/interface.go deleted file mode 100644 index 9b1472f2b3..0000000000 --- a/modules/auth/sso/interface.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package sso - -import ( - "net/http" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/session" - "code.gitea.io/gitea/modules/web/middleware" -) - -// DataStore represents a data store -type DataStore middleware.DataStore - -// SessionStore represents a session store -type SessionStore session.Store - -// SingleSignOn represents a SSO authentication method (plugin) for HTTP requests. -type SingleSignOn interface { - // Init should be called exactly once before using any of the other methods, - // in order to allow the plugin to allocate necessary resources - Init() error - - // Free should be called exactly once before application closes, in order to - // give chance to the plugin to free any allocated resources - Free() error - - // IsEnabled checks if the current SSO method has been enabled in settings. - IsEnabled() bool - - // VerifyAuthData tries to verify the SSO authentication data contained in the request. - // If verification is successful returns either an existing user object (with id > 0) - // or a new user object (with id = 0) populated with the information that was found - // in the authentication data (username or email). - // Returns nil if verification fails. - VerifyAuthData(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User -} diff --git a/modules/auth/sso/oauth2.go b/modules/auth/sso/oauth2.go deleted file mode 100644 index b052b5599a..0000000000 --- a/modules/auth/sso/oauth2.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package sso - -import ( - "net/http" - "strings" - "time" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/web/middleware" -) - -// Ensure the struct implements the interface. -var ( - _ SingleSignOn = &OAuth2{} -) - -// CheckOAuthAccessToken returns uid of user from oauth token -func CheckOAuthAccessToken(accessToken string) int64 { - // JWT tokens require a "." - if !strings.Contains(accessToken, ".") { - return 0 - } - token, err := models.ParseOAuth2Token(accessToken) - if err != nil { - log.Trace("ParseOAuth2Token: %v", err) - return 0 - } - var grant *models.OAuth2Grant - if grant, err = models.GetOAuth2GrantByID(token.GrantID); err != nil || grant == nil { - return 0 - } - if token.Type != models.TypeAccessToken { - return 0 - } - if token.ExpiresAt < time.Now().Unix() || token.IssuedAt > time.Now().Unix() { - return 0 - } - return grant.UserID -} - -// OAuth2 implements the SingleSignOn interface and authenticates requests -// (API requests only) by looking for an OAuth token in query parameters or the -// "Authorization" header. -type OAuth2 struct { -} - -// Init does nothing as the OAuth2 implementation does not need to allocate any resources -func (o *OAuth2) Init() error { - return nil -} - -// Free does nothing as the OAuth2 implementation does not have to release any resources -func (o *OAuth2) Free() error { - return nil -} - -// userIDFromToken returns the user id corresponding to the OAuth token. -func (o *OAuth2) userIDFromToken(req *http.Request, store DataStore) int64 { - _ = req.ParseForm() - - // Check access token. - tokenSHA := req.Form.Get("token") - if len(tokenSHA) == 0 { - tokenSHA = req.Form.Get("access_token") - } - if len(tokenSHA) == 0 { - // Well, check with header again. - auHead := req.Header.Get("Authorization") - if len(auHead) > 0 { - auths := strings.Fields(auHead) - if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") { - tokenSHA = auths[1] - } - } - } - if len(tokenSHA) == 0 { - return 0 - } - - // Let's see if token is valid. - if strings.Contains(tokenSHA, ".") { - uid := CheckOAuthAccessToken(tokenSHA) - if uid != 0 { - store.GetData()["IsApiToken"] = true - } - return uid - } - t, err := models.GetAccessTokenBySHA(tokenSHA) - if err != nil { - if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) { - log.Error("GetAccessTokenBySHA: %v", err) - } - return 0 - } - t.UpdatedUnix = timeutil.TimeStampNow() - if err = models.UpdateAccessToken(t); err != nil { - log.Error("UpdateAccessToken: %v", err) - } - store.GetData()["IsApiToken"] = true - return t.UID -} - -// IsEnabled returns true as this plugin is enabled by default and its not possible -// to disable it from settings. -func (o *OAuth2) IsEnabled() bool { - return true -} - -// VerifyAuthData extracts the user ID from the OAuth token in the query parameters -// or the "Authorization" header and returns the corresponding user object for that ID. -// If verification is successful returns an existing user object. -// Returns nil if verification fails. -func (o *OAuth2) VerifyAuthData(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User { - if !models.HasEngine { - return nil - } - - if middleware.IsInternalPath(req) || !middleware.IsAPIPath(req) && !isAttachmentDownload(req) { - return nil - } - - id := o.userIDFromToken(req, store) - if id <= 0 { - return nil - } - log.Trace("OAuth2 Authorization: Found token for user[%d]", id) - - user, err := models.GetUserByID(id) - if err != nil { - if !models.IsErrUserNotExist(err) { - log.Error("GetUserByName: %v", err) - } - return nil - } - - log.Trace("OAuth2 Authorization: Logged in user %-v", user) - return user -} diff --git a/modules/auth/sso/reverseproxy.go b/modules/auth/sso/reverseproxy.go deleted file mode 100644 index 3fffee0320..0000000000 --- a/modules/auth/sso/reverseproxy.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package sso - -import ( - "net/http" - "strings" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/web/middleware" - - gouuid "github.com/google/uuid" -) - -// Ensure the struct implements the interface. -var ( - _ SingleSignOn = &ReverseProxy{} -) - -// ReverseProxy implements the SingleSignOn interface, but actually relies on -// a reverse proxy for authentication of users. -// On successful authentication the proxy is expected to populate the username in the -// "setting.ReverseProxyAuthUser" header. Optionally it can also populate the email of the -// user in the "setting.ReverseProxyAuthEmail" header. -type ReverseProxy struct { -} - -// getUserName extracts the username from the "setting.ReverseProxyAuthUser" header -func (r *ReverseProxy) getUserName(req *http.Request) string { - webAuthUser := strings.TrimSpace(req.Header.Get(setting.ReverseProxyAuthUser)) - if len(webAuthUser) == 0 { - return "" - } - return webAuthUser -} - -// Init does nothing as the ReverseProxy implementation does not need initialization -func (r *ReverseProxy) Init() error { - return nil -} - -// Free does nothing as the ReverseProxy implementation does not have to release resources -func (r *ReverseProxy) Free() error { - return nil -} - -// IsEnabled checks if EnableReverseProxyAuth setting is true -func (r *ReverseProxy) IsEnabled() bool { - return setting.Service.EnableReverseProxyAuth -} - -// VerifyAuthData extracts the username from the "setting.ReverseProxyAuthUser" header -// of the request and returns the corresponding user object for that name. -// Verification of header data is not performed as it should have already been done by -// the revese proxy. -// If a username is available in the "setting.ReverseProxyAuthUser" header an existing -// user object is returned (populated with username or email found in header). -// Returns nil if header is empty. -func (r *ReverseProxy) VerifyAuthData(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User { - username := r.getUserName(req) - if len(username) == 0 { - return nil - } - log.Trace("ReverseProxy Authorization: Found username: %s", username) - - user, err := models.GetUserByName(username) - if err != nil { - if !models.IsErrUserNotExist(err) || !r.isAutoRegisterAllowed() { - log.Error("GetUserByName: %v", err) - return nil - } - user = r.newUser(req) - } - - // Make sure requests to API paths, attachment downloads, git and LFS do not create a new session - if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawOrLFSPath(req) { - if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) { - handleSignIn(w, req, sess, user) - } - } - store.GetData()["IsReverseProxy"] = true - - log.Trace("ReverseProxy Authorization: Logged in user %-v", user) - return user -} - -// isAutoRegisterAllowed checks if EnableReverseProxyAutoRegister setting is true -func (r *ReverseProxy) isAutoRegisterAllowed() bool { - return setting.Service.EnableReverseProxyAutoRegister -} - -// newUser creates a new user object for the purpose of automatic registration -// and populates its name and email with the information present in request headers. -func (r *ReverseProxy) newUser(req *http.Request) *models.User { - username := r.getUserName(req) - if len(username) == 0 { - return nil - } - - email := gouuid.New().String() + "@localhost" - if setting.Service.EnableReverseProxyEmail { - webAuthEmail := req.Header.Get(setting.ReverseProxyAuthEmail) - if len(webAuthEmail) > 0 { - email = webAuthEmail - } - } - - user := &models.User{ - Name: username, - Email: email, - IsActive: true, - } - if err := models.CreateUser(user); err != nil { - // FIXME: should I create a system notice? - log.Error("CreateUser: %v", err) - return nil - } - - return user -} diff --git a/modules/auth/sso/session.go b/modules/auth/sso/session.go deleted file mode 100644 index 7a546577d8..0000000000 --- a/modules/auth/sso/session.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package sso - -import ( - "net/http" - - "code.gitea.io/gitea/models" -) - -// Ensure the struct implements the interface. -var ( - _ SingleSignOn = &Session{} -) - -// Session checks if there is a user uid stored in the session and returns the user -// object for that uid. -type Session struct { -} - -// Init does nothing as the Session implementation does not need to allocate any resources -func (s *Session) Init() error { - return nil -} - -// Free does nothing as the Session implementation does not have to release any resources -func (s *Session) Free() error { - return nil -} - -// IsEnabled returns true as this plugin is enabled by default and its not possible to disable -// it from settings. -func (s *Session) IsEnabled() bool { - return true -} - -// VerifyAuthData checks if there is a user uid stored in the session and returns the user -// object for that uid. -// Returns nil if there is no user uid stored in the session. -func (s *Session) VerifyAuthData(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User { - user := SessionUser(sess) - if user != nil { - return user - } - return nil -} diff --git a/modules/auth/sso/sso.go b/modules/auth/sso/sso.go deleted file mode 100644 index 8543ceb2ff..0000000000 --- a/modules/auth/sso/sso.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package sso - -import ( - "fmt" - "net/http" - "reflect" - "regexp" - "strings" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/web/middleware" -) - -// ssoMethods contains the list of SSO authentication plugins in the order they are expected to be -// executed. -// -// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored -// in the session (if there is a user id stored in session other plugins might return the user -// object for that id). -// -// The Session plugin is expected to be executed second, in order to skip authentication -// for users that have already signed in. -var ssoMethods = []SingleSignOn{ - &OAuth2{}, - &Basic{}, - &Session{}, - &ReverseProxy{}, -} - -// The purpose of the following three function variables is to let the linter know that -// those functions are not dead code and are actually being used -var ( - _ = handleSignIn -) - -// Methods returns the instances of all registered SSO methods -func Methods() []SingleSignOn { - return ssoMethods -} - -// Register adds the specified instance to the list of available SSO methods -func Register(method SingleSignOn) { - ssoMethods = append(ssoMethods, method) -} - -// Init should be called exactly once when the application starts to allow SSO plugins -// to allocate necessary resources -func Init() { - for _, method := range Methods() { - err := method.Init() - if err != nil { - log.Error("Could not initialize '%s' SSO method, error: %s", reflect.TypeOf(method).String(), err) - } - } -} - -// Free should be called exactly once when the application is terminating to allow SSO plugins -// to release necessary resources -func Free() { - for _, method := range Methods() { - err := method.Free() - if err != nil { - log.Error("Could not free '%s' SSO method, error: %s", reflect.TypeOf(method).String(), err) - } - } -} - -// SessionUser returns the user object corresponding to the "uid" session variable. -func SessionUser(sess SessionStore) *models.User { - // Get user ID - uid := sess.Get("uid") - if uid == nil { - return nil - } - log.Trace("Session Authorization: Found user[%d]", uid) - - id, ok := uid.(int64) - if !ok { - return nil - } - - // Get user object - user, err := models.GetUserByID(id) - if err != nil { - if !models.IsErrUserNotExist(err) { - log.Error("GetUserById: %v", err) - } - return nil - } - - log.Trace("Session Authorization: Logged in user %-v", user) - return user -} - -// isAttachmentDownload check if request is a file download (GET) with URL to an attachment -func isAttachmentDownload(req *http.Request) bool { - return strings.HasPrefix(req.URL.Path, "/attachments/") && req.Method == "GET" -} - -var gitRawPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|raw/)`) -var lfsPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`) - -func isGitRawOrLFSPath(req *http.Request) bool { - if gitRawPathRe.MatchString(req.URL.Path) { - return true - } - if setting.LFS.StartServer { - return lfsPathRe.MatchString(req.URL.Path) - } - return false -} - -// handleSignIn clears existing session variables and stores new ones for the specified user object -func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *models.User) { - _ = sess.Delete("openid_verified_uri") - _ = sess.Delete("openid_signin_remember") - _ = sess.Delete("openid_determined_email") - _ = sess.Delete("openid_determined_username") - _ = sess.Delete("twofaUid") - _ = sess.Delete("twofaRemember") - _ = sess.Delete("u2fChallenge") - _ = sess.Delete("linkAccount") - err := sess.Set("uid", user.ID) - if err != nil { - log.Error(fmt.Sprintf("Error setting session: %v", err)) - } - err = sess.Set("uname", user.Name) - if err != nil { - log.Error(fmt.Sprintf("Error setting session: %v", err)) - } - - // Language setting of the user overwrites the one previously set - // If the user does not have a locale set, we save the current one. - if len(user.Language) == 0 { - lc := middleware.Locale(resp, req) - user.Language = lc.Language() - if err := models.UpdateUserCols(user, "language"); err != nil { - log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", user.ID, user.Language)) - return - } - } - - middleware.SetLocaleCookie(resp, user.Language, 0) - - // Clear whatever CSRF has right now, force to generate a new one - middleware.DeleteCSRFCookie(resp) -} diff --git a/modules/auth/sso/sso_test.go b/modules/auth/sso/sso_test.go deleted file mode 100644 index e57788f35a..0000000000 --- a/modules/auth/sso/sso_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package sso - -import ( - "net/http" - "testing" - - "code.gitea.io/gitea/modules/setting" -) - -func Test_isGitRawOrLFSPath(t *testing.T) { - - tests := []struct { - path string - - want bool - }{ - { - "/owner/repo/git-upload-pack", - true, - }, - { - "/owner/repo/git-receive-pack", - true, - }, - { - "/owner/repo/info/refs", - true, - }, - { - "/owner/repo/HEAD", - true, - }, - { - "/owner/repo/objects/info/alternates", - true, - }, - { - "/owner/repo/objects/info/http-alternates", - true, - }, - { - "/owner/repo/objects/info/packs", - true, - }, - { - "/owner/repo/objects/info/blahahsdhsdkla", - true, - }, - { - "/owner/repo/objects/01/23456789abcdef0123456789abcdef01234567", - true, - }, - { - "/owner/repo/objects/pack/pack-123456789012345678921234567893124567894.pack", - true, - }, - { - "/owner/repo/objects/pack/pack-0123456789abcdef0123456789abcdef0123456.idx", - true, - }, - { - "/owner/repo/raw/branch/foo/fanaso", - true, - }, - { - "/owner/repo/stars", - false, - }, - { - "/notowner", - false, - }, - { - "/owner/repo", - false, - }, - { - "/owner/repo/commit/123456789012345678921234567893124567894", - false, - }, - } - lfsTests := []string{ - "/owner/repo/info/lfs/", - "/owner/repo/info/lfs/objects/batch", - "/owner/repo/info/lfs/objects/oid/filename", - "/owner/repo/info/lfs/objects/oid", - "/owner/repo/info/lfs/objects", - "/owner/repo/info/lfs/verify", - "/owner/repo/info/lfs/locks", - "/owner/repo/info/lfs/locks/verify", - "/owner/repo/info/lfs/locks/123/unlock", - } - - origLFSStartServer := setting.LFS.StartServer - - for _, tt := range tests { - t.Run(tt.path, func(t *testing.T) { - req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil) - setting.LFS.StartServer = false - if got := isGitRawOrLFSPath(req); got != tt.want { - t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want) - } - setting.LFS.StartServer = true - if got := isGitRawOrLFSPath(req); got != tt.want { - t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want) - } - }) - } - for _, tt := range lfsTests { - t.Run(tt, func(t *testing.T) { - req, _ := http.NewRequest("POST", tt, nil) - setting.LFS.StartServer = false - if got := isGitRawOrLFSPath(req); got != setting.LFS.StartServer { - t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawPathRe.MatchString(tt)) - } - setting.LFS.StartServer = true - if got := isGitRawOrLFSPath(req); got != setting.LFS.StartServer { - t.Errorf("isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer) - } - }) - } - setting.LFS.StartServer = origLFSStartServer -} diff --git a/modules/auth/sso/sspi_windows.go b/modules/auth/sso/sspi_windows.go deleted file mode 100644 index 2092a5e289..0000000000 --- a/modules/auth/sso/sspi_windows.go +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package sso - -import ( - "errors" - "net/http" - "strings" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/web/middleware" - - gouuid "github.com/google/uuid" - "github.com/quasoft/websspi" - "github.com/unrolled/render" -) - -const ( - tplSignIn base.TplName = "user/auth/signin" -) - -var ( - // sspiAuth is a global instance of the websspi authentication package, - // which is used to avoid acquiring the server credential handle on - // every request - sspiAuth *websspi.Authenticator - - // Ensure the struct implements the interface. - _ SingleSignOn = &SSPI{} -) - -// SSPI implements the SingleSignOn interface and authenticates requests -// via the built-in SSPI module in Windows for SPNEGO authentication. -// On successful authentication returns a valid user object. -// Returns nil if authentication fails. -type SSPI struct { - rnd *render.Render -} - -// Init creates a new global websspi.Authenticator object -func (s *SSPI) Init() error { - config := websspi.NewConfig() - var err error - sspiAuth, err = websspi.New(config) - if err != nil { - return err - } - s.rnd = render.New(render.Options{ - Extensions: []string{".tmpl"}, - Directory: "templates", - Funcs: templates.NewFuncMap(), - Asset: templates.GetAsset, - AssetNames: templates.GetAssetNames, - IsDevelopment: !setting.IsProd(), - }) - return nil -} - -// Free releases resources used by the global websspi.Authenticator object -func (s *SSPI) Free() error { - return sspiAuth.Free() -} - -// IsEnabled checks if there is an active SSPI authentication source -func (s *SSPI) IsEnabled() bool { - return models.IsSSPIEnabled() -} - -// VerifyAuthData uses SSPI (Windows implementation of SPNEGO) to authenticate the request. -// If authentication is successful, returs the corresponding user object. -// If negotiation should continue or authentication fails, immediately returns a 401 HTTP -// response code, as required by the SPNEGO protocol. -func (s *SSPI) VerifyAuthData(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User { - if !s.shouldAuthenticate(req) { - return nil - } - - cfg, err := s.getConfig() - if err != nil { - log.Error("could not get SSPI config: %v", err) - return nil - } - - log.Trace("SSPI Authorization: Attempting to authenticate") - userInfo, outToken, err := sspiAuth.Authenticate(req, w) - if err != nil { - log.Warn("Authentication failed with error: %v\n", err) - sspiAuth.AppendAuthenticateHeader(w, outToken) - - // Include the user login page in the 401 response to allow the user - // to login with another authentication method if SSPI authentication - // fails - store.GetData()["Flash"] = map[string]string{ - "ErrorMsg": err.Error(), - } - store.GetData()["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn - store.GetData()["EnableSSPI"] = true - - err := s.rnd.HTML(w, 401, string(tplSignIn), templates.BaseVars().Merge(store.GetData())) - if err != nil { - log.Error("%v", err) - } - - return nil - } - if outToken != "" { - sspiAuth.AppendAuthenticateHeader(w, outToken) - } - - username := sanitizeUsername(userInfo.Username, cfg) - if len(username) == 0 { - return nil - } - log.Info("Authenticated as %s\n", username) - - user, err := models.GetUserByName(username) - if err != nil { - if !models.IsErrUserNotExist(err) { - log.Error("GetUserByName: %v", err) - return nil - } - if !cfg.AutoCreateUsers { - log.Error("User '%s' not found", username) - return nil - } - user, err = s.newUser(username, cfg) - if err != nil { - log.Error("CreateUser: %v", err) - return nil - } - } - - // Make sure requests to API paths and PWA resources do not create a new session - if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) { - handleSignIn(w, req, sess, user) - } - - log.Trace("SSPI Authorization: Logged in user %-v", user) - return user -} - -// getConfig retrieves the SSPI configuration from login sources -func (s *SSPI) getConfig() (*models.SSPIConfig, error) { - sources, err := models.ActiveLoginSources(models.LoginSSPI) - if err != nil { - return nil, err - } - if len(sources) == 0 { - return nil, errors.New("no active login sources of type SSPI found") - } - if len(sources) > 1 { - return nil, errors.New("more than one active login source of type SSPI found") - } - return sources[0].SSPI(), nil -} - -func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) { - shouldAuth = false - path := strings.TrimSuffix(req.URL.Path, "/") - if path == "/user/login" { - if req.FormValue("user_name") != "" && req.FormValue("password") != "" { - shouldAuth = false - } else if req.FormValue("auth_with_sspi") == "1" { - shouldAuth = true - } - } else if middleware.IsInternalPath(req) { - shouldAuth = false - } else if middleware.IsAPIPath(req) || isAttachmentDownload(req) { - shouldAuth = true - } - return -} - -// newUser creates a new user object for the purpose of automatic registration -// and populates its name and email with the information present in request headers. -func (s *SSPI) newUser(username string, cfg *models.SSPIConfig) (*models.User, error) { - email := gouuid.New().String() + "@localhost.localdomain" - user := &models.User{ - Name: username, - Email: email, - KeepEmailPrivate: true, - Passwd: gouuid.New().String(), - IsActive: cfg.AutoActivateUsers, - Language: cfg.DefaultLanguage, - UseCustomAvatar: true, - Avatar: models.DefaultAvatarLink(), - EmailNotificationsPreference: models.EmailNotificationsDisabled, - } - if err := models.CreateUser(user); err != nil { - return nil, err - } - return user, nil -} - -// stripDomainNames removes NETBIOS domain name and separator from down-level logon names -// (eg. "DOMAIN\user" becomes "user"), and removes the UPN suffix (domain name) and separator -// from UPNs (eg. "user@domain.local" becomes "user") -func stripDomainNames(username string) string { - if strings.Contains(username, "\\") { - parts := strings.SplitN(username, "\\", 2) - if len(parts) > 1 { - username = parts[1] - } - } else if strings.Contains(username, "@") { - parts := strings.Split(username, "@") - if len(parts) > 1 { - username = parts[0] - } - } - return username -} - -func replaceSeparators(username string, cfg *models.SSPIConfig) string { - newSep := cfg.SeparatorReplacement - username = strings.ReplaceAll(username, "\\", newSep) - username = strings.ReplaceAll(username, "/", newSep) - username = strings.ReplaceAll(username, "@", newSep) - return username -} - -func sanitizeUsername(username string, cfg *models.SSPIConfig) string { - if len(username) == 0 { - return "" - } - if cfg.StripDomainNames { - username = stripDomainNames(username) - } - // Replace separators even if we have already stripped the domain name part, - // as the username can contain several separators: eg. "MICROSOFT\useremail@live.com" - username = replaceSeparators(username, cfg) - return username -} - -// init registers the SSPI auth method as the last method in the list. -// The SSPI plugin is expected to be executed last, as it returns 401 status code if negotiation -// fails (or if negotiation should continue), which would prevent other authentication methods -// to execute at all. -func init() { - Register(&SSPI{}) -} diff --git a/modules/auth/sso/user.go b/modules/auth/sso/user.go deleted file mode 100644 index 48eebb1e91..0000000000 --- a/modules/auth/sso/user.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package sso - -import ( - "net/http" - - "code.gitea.io/gitea/models" -) - -// SignedInUser returns the user object of signed user. -// It returns a bool value to indicate whether user uses basic auth or not. -func SignedInUser(req *http.Request, w http.ResponseWriter, ds DataStore, sess SessionStore) (*models.User, bool) { - if !models.HasEngine { - return nil, false - } - - // Try to sign in with each of the enabled plugins - for _, ssoMethod := range Methods() { - if !ssoMethod.IsEnabled() { - continue - } - user := ssoMethod.VerifyAuthData(req, w, ds, sess) - if user != nil { - _, isBasic := ssoMethod.(*Basic) - return user, isBasic - } - } - - return nil, false -} diff --git a/modules/context/api.go b/modules/context/api.go index cbd90c50e4..5068246745 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -14,11 +14,11 @@ import ( "strings" "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/auth/sso" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/auth" "gitea.com/go-chi/session" ) @@ -217,6 +217,26 @@ func (ctx *APIContext) CheckForOTP() { } } +// APIAuth converts auth.Auth as a middleware +func APIAuth(authMethod auth.Auth) func(*APIContext) { + return func(ctx *APIContext) { + // Get user from session if logged in. + ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + if ctx.User != nil { + ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == new(auth.Basic).Name() + ctx.IsSigned = true + ctx.Data["IsSigned"] = ctx.IsSigned + ctx.Data["SignedUser"] = ctx.User + ctx.Data["SignedUserID"] = ctx.User.ID + ctx.Data["SignedUserName"] = ctx.User.Name + ctx.Data["IsAdmin"] = ctx.User.IsAdmin + } else { + ctx.Data["SignedUserID"] = int64(0) + ctx.Data["SignedUserName"] = "" + } + } +} + // APIContexter returns apicontext as middleware func APIContexter() func(http.Handler) http.Handler { var csrfOpts = getCsrfOpts() @@ -250,20 +270,6 @@ func APIContexter() func(http.Handler) http.Handler { } } - // Get user from session if logged in. - ctx.User, ctx.IsBasicAuth = sso.SignedInUser(ctx.Req, ctx.Resp, &ctx, ctx.Session) - if ctx.User != nil { - ctx.IsSigned = true - ctx.Data["IsSigned"] = ctx.IsSigned - ctx.Data["SignedUser"] = ctx.User - ctx.Data["SignedUserID"] = ctx.User.ID - ctx.Data["SignedUserName"] = ctx.User.Name - ctx.Data["IsAdmin"] = ctx.User.IsAdmin - } else { - ctx.Data["SignedUserID"] = int64(0) - ctx.Data["SignedUserName"] = "" - } - ctx.Resp.Header().Set(`X-Frame-Options`, `SAMEORIGIN`) ctx.Data["CsrfToken"] = html.EscapeString(ctx.csrf.GetToken()) diff --git a/modules/context/context.go b/modules/context/context.go index d45e9ff87c..492b3f80de 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -21,7 +21,6 @@ import ( "time" "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/auth/sso" "code.gitea.io/gitea/modules/base" mc "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/log" @@ -29,6 +28,7 @@ import ( "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/auth" "gitea.com/go-chi/cache" "gitea.com/go-chi/session" @@ -605,6 +605,28 @@ func getCsrfOpts() CsrfOptions { } } +// Auth converts auth.Auth as a middleware +func Auth(authMethod auth.Auth) func(*Context) { + return func(ctx *Context) { + ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + if ctx.User != nil { + ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == new(auth.Basic).Name() + ctx.IsSigned = true + ctx.Data["IsSigned"] = ctx.IsSigned + ctx.Data["SignedUser"] = ctx.User + ctx.Data["SignedUserID"] = ctx.User.ID + ctx.Data["SignedUserName"] = ctx.User.Name + ctx.Data["IsAdmin"] = ctx.User.IsAdmin + } else { + ctx.Data["SignedUserID"] = int64(0) + ctx.Data["SignedUserName"] = "" + + // ensure the session uid is deleted + _ = ctx.Session.Delete("uid") + } + } +} + // Contexter initializes a classic context for a request. func Contexter() func(next http.Handler) http.Handler { var rnd = templates.HTMLRenderer() @@ -690,24 +712,6 @@ func Contexter() func(next http.Handler) http.Handler { } } - // Get user from session if logged in. - ctx.User, ctx.IsBasicAuth = sso.SignedInUser(ctx.Req, ctx.Resp, &ctx, ctx.Session) - - if ctx.User != nil { - ctx.IsSigned = true - ctx.Data["IsSigned"] = ctx.IsSigned - ctx.Data["SignedUser"] = ctx.User - ctx.Data["SignedUserID"] = ctx.User.ID - ctx.Data["SignedUserName"] = ctx.User.Name - ctx.Data["IsAdmin"] = ctx.User.IsAdmin - } else { - ctx.Data["SignedUserID"] = int64(0) - ctx.Data["SignedUserName"] = "" - - // ensure the session uid is deleted - _ = ctx.Session.Delete("uid") - } - ctx.Resp.Header().Set(`X-Frame-Options`, `SAMEORIGIN`) ctx.Data["CsrfToken"] = html.EscapeString(ctx.csrf.GetToken()) |