mirror of
1
Fork 0

Drop SSPI auth support and more Windows files (#7148)

## Dropping SSPI auth support

SSPI authentication relied on Microsoft Windows support, removal started in https://codeberg.org/forgejo/forgejo/pulls/5353, because it was broken anyway. We have no knowledge of any users using SSPI authentication. However, if you somehow managed to run Forgejo on Windows, or want to upgrade from a Gitea version which does, please ensure that you do not use SSPI as an authentication mechanism for user accounts. Feel free to reach out if you need assistance.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7148
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Otto Richter <otto@codeberg.org>
Co-committed-by: Otto Richter <otto@codeberg.org>
This commit is contained in:
Otto Richter 2025-03-08 00:43:41 +00:00 committed by Otto
parent 3de904c963
commit 9dea54a9d6
43 changed files with 39 additions and 816 deletions

View File

@ -59,20 +59,8 @@ ifeq ($(HAS_GO), yes)
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS) CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
endif endif
ifeq ($(GOOS),windows)
IS_WINDOWS := yes
else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
ifeq ($(GOOS),)
IS_WINDOWS := yes
endif
endif
ifeq ($(IS_WINDOWS),yes)
GOFLAGS := -v -buildmode=exe
EXECUTABLE ?= gitea.exe
else
GOFLAGS := -v GOFLAGS := -v
EXECUTABLE ?= gitea EXECUTABLE ?= gitea
endif
ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu) ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
SED_INPLACE := sed -i SED_INPLACE := sed -i
@ -498,13 +486,6 @@ lint-go-fix:
$(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS) --fix $(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS) --fix
$(RUN_DEADCODE) > .deadcode-out $(RUN_DEADCODE) > .deadcode-out
# workaround step for the lint-go-windows CI task because 'go run' can not
# have distinct GOOS/GOARCH for its build and run steps
.PHONY: lint-go-windows
lint-go-windows:
@GOOS= GOARCH= $(GO) install $(GOLANGCI_LINT_PACKAGE)
golangci-lint run
.PHONY: lint-go-vet .PHONY: lint-go-vet
lint-go-vet: lint-go-vet:
@echo "Running go vet..." @echo "Running go vet..."
@ -877,10 +858,6 @@ sources-tarbal: frontend generate vendor release-sources release-check
$(DIST_DIRS): $(DIST_DIRS):
mkdir -p $(DIST_DIRS) mkdir -p $(DIST_DIRS)
.PHONY: release-windows
release-windows: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
.PHONY: release-linux .PHONY: release-linux
release-linux: | $(DIST_DIRS) release-linux: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out forgejo-$(VERSION) . CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out forgejo-$(VERSION) .

View File

@ -87,8 +87,6 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
} }
defer f.Close() defer f.Close()
// Note: chmod command does not support in Windows.
if !setting.IsWindows {
fi, err := f.Stat() fi, err := f.Stat()
if err != nil { if err != nil {
return err return err
@ -101,7 +99,6 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
return err return err
} }
} }
}
for _, key := range keys { for _, key := range keys {
if key.Type == KeyTypePrincipal { if key.Type == KeyTypePrincipal {

View File

@ -32,7 +32,7 @@ const (
PAM // 4 PAM // 4
DLDAP // 5 DLDAP // 5
OAuth2 // 6 OAuth2 // 6
SSPI // 7 _ // 7 (was SSPI)
Remote // 8 Remote // 8
) )
@ -53,7 +53,6 @@ var Names = map[Type]string{
SMTP: "SMTP", SMTP: "SMTP",
PAM: "PAM", PAM: "PAM",
OAuth2: "OAuth2", OAuth2: "OAuth2",
SSPI: "SPNEGO with SSPI",
Remote: "Remote", Remote: "Remote",
} }
@ -178,11 +177,6 @@ func (source *Source) IsOAuth2() bool {
return source.Type == OAuth2 return source.Type == OAuth2
} }
// IsSSPI returns true of this source is of the SSPI type.
func (source *Source) IsSSPI() bool {
return source.Type == SSPI
}
func (source *Source) IsRemote() bool { func (source *Source) IsRemote() bool {
return source.Type == Remote return source.Type == Remote
} }
@ -265,20 +259,6 @@ func (opts FindSourcesOptions) ToConds() builder.Cond {
return conds return conds
} }
// IsSSPIEnabled returns true if there is at least one activated login
// source of type LoginSSPI
func IsSSPIEnabled(ctx context.Context) bool {
exist, err := db.Exist[Source](ctx, FindSourcesOptions{
IsActive: optional.Some(true),
LoginType: SSPI,
}.ToConds())
if err != nil {
log.Error("IsSSPIEnabled: failed to query active SSPI sources: %v", err)
return false
}
return exist
}
// GetSourceByID returns login source by given ID. // GetSourceByID returns login source by given ID.
func GetSourceByID(ctx context.Context, id int64) (*Source, error) { func GetSourceByID(ctx context.Context, id int64) (*Source, error) {
source := new(Source) source := new(Source)

View File

@ -11,7 +11,6 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -123,9 +122,6 @@ func MainTest(m *testing.M) {
os.Exit(1) os.Exit(1)
} }
giteaBinary := "gitea" giteaBinary := "gitea"
if runtime.GOOS == "windows" {
giteaBinary += ".exe"
}
setting.AppPath = path.Join(giteaRoot, giteaBinary) setting.AppPath = path.Join(giteaRoot, giteaBinary)
if _, err := os.Stat(setting.AppPath); err != nil { if _, err := os.Stat(setting.AppPath); err != nil {
fmt.Printf("Could not find gitea binary at %s\n", setting.AppPath) fmt.Printf("Could not find gitea binary at %s\n", setting.AppPath)

View File

@ -139,7 +139,7 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath
cmd := NewCommandContextNoGlobals(ctx, "blame", "--porcelain") cmd := NewCommandContextNoGlobals(ctx, "blame", "--porcelain")
if ignoreRevsFile != nil { if ignoreRevsFile != nil {
// Possible improvement: use --ignore-revs-file /dev/stdin on unix // Possible improvement: use --ignore-revs-file /dev/stdin on unix
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend. // This was not done in Gitea because it would not have been compatible with Windows.
cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile) cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile)
} }
cmd.AddDynamicArguments(commit.ID.String()). cmd.AddDynamicArguments(commit.ID.String()).

View File

@ -12,7 +12,6 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"runtime"
"runtime/trace" "runtime/trace"
"strings" "strings"
"time" "time"
@ -359,17 +358,6 @@ func (c *Command) Run(opts *RunOpts) error {
log.Debug("slow git.Command.Run: %s (%s)", c, elapsed) log.Debug("slow git.Command.Run: %s (%s)", c, elapsed)
} }
// We need to check if the context is canceled by the program on Windows.
// This is because Windows does not have signal checking when terminating the process.
// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
if runtime.GOOS == "windows" &&
err != nil &&
err.Error() == "" &&
cmd.ProcessState.ExitCode() == 1 &&
ctx.Err() == context.Canceled {
return ctx.Err()
}
if err != nil && ctx.Err() != context.DeadlineExceeded { if err != nil && ctx.Err() != context.DeadlineExceeded {
return err return err
} }

View File

@ -59,15 +59,7 @@ func loadGitVersion() error {
return fmt.Errorf("invalid git version output: %s", stdout) return fmt.Errorf("invalid git version output: %s", stdout)
} }
var versionString string versionString := fields[2]
// Handle special case on Windows.
i := strings.Index(fields[2], "windows")
if i >= 1 {
versionString = fields[2][:i-1]
} else {
versionString = fields[2]
}
var err error var err error
gitVersion, err = version.NewVersion(versionString) gitVersion, err = version.NewVersion(versionString)
@ -280,24 +272,11 @@ func syncGitConfig() (err error) {
// Thus the owner uid/gid for files on these filesystems will be marked as root. // Thus the owner uid/gid for files on these filesystems will be marked as root.
// As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea, // As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea,
// it is now safe to set "safe.directory=*" for internal usage only. // it is now safe to set "safe.directory=*" for internal usage only.
// Please note: the wildcard "*" is only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later // Please note: the wildcard "*" is only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later,
// Although only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later - this setting is tolerated by earlier versions // but is tolerated by earlier versions
if err := configAddNonExist("safe.directory", "*"); err != nil { if err := configAddNonExist("safe.directory", "*"); err != nil {
return err return err
} }
if runtime.GOOS == "windows" {
if err := configSet("core.longpaths", "true"); err != nil {
return err
}
if setting.Git.DisableCoreProtectNTFS {
err = configSet("core.protectNTFS", "false")
} else {
err = configUnsetAll("core.protectNTFS", "false")
}
if err != nil {
return err
}
}
// By default partial clones are disabled, enable them from git v2.22 // By default partial clones are disabled, enable them from git v2.22
if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil { if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil {

View File

@ -1,8 +1,6 @@
// Copyright 2019 The Gitea Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build !windows
package graceful package graceful
import ( import (

View File

@ -3,8 +3,6 @@
// This code is heavily inspired by the archived gofacebook/gracenet/net.go handler // This code is heavily inspired by the archived gofacebook/gracenet/net.go handler
//go:build !windows
package graceful package graceful
import ( import (

View File

@ -3,8 +3,6 @@
// This code is heavily inspired by the archived gofacebook/gracenet/net.go handler // This code is heavily inspired by the archived gofacebook/gracenet/net.go handler
//go:build !windows
package graceful package graceful
import ( import (

View File

@ -1,8 +1,6 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build !windows
package log package log
import ( import (

View File

@ -1,42 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"os"
"github.com/mattn/go-isatty"
"golang.org/x/sys/windows"
)
func enableVTMode(console windows.Handle) bool {
mode := uint32(0)
err := windows.GetConsoleMode(console, &mode)
if err != nil {
return false
}
// EnableVirtualTerminalProcessing is the console mode to allow ANSI code
// interpretation on the console. See:
// https://docs.microsoft.com/en-us/windows/console/setconsolemode
// It only works on Windows 10. Earlier terminals will fail with an err which we will
// handle to say don't color
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
err = windows.SetConsoleMode(console, mode)
return err == nil
}
func init() {
if isatty.IsTerminal(os.Stdout.Fd()) {
CanColorStdout = enableVTMode(windows.Stdout)
} else {
CanColorStdout = isatty.IsCygwinTerminal(os.Stderr.Fd())
}
if isatty.IsTerminal(os.Stderr.Fd()) {
CanColorStderr = enableVTMode(windows.Stderr)
} else {
CanColorStderr = isatty.IsCygwinTerminal(os.Stderr.Fd())
}
}

View File

@ -9,7 +9,6 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
@ -70,9 +69,6 @@ func (p *Renderer) DisplayInIFrame() bool {
} }
func envMark(envName string) string { func envMark(envName string) string {
if runtime.GOOS == "windows" {
return "%" + envName + "%"
}
return "$" + envName return "$" + envName
} }

View File

@ -1,8 +1,6 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build !windows
package process package process
import ( import (

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -146,10 +145,6 @@ func CreateDelegateHooks(repoPath string) (err error) {
} }
func checkExecutable(filename string) bool { func checkExecutable(filename string) bool {
// windows has no concept of a executable bit
if runtime.GOOS == "windows" {
return true
}
fileInfo, err := os.Stat(filename) fileInfo, err := os.Stat(filename)
if err != nil { if err != nil {
return false return false

View File

@ -34,11 +34,7 @@ var (
func getAppPath() (string, error) { func getAppPath() (string, error) {
var appPath string var appPath string
var err error var err error
if IsWindows && filepath.IsAbs(os.Args[0]) {
appPath = filepath.Clean(os.Args[0])
} else {
appPath, err = exec.LookPath(os.Args[0]) appPath, err = exec.LookPath(os.Args[0])
}
if err != nil { if err != nil {
if !errors.Is(err, exec.ErrDot) { if !errors.Is(err, exec.ErrDot) {
return "", err return "", err

View File

@ -8,7 +8,6 @@ package setting
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
"strings" "strings"
"time" "time"
@ -34,7 +33,6 @@ var (
RunMode string RunMode string
RunUser string RunUser string
IsProd bool IsProd bool
IsWindows bool
// IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing // IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing
// TODO: this is only a temporary solution, we should make the test code more reliable // TODO: this is only a temporary solution, we should make the test code more reliable
@ -42,22 +40,18 @@ var (
) )
func init() { func init() {
IsWindows = runtime.GOOS == "windows"
if AppVer == "" { if AppVer == "" {
AppVer = "dev" AppVer = "dev"
} }
// We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically
// By default set this logger at Info - we'll change it later, but we need to start with something. // By default set this logger at Info - we'll change it later, but we need to start with something.
log.SetConsoleLogger(log.DEFAULT, "console", log.INFO) log.SetConsoleLogger(log.DEFAULT, "console", log.INFO)
} }
// IsRunUserMatchCurrentUser returns false if configured run user does not match // IsRunUserMatchCurrentUser returns false if configured run user does not match
// actual user that runs the app. The first return value is the actual user name. // actual user that runs the app. The first return value is the actual user name.
// This check is ignored under Windows since SSH remote login is not the main
// method to login on Windows.
func IsRunUserMatchCurrentUser(runUser string) (string, bool) { func IsRunUserMatchCurrentUser(runUser string) (string, bool) {
if IsWindows || SSH.StartBuiltinServer { if SSH.StartBuiltinServer {
return "", true return "", true
} }

View File

@ -6,8 +6,6 @@ package user
import ( import (
"os" "os"
"os/user" "os/user"
"runtime"
"strings"
) )
// CurrentUsername return current login OS user name // CurrentUsername return current login OS user name
@ -16,12 +14,7 @@ func CurrentUsername() string {
if err != nil { if err != nil {
return fallbackCurrentUsername() return fallbackCurrentUsername()
} }
username := userinfo.Username return userinfo.Username
if runtime.GOOS == "windows" {
parts := strings.Split(username, "\\")
username = parts[len(parts)-1]
}
return username
} }
// Old method, used if new method doesn't work on your OS for some reason // Old method, used if new method doesn't work on your OS for some reason

View File

@ -5,7 +5,6 @@ package user
import ( import (
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"testing" "testing"
) )
@ -23,10 +22,6 @@ func TestCurrentUsername(t *testing.T) {
if len(user) == 0 { if len(user) == 0 {
t.Errorf("expected non-empty user, got: %s", user) t.Errorf("expected non-empty user, got: %s", user)
} }
// Windows whoami is weird, so just skip remaining tests
if runtime.GOOS == "windows" {
t.Skip("skipped test because of weird whoami on Windows")
}
whoami, err := getWhoamiOutput() whoami, err := getWhoamiOutput()
if err != nil { if err != nil {
t.Errorf("failed to run whoami to test current user: %f", err) t.Errorf("failed to run whoami to test current user: %f", err)

View File

@ -1,8 +1,6 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build !windows
package util package util
import ( import (

View File

@ -1,8 +1,6 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build !windows
package util package util
import ( import (

View File

@ -10,8 +10,6 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"runtime"
"strings" "strings"
) )
@ -78,11 +76,7 @@ func FilePathJoinAbs(base string, sub ...string) string {
// POSIX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators // POSIX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
// to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/` // to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
if isOSWindows() {
elems[0] = filepath.Clean(base)
} else {
elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator)) elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator))
}
if !filepath.IsAbs(elems[0]) { if !filepath.IsAbs(elems[0]) {
// This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead // This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", elems[0], elems)) panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", elems[0], elems))
@ -91,12 +85,8 @@ func FilePathJoinAbs(base string, sub ...string) string {
if s == "" { if s == "" {
continue continue
} }
if isOSWindows() {
elems = append(elems, filepath.Clean(pathSeparator+s))
} else {
elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator))) elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator)))
} }
}
// the elems[0] must be an absolute path, just join them together // the elems[0] must be an absolute path, just join them together
return filepath.Join(elems...) return filepath.Join(elems...)
} }
@ -217,12 +207,6 @@ func StatDir(rootPath string, includeDir ...bool) ([]string, error) {
return statDir(rootPath, "", isIncludeDir, false, false) return statDir(rootPath, "", isIncludeDir, false, false)
} }
func isOSWindows() bool {
return runtime.GOOS == "windows"
}
var driveLetterRegexp = regexp.MustCompile("/[A-Za-z]:/")
// FileURLToPath extracts the path information from a file://... url. // FileURLToPath extracts the path information from a file://... url.
// It returns an error only if the URL is not a file URL. // It returns an error only if the URL is not a file URL.
func FileURLToPath(u *url.URL) (string, error) { func FileURLToPath(u *url.URL) (string, error) {
@ -230,17 +214,7 @@ func FileURLToPath(u *url.URL) (string, error) {
return "", errors.New("URL scheme is not 'file': " + u.String()) return "", errors.New("URL scheme is not 'file': " + u.String())
} }
path := u.Path return u.Path, nil
if !isOSWindows() {
return path, nil
}
// If it looks like there's a Windows drive letter at the beginning, strip off the leading slash.
if driveLetterRegexp.MatchString(path) {
return path[1:], nil
}
return path, nil
} }
// HomeDir returns path of '~'(in Linux) on Windows, // HomeDir returns path of '~'(in Linux) on Windows,
@ -249,14 +223,7 @@ func HomeDir() (home string, err error) {
// TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually) // TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually)
// TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory // TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory
// so at the moment we can not use `user.Current().HomeDir` // so at the moment we can not use `user.Current().HomeDir`
if isOSWindows() {
home = os.Getenv("USERPROFILE")
if home == "" {
home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
}
} else {
home = os.Getenv("HOME") home = os.Getenv("HOME")
}
if home == "" { if home == "" {
return "", errors.New("cannot get home directory") return "", errors.New("cannot get home directory")

View File

@ -5,7 +5,6 @@ package util
import ( import (
"net/url" "net/url"
"runtime"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -17,7 +16,6 @@ func TestFileURLToPath(t *testing.T) {
url string url string
expected string expected string
haserror bool haserror bool
windows bool
}{ }{
// case 0 // case 0
{ {
@ -34,18 +32,9 @@ func TestFileURLToPath(t *testing.T) {
url: "file:///path", url: "file:///path",
expected: "/path", expected: "/path",
}, },
// case 3
{
url: "file:///C:/path",
expected: "C:/path",
windows: true,
},
} }
for n, c := range cases { for n, c := range cases {
if c.windows && runtime.GOOS != "windows" {
continue
}
u, _ := url.Parse(c.url) u, _ := url.Parse(c.url)
p, err := FileURLToPath(u) p, err := FileURLToPath(u)
if c.haserror { if c.haserror {
@ -177,22 +166,6 @@ func TestCleanPath(t *testing.T) {
assert.Equal(t, c.expected, PathJoinRelX(c.elems...), "case: %v", c.elems) assert.Equal(t, c.expected, PathJoinRelX(c.elems...), "case: %v", c.elems)
} }
// for POSIX only, but the result is similar on Windows, because the first element must be an absolute path
if isOSWindows() {
cases = []struct {
elems []string
expected string
}{
{[]string{`C:\..`}, `C:\`},
{[]string{`C:\a`}, `C:\a`},
{[]string{`C:\a/`}, `C:\a`},
{[]string{`C:\..\a\`, `../b`, `c\..`, `d`}, `C:\a\b\d`},
{[]string{`C:\a/..\b`}, `C:\b`},
{[]string{`C:\a`, ``, `b`}, `C:\a\b`},
{[]string{`C:\a`, `..`, `b`}, `C:\a\b`},
{[]string{`C:\lfs`, `repo/..`, `user/../path`}, `C:\lfs\path`},
}
} else {
cases = []struct { cases = []struct {
elems []string elems []string
expected string expected string
@ -206,7 +179,6 @@ func TestCleanPath(t *testing.T) {
{[]string{`/a`, `..`, `b`}, `/a/b`}, {[]string{`/a`, `..`, `b`}, `/a/b`},
{[]string{`/lfs`, `repo/..`, `user/../path`}, `/lfs/path`}, {[]string{`/lfs`, `repo/..`, `user/../path`}, `/lfs/path`},
} }
}
for _, c := range cases { for _, c := range cases {
assert.Equal(t, c.expected, FilePathJoinAbs(c.elems[0], c.elems[1:]...), "case: %v", c.elems) assert.Equal(t, c.expected, FilePathJoinAbs(c.elems[0], c.elems[1:]...), "case: %v", c.elems)
} }

View File

@ -5,13 +5,10 @@ package util
import ( import (
"os" "os"
"runtime"
"syscall" "syscall"
"time" "time"
) )
const windowsSharingViolationError syscall.Errno = 32
// Remove removes the named file or (empty) directory with at most 5 attempts. // Remove removes the named file or (empty) directory with at most 5 attempts.
func Remove(name string) error { func Remove(name string) error {
var err error var err error
@ -27,12 +24,6 @@ func Remove(name string) error {
continue continue
} }
if unwrapped == windowsSharingViolationError && runtime.GOOS == "windows" {
// try again
<-time.After(100 * time.Millisecond)
continue
}
if unwrapped == syscall.ENOENT { if unwrapped == syscall.ENOENT {
// it's already gone // it's already gone
return nil return nil
@ -56,12 +47,6 @@ func RemoveAll(name string) error {
continue continue
} }
if unwrapped == windowsSharingViolationError && runtime.GOOS == "windows" {
// try again
<-time.After(100 * time.Millisecond)
continue
}
if unwrapped == syscall.ENOENT { if unwrapped == syscall.ENOENT {
// it's already gone // it's already gone
return nil return nil
@ -85,12 +70,6 @@ func Rename(oldpath, newpath string) error {
continue continue
} }
if unwrapped == windowsSharingViolationError && runtime.GOOS == "windows" {
// try again
<-time.After(100 * time.Millisecond)
continue
}
if i == 0 && os.IsNotExist(err) { if i == 0 && os.IsNotExist(err) {
return err return err
} }

View File

@ -609,9 +609,6 @@ CommitChoice = Commit choice
TreeName = File path TreeName = File path
Content = Content Content = Content
SSPISeparatorReplacement = Separator
SSPIDefaultLanguage = Default language
require_error = ` cannot be empty.` require_error = ` cannot be empty.`
alpha_dash_error = ` should contain only alphanumeric, dash ("-") and underscore ("_") characters.` alpha_dash_error = ` should contain only alphanumeric, dash ("-") and underscore ("_") characters.`
alpha_dash_dot_error = ` should contain only alphanumeric, dash ("-"), underscore ("_") and dot (".") characters.` alpha_dash_dot_error = ` should contain only alphanumeric, dash ("-"), underscore ("_") and dot (".") characters.`
@ -3300,16 +3297,6 @@ auths.oauth2_admin_group = Group claim value for administrator users. (Optional
auths.oauth2_restricted_group = Group claim value for restricted users. (Optional - requires claim name above) auths.oauth2_restricted_group = Group claim value for restricted users. (Optional - requires claim name above)
auths.oauth2_map_group_to_team = Map claimed groups to organization teams. (Optional - requires claim name above) auths.oauth2_map_group_to_team = Map claimed groups to organization teams. (Optional - requires claim name above)
auths.oauth2_map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding group. auths.oauth2_map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding group.
auths.sspi_auto_create_users = Automatically create users
auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time
auths.sspi_auto_activate_users = Automatically activate users
auths.sspi_auto_activate_users_helper = Allow SSPI auth method to automatically activate new users
auths.sspi_strip_domain_names = Remove domain names from usernames
auths.sspi_strip_domain_names_helper = If checked, domain names will be removed from logon names (eg. "DOMAIN\user" and "user@example.org" both will become just "user").
auths.sspi_separator_replacement = Separator to use instead of \, / and @
auths.sspi_separator_replacement_helper = The character to use to replace the separators of down-level logon names (eg. the \ in "DOMAIN\user") and user principal names (eg. the @ in "user@example.org").
auths.sspi_default_language = Default user language
auths.sspi_default_language_helper = Default language for users automatically created by SSPI auth method. Leave empty if you prefer language to be automatically detected.
auths.tips = Tips auths.tips = Tips
auths.tips.gmail_settings = Gmail settings: auths.tips.gmail_settings = Gmail settings:
auths.tips.oauth2.general = OAuth2 authentication auths.tips.oauth2.general = OAuth2 authentication

View File

@ -6,8 +6,6 @@ package shared
import ( import (
"net/http" "net/http"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
@ -51,10 +49,6 @@ func buildAuthGroup() *auth.Group {
group.Add(&auth.ReverseProxy{}) group.Add(&auth.ReverseProxy{})
} }
if setting.IsWindows && auth_model.IsSSPIEnabled(db.DefaultContext) {
group.Add(&auth.SSPI{}) // it MUST be the last, see the comment of SSPI
}
return group return group
} }

View File

@ -29,7 +29,6 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/user"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
@ -119,15 +118,7 @@ func Install(ctx *context.Context) {
form.AppSlogan = "Beyond coding. We Forge." form.AppSlogan = "Beyond coding. We Forge."
form.RepoRootPath = setting.RepoRootPath form.RepoRootPath = setting.RepoRootPath
form.LFSRootPath = setting.LFS.Storage.Path form.LFSRootPath = setting.LFS.Storage.Path
// Note(unknown): it's hard for Windows users change a running user,
// so just use current one if config says default.
if setting.IsWindows && setting.RunUser == "git" {
form.RunUser = user.CurrentUsername()
} else {
form.RunUser = setting.RunUser form.RunUser = setting.RunUser
}
form.Domain = setting.Domain form.Domain = setting.Domain
form.SSHPort = setting.SSH.Port form.SSHPort = setting.SSH.Port
form.HTTPPort = setting.HTTPPort form.HTTPPort = setting.HTTPPort

View File

@ -1,8 +1,6 @@
// Copyright 2020 The Gitea Authors. All rights reserved. // Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//go:build !windows
package private package private
import ( import (

View File

@ -4,11 +4,9 @@
package admin package admin
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"strconv" "strconv"
"strings" "strings"
@ -18,14 +16,12 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
auth_service "code.gitea.io/gitea/services/auth" auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/ldap" "code.gitea.io/gitea/services/auth/source/ldap"
"code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/oauth2"
pam_service "code.gitea.io/gitea/services/auth/source/pam" pam_service "code.gitea.io/gitea/services/auth/source/pam"
"code.gitea.io/gitea/services/auth/source/smtp" "code.gitea.io/gitea/services/auth/source/smtp"
"code.gitea.io/gitea/services/auth/source/sspi"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
@ -38,11 +34,6 @@ const (
tplAuthEdit base.TplName = "admin/auth/edit" tplAuthEdit base.TplName = "admin/auth/edit"
) )
var (
separatorAntiPattern = regexp.MustCompile(`[^\w-\.]`)
langCodePattern = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`)
)
// Authentications show authentication config page // Authentications show authentication config page
func Authentications(ctx *context.Context) { func Authentications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.authentication") ctx.Data["Title"] = ctx.Tr("admin.authentication")
@ -70,7 +61,6 @@ var (
{auth.DLDAP.String(), auth.DLDAP}, {auth.DLDAP.String(), auth.DLDAP},
{auth.SMTP.String(), auth.SMTP}, {auth.SMTP.String(), auth.SMTP},
{auth.OAuth2.String(), auth.OAuth2}, {auth.OAuth2.String(), auth.OAuth2},
{auth.SSPI.String(), auth.SSPI},
} }
if pam.Supported { if pam.Supported {
items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM}) items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM})
@ -102,12 +92,6 @@ func NewAuthSource(ctx *context.Context) {
oauth2providers := oauth2.GetSupportedOAuth2Providers() oauth2providers := oauth2.GetSupportedOAuth2Providers()
ctx.Data["OAuth2Providers"] = oauth2providers ctx.Data["OAuth2Providers"] = oauth2providers
ctx.Data["SSPIAutoCreateUsers"] = true
ctx.Data["SSPIAutoActivateUsers"] = true
ctx.Data["SSPIStripDomainNames"] = true
ctx.Data["SSPISeparatorReplacement"] = "_"
ctx.Data["SSPIDefaultLanguage"] = ""
// only the first as default // only the first as default
ctx.Data["oauth2_provider"] = oauth2providers[0].Name() ctx.Data["oauth2_provider"] = oauth2providers[0].Name()
@ -209,30 +193,6 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
} }
} }
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
if util.IsEmptyString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error"))
}
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error"))
}
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
ctx.Data["Err_SSPIDefaultLanguage"] = true
return nil, errors.New(ctx.Locale.TrString("form.lang_select_error"))
}
return &sspi.Source{
AutoCreateUsers: form.SSPIAutoCreateUsers,
AutoActivateUsers: form.SSPIAutoActivateUsers,
StripDomainNames: form.SSPIStripDomainNames,
SeparatorReplacement: form.SSPISeparatorReplacement,
DefaultLanguage: form.SSPIDefaultLanguage,
}, nil
}
// NewAuthSourcePost response for adding an auth source // NewAuthSourcePost response for adding an auth source
func NewAuthSourcePost(ctx *context.Context) { func NewAuthSourcePost(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AuthenticationForm) form := *web.GetForm(ctx).(*forms.AuthenticationForm)
@ -247,12 +207,6 @@ func NewAuthSourcePost(ctx *context.Context) {
oauth2providers := oauth2.GetSupportedOAuth2Providers() oauth2providers := oauth2.GetSupportedOAuth2Providers()
ctx.Data["OAuth2Providers"] = oauth2providers ctx.Data["OAuth2Providers"] = oauth2providers
ctx.Data["SSPIAutoCreateUsers"] = true
ctx.Data["SSPIAutoActivateUsers"] = true
ctx.Data["SSPIStripDomainNames"] = true
ctx.Data["SSPISeparatorReplacement"] = "_"
ctx.Data["SSPIDefaultLanguage"] = ""
hasTLS := false hasTLS := false
var config convert.Conversion var config convert.Conversion
switch auth.Type(form.Type) { switch auth.Type(form.Type) {
@ -279,19 +233,6 @@ func NewAuthSourcePost(ctx *context.Context) {
return return
} }
} }
case auth.SSPI:
var err error
config, err = parseSSPIConfig(ctx, form)
if err != nil {
ctx.RenderWithErr(err.Error(), tplAuthNew, form)
return
}
existing, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{LoginType: auth.SSPI})
if err != nil || len(existing) > 0 {
ctx.Data["Err_Type"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
return
}
default: default:
ctx.Error(http.StatusBadRequest) ctx.Error(http.StatusBadRequest)
return return
@ -408,12 +349,6 @@ func EditAuthSourcePost(ctx *context.Context) {
return return
} }
} }
case auth.SSPI:
config, err = parseSSPIConfig(ctx, form)
if err != nil {
ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
return
}
default: default:
ctx.Error(http.StatusBadRequest) ctx.Error(http.StatusBadRequest)
return return

View File

@ -164,7 +164,6 @@ func SignIn(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login" ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLogin"] = true ctx.Data["PageIsLogin"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
ctx.Data["EnableInternalSignIn"] = setting.Service.EnableInternalSignIn ctx.Data["EnableInternalSignIn"] = setting.Service.EnableInternalSignIn
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
@ -190,7 +189,6 @@ func SignInPost(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login" ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLogin"] = true ctx.Data["PageIsLogin"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
ctx.Data["EnableInternalSignIn"] = setting.Service.EnableInternalSignIn ctx.Data["EnableInternalSignIn"] = setting.Service.EnableInternalSignIn
ctx.Data["DisablePassword"] = !setting.Service.EnableInternalSignIn ctx.Data["DisablePassword"] = !setting.Service.EnableInternalSignIn

View File

@ -8,8 +8,6 @@ import (
"net/http" "net/http"
"strings" "strings"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
quota_model "code.gitea.io/gitea/models/quota" quota_model "code.gitea.io/gitea/models/quota"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
@ -110,10 +108,6 @@ func buildAuthGroup() *auth_service.Group {
} }
group.Add(&auth_service.Session{}) group.Add(&auth_service.Session{})
if setting.IsWindows && auth_model.IsSSPIEnabled(db.DefaultContext) {
group.Add(&auth_service.SSPI{}) // it MUST be the last, see the comment of SSPI
}
return group return group
} }

View File

@ -18,7 +18,6 @@ import (
_ "code.gitea.io/gitea/services/auth/source/db" // register the sources (and below) _ "code.gitea.io/gitea/services/auth/source/db" // register the sources (and below)
_ "code.gitea.io/gitea/services/auth/source/ldap" // register the ldap source _ "code.gitea.io/gitea/services/auth/source/ldap" // register the ldap source
_ "code.gitea.io/gitea/services/auth/source/pam" // register the pam source _ "code.gitea.io/gitea/services/auth/source/pam" // register the pam source
_ "code.gitea.io/gitea/services/auth/source/sspi" // register the sspi source
) )
// UserSignIn validates user name and password. // UserSignIn validates user name and password.

View File

@ -1,18 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package sspi_test
import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/services/auth/source/sspi"
)
// This test file exists to assert that our Source exposes the interfaces that we expect
// It tightly binds the interfaces and implementation without breaking go import cycles
type sourceInterface interface {
auth.Config
}
var _ (sourceInterface) = &sspi.Source{}

View File

@ -1,39 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package sspi
import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/json"
)
// _________ ___________________.___
// / _____// _____/\______ \ |
// \_____ \ \_____ \ | ___/ |
// / \/ \ | | | |
// /_______ /_______ / |____| |___|
// \/ \/
// Source holds configuration for SSPI single sign-on.
type Source struct {
AutoCreateUsers bool
AutoActivateUsers bool
StripDomainNames bool
SeparatorReplacement string
DefaultLanguage string
}
// FromDB fills up an SSPIConfig from serialized format.
func (cfg *Source) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
// ToDB exports an SSPIConfig to a serialized format.
func (cfg *Source) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
func init() {
auth.RegisterTypeConfig(auth.SSPI, &Source{})
}

View File

@ -1,223 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"context"
"errors"
"net/http"
"strings"
"sync"
"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/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/auth/source/sspi"
gitea_context "code.gitea.io/gitea/services/context"
gouuid "github.com/google/uuid"
)
const (
tplSignIn base.TplName = "user/auth/signin"
)
type SSPIAuth interface {
AppendAuthenticateHeader(w http.ResponseWriter, data string)
Authenticate(r *http.Request, w http.ResponseWriter) (userInfo *SSPIUserInfo, outToken string, err error)
}
var (
sspiAuth SSPIAuth // a global instance of the websspi authenticator to avoid acquiring the server credential handle on every request
sspiAuthOnce sync.Once
sspiAuthErrInit error
// Ensure the struct implements the interface.
_ Method = &SSPI{}
)
// SSPI implements the SingleSignOn interface and authenticates requests
// via the built-in SSPI module in Windows for SPNEGO authentication.
// 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.
type SSPI struct{}
// Name represents the name of auth method
func (s *SSPI) Name() string {
return "sspi"
}
// Verify uses SSPI (Windows implementation of SPNEGO) to authenticate the request.
// If authentication is successful, returns 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) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
sspiAuthOnce.Do(func() { sspiAuthErrInit = sspiAuthInit() })
if sspiAuthErrInit != nil {
return nil, sspiAuthErrInit
}
if !s.shouldAuthenticate(req) {
return nil, nil
}
cfg, err := s.getConfig(req.Context())
if err != nil {
log.Error("could not get SSPI config: %v", err)
return nil, err
}
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
// in this case, the Verify function is called in Gitea's web context
// FIXME: it doesn't look good to render the page here, why not redirect?
gitea_context.GetWebContext(req).HTML(http.StatusUnauthorized, tplSignIn)
return nil, err
}
if outToken != "" {
sspiAuth.AppendAuthenticateHeader(w, outToken)
}
username := sanitizeUsername(userInfo.Username, cfg)
if len(username) == 0 {
return nil, nil
}
log.Info("Authenticated as %s\n", username)
user, err := user_model.GetUserByName(req.Context(), username)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByName: %v", err)
return nil, err
}
if !cfg.AutoCreateUsers {
log.Error("User '%s' not found", username)
return nil, nil
}
user, err = s.newUser(req.Context(), username, cfg)
if err != nil {
log.Error("CreateUser: %v", err)
return nil, err
}
}
// 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, nil
}
// getConfig retrieves the SSPI configuration from login sources
func (s *SSPI) getConfig(ctx context.Context) (*sspi.Source, error) {
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
IsActive: optional.Some(true),
LoginType: auth.SSPI,
})
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].Cfg.(*sspi.Source), 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.IsAPIPath(req) || isAttachmentDownload(req) {
shouldAuth = true
}
return shouldAuth
}
// 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(ctx context.Context, username string, cfg *sspi.Source) (*user_model.User, error) {
email := gouuid.New().String() + "@localhost.localdomain"
user := &user_model.User{
Name: username,
Email: email,
Language: cfg.DefaultLanguage,
}
emailNotificationPreference := user_model.EmailNotificationsDisabled
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsActive: optional.Some(cfg.AutoActivateUsers),
KeepEmailPrivate: optional.Some(true),
EmailNotificationsPreference: &emailNotificationPreference,
}
if err := user_model.CreateUser(ctx, user, overwriteDefault); 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 *sspi.Source) 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 *sspi.Source) 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
}

View File

@ -1,30 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !windows
package auth
import (
"errors"
"net/http"
)
type SSPIUserInfo struct {
Username string // Name of user, usually in the form DOMAIN\User
Groups []string // The global groups the user is a member of
}
type sspiAuthMock struct{}
func (s sspiAuthMock) AppendAuthenticateHeader(w http.ResponseWriter, data string) {
}
func (s sspiAuthMock) Authenticate(r *http.Request, w http.ResponseWriter) (userInfo *SSPIUserInfo, outToken string, err error) {
return nil, "", errors.New("not implemented")
}
func sspiAuthInit() error {
sspiAuth = &sspiAuthMock{} // TODO: we can mock the SSPI auth in tests
return nil
}

View File

@ -77,11 +77,6 @@ type AuthenticationForm struct {
Oauth2GroupTeamMapRemoval bool Oauth2GroupTeamMapRemoval bool
Oauth2AttributeSSHPublicKey string Oauth2AttributeSSHPublicKey string
SkipLocalTwoFA bool SkipLocalTwoFA bool
SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool
SSPIStripDomainNames bool
SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
SSPIDefaultLanguage string
GroupTeamMap string `binding:"ValidGroupTeamMap"` GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool GroupTeamMapRemoval bool
} }

View File

@ -380,51 +380,6 @@
</div> </div>
{{end}} {{end}}
<!-- SSPI -->
{{if .Source.IsSSPI}}
{{$cfg:=.Source.Cfg}}
<div class="field">
<div class="ui checkbox">
<label for="sspi_auto_create_users"><strong>{{ctx.Locale.Tr "admin.auths.sspi_auto_create_users"}}</strong></label>
<input id="sspi_auto_create_users" name="sspi_auto_create_users" class="sspi-auto-create-users" type="checkbox" {{if $cfg.AutoCreateUsers}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_auto_create_users_helper"}}</p>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<label for="sspi_auto_activate_users"><strong>{{ctx.Locale.Tr "admin.auths.sspi_auto_activate_users"}}</strong></label>
<input id="sspi_auto_activate_users" name="sspi_auto_activate_users" class="sspi-auto-activate-users" type="checkbox" {{if $cfg.AutoActivateUsers}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_auto_activate_users_helper"}}</p>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<label for="sspi_strip_domain_names"><strong>{{ctx.Locale.Tr "admin.auths.sspi_strip_domain_names"}}</strong></label>
<input id="sspi_strip_domain_names" name="sspi_strip_domain_names" class="sspi-strip-domain-names" type="checkbox" {{if $cfg.StripDomainNames}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_strip_domain_names_helper"}}</p>
</div>
</div>
<div class="required field">
<label for="sspi_separator_replacement">{{ctx.Locale.Tr "admin.auths.sspi_separator_replacement"}}</label>
<input id="sspi_separator_replacement" name="sspi_separator_replacement" value="{{$cfg.SeparatorReplacement}}" required>
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_separator_replacement_helper"}}</p>
</div>
<div class="field">
<label for="sspi_default_language">{{ctx.Locale.Tr "admin.auths.sspi_default_language"}}</label>
<div class="ui language selection dropdown" id="sspi_default_language">
<input name="sspi_default_language" type="hidden" value="{{$cfg.DefaultLanguage}}">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="text">{{range .AllLangs}}{{if eq $cfg.DefaultLanguage .Lang}}{{.Name}}{{end}}{{end}}</div>
<div class="menu">
<div class="item{{if not $.SSPIDefaultLanguage}} active selected{{end}}" data-value="">-</div>
{{range .AllLangs}}
<div class="item{{if eq $cfg.DefaultLanguage .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div>
{{end}}
</div>
</div>
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_default_language_helper"}}</p>
</div>
{{end}}
{{if .Source.IsLDAP}} {{if .Source.IsLDAP}}
<div class="inline field"> <div class="inline field">
<div class="ui checkbox"> <div class="ui checkbox">

View File

@ -50,9 +50,6 @@
<!-- OAuth2 --> <!-- OAuth2 -->
{{template "admin/auth/source/oauth" .}} {{template "admin/auth/source/oauth" .}}
<!-- SSPI -->
{{template "admin/auth/source/sspi" .}}
<div class="ldap field"> <div class="ldap field">
<div class="ui checkbox"> <div class="ui checkbox">
<label><strong>{{ctx.Locale.Tr "admin.auths.attributes_in_bind"}}</strong></label> <label><strong>{{ctx.Locale.Tr "admin.auths.attributes_in_bind"}}</strong></label>

View File

@ -1,43 +0,0 @@
<div class="sspi field {{if not (eq .type 7)}}tw-hidden{{end}}">
<div class="field">
<div class="ui checkbox">
<label for="sspi_auto_create_users"><strong>{{ctx.Locale.Tr "admin.auths.sspi_auto_create_users"}}</strong></label>
<input id="sspi_auto_create_users" name="sspi_auto_create_users" class="sspi-auto-create-users" type="checkbox" {{if .SSPIAutoCreateUsers}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_auto_create_users_helper"}}</p>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<label for="sspi_auto_activate_users"><strong>{{ctx.Locale.Tr "admin.auths.sspi_auto_activate_users"}}</strong></label>
<input id="sspi_auto_activate_users" name="sspi_auto_activate_users" class="sspi-auto-activate-users" type="checkbox" {{if .SSPIAutoActivateUsers}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_auto_activate_users_helper"}}</p>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<label for="sspi_strip_domain_names"><strong>{{ctx.Locale.Tr "admin.auths.sspi_strip_domain_names"}}</strong></label>
<input id="sspi_strip_domain_names" name="sspi_strip_domain_names" class="sspi-strip-domain-names" type="checkbox" {{if .SSPIStripDomainNames}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_strip_domain_names_helper"}}</p>
</div>
</div>
<div class="required field">
<label for="sspi_separator_replacement">{{ctx.Locale.Tr "admin.auths.sspi_separator_replacement"}}</label>
<input id="sspi_separator_replacement" name="sspi_separator_replacement" value="{{.SSPISeparatorReplacement}}">
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_separator_replacement_helper"}}</p>
</div>
<div class="field">
<label for="sspi_default_language">{{ctx.Locale.Tr "admin.auths.sspi_default_language"}}</label>
<div class="ui language selection dropdown" id="sspi_default_language">
<input name="sspi_default_language" type="hidden" value="{{.SSPIDefaultLanguage}}">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="text">{{range .AllLangs}}{{if eq $.SSPIDefaultLanguage .Lang}}{{.Name}}{{end}}{{end}}</div>
<div class="menu">
<div class="item{{if not $.SSPIDefaultLanguage}} active selected{{end}}" data-value="">-</div>
{{range .AllLangs}}
<div class="item{{if eq $.SSPIDefaultLanguage .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div>
{{end}}
</div>
</div>
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_default_language_helper"}}</p>
</div>
</div>

View File

@ -19,12 +19,6 @@
{{ctx.Locale.Tr "auth.sign_in_openid"}} {{ctx.Locale.Tr "auth.sign_in_openid"}}
</a> </a>
{{end}} {{end}}
{{if .EnableSSPI}}
<a class="ui button tw-flex tw-items-center tw-justify-center tw-py-2 tw-w-full" rel="nofollow" href="{{AppSubUrl}}/user/login?auth_with_sspi=1">
{{svg "fontawesome-windows"}}
&nbsp;SSPI
</a>
{{end}}
</div> </div>
</div> </div>
</div> </div>

View File

@ -66,9 +66,6 @@ func InitTest(requireGitea bool) {
setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom") setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
if requireGitea { if requireGitea {
giteaBinary := "gitea" giteaBinary := "gitea"
if setting.IsWindows {
giteaBinary += ".exe"
}
setting.AppPath = path.Join(giteaRoot, giteaBinary) setting.AppPath = path.Join(giteaRoot, giteaBinary)
if _, err := os.Stat(setting.AppPath); err != nil { if _, err := os.Stat(setting.AppPath); err != nil {
exitf("Could not find gitea binary at %s", setting.AppPath) exitf("Could not find gitea binary at %s", setting.AppPath)

View File

@ -123,9 +123,9 @@ export function initAdminCommon() {
// New authentication // New authentication
if (document.querySelector('.admin.new.authentication')) { if (document.querySelector('.admin.new.authentication')) {
document.getElementById('auth_type')?.addEventListener('change', function () { document.getElementById('auth_type')?.addEventListener('change', function () {
hideElem('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi'); hideElem('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size');
for (const input of document.querySelectorAll('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]')) { for (const input of document.querySelectorAll('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required]')) {
input.removeAttribute('required'); input.removeAttribute('required');
} }
@ -166,12 +166,6 @@ export function initAdminCommon() {
} }
onOAuth2Change(true); onOAuth2Change(true);
break; break;
case '7': // SSPI
showElem('.sspi');
for (const input of document.querySelectorAll('.sspi div.required input')) {
input.setAttribute('required', 'required');
}
break;
} }
if (authType === '2' || authType === '5') { if (authType === '2' || authType === '5') {
onSecurityProtocolChange(); onSecurityProtocolChange();