From de972f1bbb1c34501d9408bd31fbd38bca3e2eb3 Mon Sep 17 00:00:00 2001 From: QuaSoft Date: Tue, 22 Oct 2019 15:49:36 +0300 Subject: [PATCH 01/40] Add single sign-on support via SSPI on Windows --- .../doc/features/authentication.en-us.md | 37 + go.mod | 3 +- go.sum | 6 +- models/login_source.go | 46 +- modules/auth/auth.go | 211 +--- modules/auth/auth_form.go | 6 +- modules/auth/sso/basic.go | 126 ++ modules/auth/sso/interface.go | 33 + modules/auth/sso/oauth2.go | 146 +++ modules/auth/sso/reverseproxy.go | 116 ++ modules/auth/sso/session.go | 51 + modules/auth/sso/sso.go | 180 +++ modules/auth/sso/sspi_windows.go | 175 +++ modules/context/context.go | 3 + options/locale/locale_en-US.ini | 11 + public/js/gitgraph.js | 2 +- public/js/gitgraph.js.map | 2 +- public/js/index.js | 2 +- public/js/index.js.map | 2 +- routers/admin/auths.go | 24 + routers/init.go | 5 + routers/repo/http.go | 4 +- templates/admin/auth/edit.tmpl | 38 + templates/admin/auth/new.tmpl | 3 + templates/admin/auth/source/sspi.tmpl | 35 + templates/base/head_navbar.tmpl | 1 - templates/user/auth/signin_navbar.tmpl | 10 +- vendor/github.com/quasoft/websspi/.gitignore | 12 + vendor/github.com/quasoft/websspi/.travis.yml | 13 + vendor/github.com/quasoft/websspi/LICENSE | 21 + vendor/github.com/quasoft/websspi/README.md | 41 + vendor/github.com/quasoft/websspi/go.mod | 9 + vendor/github.com/quasoft/websspi/go.sum | 6 + .../quasoft/websspi/secctx/session.go | 36 + .../quasoft/websspi/secctx/store.go | 13 + vendor/github.com/quasoft/websspi/userinfo.go | 7 + vendor/github.com/quasoft/websspi/utf16.go | 22 + .../quasoft/websspi/websspi_windows.go | 615 ++++++++++ .../quasoft/websspi/win32_windows.go | 312 +++++ vendor/golang.org/x/sys/cpu/byteorder.go | 38 +- vendor/golang.org/x/sys/cpu/cpu.go | 36 + vendor/golang.org/x/sys/cpu/cpu_arm.go | 33 +- vendor/golang.org/x/sys/cpu/cpu_linux_arm.go | 39 + vendor/golang.org/x/sys/unix/mkall.sh | 4 +- vendor/golang.org/x/sys/unix/mkerrors.sh | 48 +- .../x/sys/unix/syscall_darwin.1_12.go | 29 + .../x/sys/unix/syscall_darwin.1_13.go | 103 ++ .../golang.org/x/sys/unix/syscall_darwin.go | 2 +- .../x/sys/unix/syscall_darwin_386.1_11.go | 9 + .../x/sys/unix/syscall_darwin_386.go | 1 - .../x/sys/unix/syscall_darwin_amd64.1_11.go | 9 + .../x/sys/unix/syscall_darwin_amd64.go | 1 - .../x/sys/unix/syscall_darwin_arm.1_11.go | 11 + .../x/sys/unix/syscall_darwin_arm.go | 4 - .../x/sys/unix/syscall_darwin_arm64.1_11.go | 11 + .../x/sys/unix/syscall_darwin_arm64.go | 4 - .../x/sys/unix/syscall_darwin_libSystem.go | 2 + .../x/sys/unix/syscall_dragonfly.go | 2 +- .../golang.org/x/sys/unix/syscall_freebsd.go | 2 +- vendor/golang.org/x/sys/unix/syscall_linux.go | 43 + .../golang.org/x/sys/unix/syscall_netbsd.go | 2 +- .../golang.org/x/sys/unix/syscall_openbsd.go | 2 +- .../golang.org/x/sys/unix/syscall_solaris.go | 2 +- .../x/sys/unix/zerrors_darwin_386.go | 3 +- .../x/sys/unix/zerrors_darwin_amd64.go | 3 +- .../x/sys/unix/zerrors_darwin_arm.go | 3 +- .../x/sys/unix/zerrors_darwin_arm64.go | 3 +- .../x/sys/unix/zerrors_dragonfly_amd64.go | 1 + .../x/sys/unix/zerrors_freebsd_386.go | 3 +- .../x/sys/unix/zerrors_freebsd_amd64.go | 3 +- .../x/sys/unix/zerrors_freebsd_arm.go | 3 +- .../x/sys/unix/zerrors_freebsd_arm64.go | 3 +- .../x/sys/unix/zerrors_linux_386.go | 46 +- .../x/sys/unix/zerrors_linux_amd64.go | 46 +- .../x/sys/unix/zerrors_linux_arm.go | 46 +- .../x/sys/unix/zerrors_linux_arm64.go | 48 +- .../x/sys/unix/zerrors_linux_mips.go | 46 +- .../x/sys/unix/zerrors_linux_mips64.go | 46 +- .../x/sys/unix/zerrors_linux_mips64le.go | 46 +- .../x/sys/unix/zerrors_linux_mipsle.go | 46 +- .../x/sys/unix/zerrors_linux_ppc64.go | 46 +- .../x/sys/unix/zerrors_linux_ppc64le.go | 46 +- .../x/sys/unix/zerrors_linux_riscv64.go | 46 +- .../x/sys/unix/zerrors_linux_s390x.go | 46 +- .../x/sys/unix/zerrors_linux_sparc64.go | 46 +- .../x/sys/unix/zerrors_netbsd_386.go | 3 +- .../x/sys/unix/zerrors_netbsd_amd64.go | 3 +- .../x/sys/unix/zerrors_netbsd_arm.go | 3 +- .../x/sys/unix/zerrors_netbsd_arm64.go | 3 +- .../x/sys/unix/zerrors_openbsd_386.go | 17 +- .../x/sys/unix/zerrors_openbsd_amd64.go | 6 +- .../x/sys/unix/zerrors_openbsd_arm.go | 11 +- .../x/sys/unix/zerrors_openbsd_arm64.go | 1 + .../x/sys/unix/zerrors_solaris_amd64.go | 3 +- .../x/sys/unix/zsyscall_darwin_386.1_11.go | 73 +- .../x/sys/unix/zsyscall_darwin_386.1_13.go | 41 + .../x/sys/unix/zsyscall_darwin_386.1_13.s | 12 + .../x/sys/unix/zsyscall_darwin_386.go | 27 +- .../x/sys/unix/zsyscall_darwin_386.s | 2 - .../x/sys/unix/zsyscall_darwin_amd64.1_11.go | 41 +- .../x/sys/unix/zsyscall_darwin_amd64.1_13.go | 41 + .../x/sys/unix/zsyscall_darwin_amd64.1_13.s | 12 + .../x/sys/unix/zsyscall_darwin_amd64.go | 27 +- .../x/sys/unix/zsyscall_darwin_amd64.s | 2 - .../x/sys/unix/zsyscall_darwin_arm.1_11.go | 7 +- .../x/sys/unix/zsyscall_darwin_arm.1_13.go | 41 + .../x/sys/unix/zsyscall_darwin_arm.1_13.s | 12 + .../x/sys/unix/zsyscall_darwin_arm.go | 5 +- .../x/sys/unix/zsyscall_darwin_arm.s | 4 +- .../x/sys/unix/zsyscall_darwin_arm64.1_11.go | 7 +- .../x/sys/unix/zsyscall_darwin_arm64.1_13.go | 41 + .../x/sys/unix/zsyscall_darwin_arm64.1_13.s | 12 + .../x/sys/unix/zsyscall_darwin_arm64.go | 5 +- .../x/sys/unix/zsyscall_dragonfly_amd64.go | 5 +- .../x/sys/unix/zsyscall_freebsd_386.go | 5 +- .../x/sys/unix/zsyscall_freebsd_amd64.go | 45 +- .../x/sys/unix/zsyscall_freebsd_arm.go | 45 +- .../x/sys/unix/zsyscall_freebsd_arm64.go | 45 +- .../x/sys/unix/zsyscall_linux_386.go | 30 + .../x/sys/unix/zsyscall_linux_amd64.go | 30 + .../x/sys/unix/zsyscall_linux_arm.go | 30 + .../x/sys/unix/zsyscall_linux_arm64.go | 30 + .../x/sys/unix/zsyscall_linux_mips.go | 30 + .../x/sys/unix/zsyscall_linux_mips64.go | 30 + .../x/sys/unix/zsyscall_linux_mips64le.go | 30 + .../x/sys/unix/zsyscall_linux_mipsle.go | 30 + .../x/sys/unix/zsyscall_linux_ppc64.go | 30 + .../x/sys/unix/zsyscall_linux_ppc64le.go | 30 + .../x/sys/unix/zsyscall_linux_riscv64.go | 30 + .../x/sys/unix/zsyscall_linux_s390x.go | 30 + .../x/sys/unix/zsyscall_linux_sparc64.go | 30 + .../x/sys/unix/zsyscall_netbsd_386.go | 37 +- .../x/sys/unix/zsyscall_netbsd_amd64.go | 37 +- .../x/sys/unix/zsyscall_netbsd_arm.go | 37 +- .../x/sys/unix/zsyscall_netbsd_arm64.go | 37 +- .../x/sys/unix/zsyscall_openbsd_386.go | 37 +- .../x/sys/unix/zsyscall_openbsd_amd64.go | 37 +- .../x/sys/unix/zsyscall_openbsd_arm.go | 37 +- .../x/sys/unix/zsyscall_openbsd_arm64.go | 37 +- .../x/sys/unix/zsyscall_solaris_amd64.go | 5 +- .../x/sys/unix/zsysnum_linux_386.go | 2 + .../x/sys/unix/zsysnum_linux_amd64.go | 2 + .../x/sys/unix/zsysnum_linux_arm.go | 2 + .../x/sys/unix/zsysnum_linux_arm64.go | 1 + .../x/sys/unix/zsysnum_linux_mips.go | 1 + .../x/sys/unix/zsysnum_linux_mips64.go | 1 + .../x/sys/unix/zsysnum_linux_mips64le.go | 1 + .../x/sys/unix/zsysnum_linux_mipsle.go | 1 + .../x/sys/unix/zsysnum_linux_ppc64.go | 2 + .../x/sys/unix/zsysnum_linux_ppc64le.go | 2 + .../x/sys/unix/zsysnum_linux_riscv64.go | 2 + .../x/sys/unix/zsysnum_linux_s390x.go | 2 + .../x/sys/unix/zsysnum_linux_sparc64.go | 1 + .../golang.org/x/sys/unix/ztypes_linux_386.go | 74 +- .../x/sys/unix/ztypes_linux_amd64.go | 74 +- .../golang.org/x/sys/unix/ztypes_linux_arm.go | 74 +- .../x/sys/unix/ztypes_linux_arm64.go | 74 +- .../x/sys/unix/ztypes_linux_mips.go | 74 +- .../x/sys/unix/ztypes_linux_mips64.go | 74 +- .../x/sys/unix/ztypes_linux_mips64le.go | 74 +- .../x/sys/unix/ztypes_linux_mipsle.go | 74 +- .../x/sys/unix/ztypes_linux_ppc64.go | 74 +- .../x/sys/unix/ztypes_linux_ppc64le.go | 74 +- .../x/sys/unix/ztypes_linux_riscv64.go | 74 +- .../x/sys/unix/ztypes_linux_s390x.go | 74 +- .../x/sys/unix/ztypes_linux_sparc64.go | 74 +- .../x/sys/windows/asm_windows_386.s | 13 - .../x/sys/windows/asm_windows_amd64.s | 13 - .../x/sys/windows/asm_windows_arm.s | 11 - .../golang.org/x/sys/windows/dll_windows.go | 22 +- vendor/golang.org/x/sys/windows/mksyscall.go | 2 +- .../x/sys/windows/security_windows.go | 558 ++++++++- .../x/sys/windows/syscall_windows.go | 45 +- .../golang.org/x/sys/windows/types_windows.go | 90 +- .../x/sys/windows/zsyscall_windows.go | 1071 ++++++++++++----- vendor/modules.txt | 5 +- web_src/js/index.js | 8 +- 177 files changed, 6262 insertions(+), 1303 deletions(-) create mode 100644 modules/auth/sso/basic.go create mode 100644 modules/auth/sso/interface.go create mode 100644 modules/auth/sso/oauth2.go create mode 100644 modules/auth/sso/reverseproxy.go create mode 100644 modules/auth/sso/session.go create mode 100644 modules/auth/sso/sso.go create mode 100644 modules/auth/sso/sspi_windows.go create mode 100644 templates/admin/auth/source/sspi.tmpl create mode 100644 vendor/github.com/quasoft/websspi/.gitignore create mode 100644 vendor/github.com/quasoft/websspi/.travis.yml create mode 100644 vendor/github.com/quasoft/websspi/LICENSE create mode 100644 vendor/github.com/quasoft/websspi/README.md create mode 100644 vendor/github.com/quasoft/websspi/go.mod create mode 100644 vendor/github.com/quasoft/websspi/go.sum create mode 100644 vendor/github.com/quasoft/websspi/secctx/session.go create mode 100644 vendor/github.com/quasoft/websspi/secctx/store.go create mode 100644 vendor/github.com/quasoft/websspi/userinfo.go create mode 100644 vendor/github.com/quasoft/websspi/utf16.go create mode 100644 vendor/github.com/quasoft/websspi/websspi_windows.go create mode 100644 vendor/github.com/quasoft/websspi/win32_windows.go create mode 100644 vendor/golang.org/x/sys/cpu/cpu_linux_arm.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin.1_12.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin.1_13.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin_386.1_11.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin_amd64.1_11.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin_arm.1_11.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin_arm64.1_11.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_386.1_13.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_386.1_13.s create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.1_13.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.1_13.s create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_arm.1_13.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_arm.1_13.s create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.1_13.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.1_13.s delete mode 100644 vendor/golang.org/x/sys/windows/asm_windows_386.s delete mode 100644 vendor/golang.org/x/sys/windows/asm_windows_amd64.s delete mode 100644 vendor/golang.org/x/sys/windows/asm_windows_arm.s diff --git a/docs/content/doc/features/authentication.en-us.md b/docs/content/doc/features/authentication.en-us.md index afa92196763c..77856c7c9fcf 100644 --- a/docs/content/doc/features/authentication.en-us.md +++ b/docs/content/doc/features/authentication.en-us.md @@ -216,3 +216,40 @@ configure this, set the fields below: - Log in to Gitea as an Administrator and click on "Authentication" under Admin Panel. Then click `Add New Source` and fill in the details, changing all where appropriate. + +## SSPI (Kerberos/NTLM SPNEGO, for Windows only) + +Gitea supports SPNEGO single sign-on authentication (the scheme defined by RFC4559) for the web part of the server via the Security Support Provider Interface (SSPI) built in Windows. SSPI works only in Windows environments - when both the server and the clients are running Windows. + +Before activating SSPI single sign-on authentication (SSO) you have to prepare your environment: + +- Create a separate user account in active directory, under which the `gitea.exe` process will be running (eg. `user` under domain `domain.local`): + +- Create a service principal name for the host where `gitea.exe` is running with class `HTTP`: + - Start `Command Prompt` or `PowerShell` as a priviledged domain user (eg. Domain Administrator) + - Run the command below, replacing `host.domain.local` with the fully qualified domain name (FQDN) of the server where the web application will be running, and `domain\user` with the name of the account created in the previous step: + ``` + setspn -A HTTP/host.domain.local domain\user + ``` + +- Sign in (*sign out if you were already signed in*) with the user created + +- Start the web server (`gitea.exe web`) + +- Enable SSPI authentication by adding an SSPI authentication source in `Site Administration -> Authentication Sources` + +- Sign in to a client computer in the same domain with any domain user + +- If you are using Chrome, Edge or Internet Explorer, add the URL of the web app to the Local intranet sites (`Internet Options -> Security -> Local intranet -> Sites`) + +- Start Chrome, Edge or Internet Explorer and navigate to FQDN URL of gitea (eg. `http://host.domain.local:3000`) + +- Click the `Sign In` button on the dashboard and you should be automatically logged in with the same user that is currently logged on to the computer + +- If it does not work, make sure that: + - You are not running the web browser on the same server where gitea is running. You should be running the web browser on a domain joined computer (client) that is different from the server. If both the client and server are runnning on the same computer NTLM will be prefered over Kerberos. + - There is only one `HTTP/...` SPN for the host + - The SPN contains only the hostname, without the port + - You have added the URL of the web app to the `Local intranet zone` + - The clocks of the server and client should not differ with more than 5 minutes (depends on group policy) + - `Integrated Windows Authentication` should be enabled in Internet Explorer (under `Advanced settings`) diff --git a/go.mod b/go.mod index 3c99c64e93f4..621d93e91274 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,7 @@ require ( github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e github.com/prometheus/client_golang v1.1.0 github.com/prometheus/procfs v0.0.4 // indirect + github.com/quasoft/websspi v1.0.0 github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001 // indirect github.com/russross/blackfriday/v2 v2.0.1 github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect @@ -101,7 +102,7 @@ require ( golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f golang.org/x/net v0.0.0-20191101175033-0deb6923b6d9 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 - golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b + golang.org/x/sys v0.0.0-20191010194322-b09406accb47 golang.org/x/text v0.3.2 golang.org/x/tools v0.0.0-20190910221609-7f5965fd7709 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index 344371168789..4bdeda9e7718 100644 --- a/go.sum +++ b/go.sum @@ -459,6 +459,8 @@ github.com/prometheus/procfs v0.0.4 h1:w8DjqFMJDjuVwdZBQoOozr4MVWOnwF7RcL/7uxBjY github.com/prometheus/procfs v0.0.4/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= +github.com/quasoft/websspi v1.0.0 h1:5nDgdM5xSur9s+B5w2xQ5kxf5nUGqgFgU4W0aDLZ8Mw= +github.com/quasoft/websspi v1.0.0/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001 h1:YDeskXpkNDhPdWN3REluVa46HQOVuVkjkd2sWnrABNQ= github.com/remyoudompheng/bigfft v0.0.0-20190321074620-2f0d2b0e0001/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -656,8 +658,8 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190730183949-1393eb018365/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b h1:3S2h5FadpNr0zUUCVZjlKIEYF+KaX/OBplTGo89CYHI= -golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= diff --git a/models/login_source.go b/models/login_source.go index b8441adcc4c7..4fbaf9c2aa5c 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -39,6 +39,7 @@ const ( LoginPAM // 4 LoginDLDAP // 5 LoginOAuth2 // 6 + LoginSSPI // 7 ) // LoginNames contains the name of LoginType values. @@ -48,6 +49,7 @@ var LoginNames = map[LoginType]string{ LoginSMTP: "SMTP", LoginPAM: "PAM", LoginOAuth2: "OAuth2", + LoginSSPI: "SPNEGO with SSPI", } // SecurityProtocolNames contains the name of SecurityProtocol values. @@ -63,6 +65,7 @@ var ( _ core.Conversion = &SMTPConfig{} _ core.Conversion = &PAMConfig{} _ core.Conversion = &OAuth2Config{} + _ core.Conversion = &SSPIConfig{} ) // LDAPConfig holds configuration for LDAP login source. @@ -140,6 +143,24 @@ func (cfg *OAuth2Config) ToDB() ([]byte, error) { return json.Marshal(cfg) } +// SSPIConfig holds configuration for SSPI single sign-on. +type SSPIConfig struct { + AutoCreateUsers bool + AutoActivateUsers bool + SeparatorReplacement string + DefaultLanguage string +} + +// FromDB fills up an SSPIConfig from serialized format. +func (cfg *SSPIConfig) FromDB(bs []byte) error { + return json.Unmarshal(bs, cfg) +} + +// ToDB exports an SSPIConfig to a serialized format. +func (cfg *SSPIConfig) ToDB() ([]byte, error) { + return json.Marshal(cfg) +} + // LoginSource represents an external way for authorizing users. type LoginSource struct { ID int64 `xorm:"pk autoincr"` @@ -176,6 +197,8 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { source.Cfg = new(PAMConfig) case LoginOAuth2: source.Cfg = new(OAuth2Config) + case LoginSSPI: + source.Cfg = new(SSPIConfig) default: panic("unrecognized login source type: " + com.ToStr(*val)) } @@ -212,6 +235,11 @@ func (source *LoginSource) IsOAuth2() bool { return source.Type == LoginOAuth2 } +// IsSSPI returns true of this source is of the SSPI type. +func (source *LoginSource) IsSSPI() bool { + return source.Type == LoginSSPI +} + // HasTLS returns true of this source supports TLS. func (source *LoginSource) HasTLS() bool { return ((source.IsLDAP() || source.IsDLDAP()) && @@ -264,6 +292,11 @@ func (source *LoginSource) OAuth2() *OAuth2Config { return source.Cfg.(*OAuth2Config) } +// SSPI returns SSPIConfig for this source, if of SSPI type. +func (source *LoginSource) SSPI() *SSPIConfig { + return source.Cfg.(*SSPIConfig) +} + // CreateLoginSource inserts a LoginSource in the DB if not already // existing with the given name. func CreateLoginSource(source *LoginSource) error { @@ -300,6 +333,15 @@ func LoginSources() ([]*LoginSource, error) { return auths, x.Find(&auths) } +// ActiveLoginSources returns all active sources of the specified type +func ActiveLoginSources(loginType LoginType) ([]*LoginSource, error) { + sources := make([]*LoginSource, 0, 1) + if err := x.Where("is_actived = ? and type = ?", true, loginType).Find(&sources); err != nil { + return nil, err + } + return sources, nil +} + // GetLoginSourceByID returns login source by given ID. func GetLoginSourceByID(id int64) (*LoginSource, error) { source := new(LoginSource) @@ -719,8 +761,8 @@ func UserSignIn(username, password string) (*User, error) { } for _, source := range sources { - if source.IsOAuth2() { - // don't try to authenticate against OAuth2 sources + if source.IsOAuth2() || source.IsSSPI() { + // don't try to authenticate against OAuth2 and SSPI sources here continue } authUser, err := ExternalUserLogin(nil, username, password, source, true) diff --git a/modules/auth/auth.go b/modules/auth/auth.go index 1ba149f0f875..7143067988fd 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -8,19 +8,14 @@ package auth import ( "reflect" "strings" - "time" "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/auth/sso" "code.gitea.io/gitea/modules/validation" "gitea.com/macaron/binding" "gitea.com/macaron/macaron" "gitea.com/macaron/session" - gouuid "github.com/satori/go.uuid" "github.com/unknwon/com" ) @@ -34,87 +29,6 @@ func IsAttachmentDownload(ctx *macaron.Context) bool { return strings.HasPrefix(ctx.Req.URL.Path, "/attachments/") && ctx.Req.Method == "GET" } -// SignedInID returns the id of signed in user. -func SignedInID(ctx *macaron.Context, sess session.Store) int64 { - if !models.HasEngine { - return 0 - } - - // Check access token. - if IsAPIPath(ctx.Req.URL.Path) || IsAttachmentDownload(ctx) { - tokenSHA := ctx.Query("token") - if len(tokenSHA) == 0 { - tokenSHA = ctx.Query("access_token") - } - if len(tokenSHA) == 0 { - // Well, check with header again. - auHead := ctx.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] - } - } - } - - // Let's see if token is valid. - if len(tokenSHA) > 0 { - if strings.Contains(tokenSHA, ".") { - uid := CheckOAuthAccessToken(tokenSHA) - if uid != 0 { - ctx.Data["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) - } - ctx.Data["IsApiToken"] = true - return t.UID - } - } - - uid := sess.Get("uid") - if uid == nil { - return 0 - } else if id, ok := uid.(int64); ok { - return id - } - return 0 -} - -// CheckOAuthAccessToken returns uid of user from oauth token 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 -} - // SignedInUser returns the user object of signed user. // It returns a bool value to indicate whether user uses basic auth or not. func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) { @@ -122,125 +36,18 @@ func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) return nil, false } - if uid := SignedInID(ctx, sess); uid > 0 { - user, err := models.GetUserByID(uid) - if err == nil { - return user, false - } else if !models.IsErrUserNotExist(err) { - log.Error("GetUserById: %v", err) + // Try to sign in with each of the enabled plugins + for _, ssoMethod := range sso.MethodsByPriority() { + if !ssoMethod.IsEnabled() { + continue } - } - - if setting.Service.EnableReverseProxyAuth { - webAuthUser := ctx.Req.Header.Get(setting.ReverseProxyAuthUser) - if len(webAuthUser) > 0 { - u, err := models.GetUserByName(webAuthUser) - if err != nil { - if !models.IsErrUserNotExist(err) { - log.Error("GetUserByName: %v", err) - return nil, false - } - - // Check if enabled auto-registration. - if setting.Service.EnableReverseProxyAutoRegister { - email := gouuid.NewV4().String() + "@localhost" - if setting.Service.EnableReverseProxyEmail { - webAuthEmail := ctx.Req.Header.Get(setting.ReverseProxyAuthEmail) - if len(webAuthEmail) > 0 { - email = webAuthEmail - } - } - u := &models.User{ - Name: webAuthUser, - Email: email, - Passwd: webAuthUser, - IsActive: true, - } - if err = models.CreateUser(u); err != nil { - // FIXME: should I create a system notice? - log.Error("CreateUser: %v", err) - return nil, false - } - return u, false - } - } - return u, false + user := ssoMethod.VerifyAuthData(ctx, sess) + if user != nil { + _, isBasic := ssoMethod.(*sso.Basic) + return user, isBasic } } - // Check with basic auth. - baHead := ctx.Req.Header.Get("Authorization") - if len(baHead) > 0 { - auths := strings.Fields(baHead) - if len(auths) == 2 && auths[0] == "Basic" { - var u *models.User - - 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 { - // Assume password is token - authToken = passwd - } - - uid := CheckOAuthAccessToken(authToken) - if uid != 0 { - var err error - ctx.Data["IsApiToken"] = true - - u, err = models.GetUserByID(uid) - if err != nil { - log.Error("GetUserByID: %v", err) - return nil, false - } - } - token, err := models.GetAccessTokenBySHA(authToken) - if err == nil { - if isUsernameToken { - u, err = models.GetUserByID(token.UID) - if err != nil { - log.Error("GetUserByID: %v", err) - return nil, false - } - } else { - u, err = models.GetUserByName(uname) - if err != nil { - log.Error("GetUserByID: %v", err) - return nil, false - } - if u.ID != token.UID { - return nil, false - } - } - token.UpdatedUnix = timeutil.TimeStampNow() - if err = models.UpdateAccessToken(token); err != nil { - log.Error("UpdateAccessToken: %v", err) - } - } else if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) { - log.Error("GetAccessTokenBySha: %v", err) - } - - if u == nil { - if !setting.Service.EnableBasicAuth { - return nil, false - } - u, err = models.UserSignIn(uname, passwd) - if err != nil { - if !models.IsErrUserNotExist(err) { - log.Error("UserSignIn: %v", err) - } - return nil, false - } - } else { - ctx.Data["IsApiToken"] = true - } - - return u, true - } - } return nil, false } diff --git a/modules/auth/auth_form.go b/modules/auth/auth_form.go index 358472a3855b..a3ec04aae780 100644 --- a/modules/auth/auth_form.go +++ b/modules/auth/auth_form.go @@ -12,7 +12,7 @@ import ( // AuthenticationForm form for authentication type AuthenticationForm struct { ID int64 - Type int `binding:"Range(2,6)"` + Type int `binding:"Range(2,7)"` Name string `binding:"Required;MaxSize(30)"` Host string Port int @@ -49,6 +49,10 @@ type AuthenticationForm struct { Oauth2AuthURL string Oauth2ProfileURL string Oauth2EmailURL string + SSPIAutoCreateUsers bool + SSPIAutoActivateUsers bool + SSPISeparatorReplacement string `binding:"Required;AlphaDashDot;MaxSize(5)"` + SSPIDefaultLanguage string } // Validate validates fields diff --git a/modules/auth/sso/basic.go b/modules/auth/sso/basic.go new file mode 100644 index 000000000000..2acd978eaa26 --- /dev/null +++ b/modules/auth/sso/basic.go @@ -0,0 +1,126 @@ +package sso + +import ( + "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" + + "gitea.com/macaron/macaron" + "gitea.com/macaron/session" +) + +// 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 setting.Service.EnableBasicAuth +} + +// Priority determines the order in which authentication methods are executed. +// The lower the priority, the sooner the plugin is executed. +func (b *Basic) Priority() int { + return 40000 +} + +// 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(ctx *macaron.Context, sess session.Store) *models.User { + baHead := ctx.Req.Header.Get("Authorization") + if len(baHead) == 0 { + return nil + } + + auths := strings.Fields(baHead) + if len(auths) != 2 || auths[0] != "Basic" { + return nil + } + + var u *models.User + 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 { + // Assume password is token + authToken = passwd + } + + uid := CheckOAuthAccessToken(authToken) + if uid != 0 { + var err error + ctx.Data["IsApiToken"] = true + + u, err = models.GetUserByID(uid) + if err != nil { + log.Error("GetUserByID: %v", err) + return nil + } + } + token, err := models.GetAccessTokenBySHA(authToken) + if err == nil { + if isUsernameToken { + u, err = models.GetUserByID(token.UID) + if err != nil { + log.Error("GetUserByID: %v", err) + return nil + } + } else { + u, err = models.GetUserByName(uname) + if err != nil { + log.Error("GetUserByID: %v", err) + return nil + } + if u.ID != token.UID { + return nil + } + } + token.UpdatedUnix = timeutil.TimeStampNow() + if err = models.UpdateAccessToken(token); err != nil { + log.Error("UpdateAccessToken: %v", err) + } + } else if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) { + log.Error("GetAccessTokenBySha: %v", err) + } + + if u == nil { + u, err = models.UserSignIn(uname, passwd) + if err != nil { + if !models.IsErrUserNotExist(err) { + log.Error("UserSignIn: %v", err) + } + return nil + } + } else { + ctx.Data["IsApiToken"] = true + } + + return u +} + +// init registers the plugin to the list of available SSO methods +func init() { + Register(&Basic{}) +} diff --git a/modules/auth/sso/interface.go b/modules/auth/sso/interface.go new file mode 100644 index 000000000000..c2ff5faf8183 --- /dev/null +++ b/modules/auth/sso/interface.go @@ -0,0 +1,33 @@ +package sso + +import ( + "code.gitea.io/gitea/models" + + "gitea.com/macaron/macaron" + "gitea.com/macaron/session" +) + +// 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 + + // Priority determines the order in which authentication methods are executed. + // The lower the priority, the sooner the plugin is executed. + Priority() int + + // 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(ctx *macaron.Context, sess session.Store) *models.User +} diff --git a/modules/auth/sso/oauth2.go b/modules/auth/sso/oauth2.go new file mode 100644 index 000000000000..faae3d700619 --- /dev/null +++ b/modules/auth/sso/oauth2.go @@ -0,0 +1,146 @@ +package sso + +import ( + "strings" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/timeutil" + + "gitea.com/macaron/macaron" + "gitea.com/macaron/session" +) + +// 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(ctx *macaron.Context) int64 { + // Check access token. + tokenSHA := ctx.Query("token") + if len(tokenSHA) == 0 { + tokenSHA = ctx.Query("access_token") + } + if len(tokenSHA) == 0 { + // Well, check with header again. + auHead := ctx.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 { + ctx.Data["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) + } + ctx.Data["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 +} + +// Priority determines the order in which authentication methods are executed. +// The lower the priority, the sooner the plugin is executed. +// The OAuth2 plugin should 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). +func (o *OAuth2) Priority() int { + return 10000 +} + +// 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(ctx *macaron.Context, sess session.Store) *models.User { + if !models.HasEngine { + return nil + } + + if !isAPIPath(ctx.Req.URL.Path) { + return nil + } + + id := o.userIDFromToken(ctx) + if id <= 0 { + return nil + } + + user, err := models.GetUserByID(id) + if err != nil { + if !models.IsErrUserNotExist(err) { + log.Error("GetUserByName: %v", err) + } + return nil + } + + return user +} + +// init registers the plugin to the list of available SSO methods +func init() { + Register(&OAuth2{}) +} diff --git a/modules/auth/sso/reverseproxy.go b/modules/auth/sso/reverseproxy.go new file mode 100644 index 000000000000..1752ec07df21 --- /dev/null +++ b/modules/auth/sso/reverseproxy.go @@ -0,0 +1,116 @@ +package sso + +import ( + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "gitea.com/macaron/macaron" + "gitea.com/macaron/session" + gouuid "github.com/satori/go.uuid" +) + +// 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(ctx *macaron.Context) string { + webAuthUser := strings.TrimSpace(ctx.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 +} + +// Priority determines the order in which authentication methods are executed. +// The lower the priority, the sooner the plugin is executed. +func (r *ReverseProxy) Priority() int { + return 30000 +} + +// 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(ctx *macaron.Context, sess session.Store) *models.User { + username := r.getUserName(ctx) + if len(username) == 0 { + return nil + } + + user, err := models.GetUserByName(username) + if err != nil { + if models.IsErrUserNotExist(err) && r.isAutoRegisterAllowed() { + return r.newUser(ctx) + } + log.Error("GetUserByName: %v", err) + return nil + } + + 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(ctx *macaron.Context) *models.User { + username := r.getUserName(ctx) + if len(username) == 0 { + return nil + } + + email := gouuid.NewV4().String() + "@example.org" + if setting.Service.EnableReverseProxyEmail { + webAuthEmail := ctx.Req.Header.Get(setting.ReverseProxyAuthEmail) + if len(webAuthEmail) > 0 { + email = webAuthEmail + } + } + + user := &models.User{ + Name: username, + Email: email, + Passwd: username, + 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 +} + +// init registers the plugin to the list of available SSO methods +func init() { + Register(&ReverseProxy{}) +} diff --git a/modules/auth/sso/session.go b/modules/auth/sso/session.go new file mode 100644 index 000000000000..1e99523791e9 --- /dev/null +++ b/modules/auth/sso/session.go @@ -0,0 +1,51 @@ +package sso + +import ( + "code.gitea.io/gitea/models" + + "gitea.com/macaron/macaron" + "gitea.com/macaron/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 +} + +// Priority determines the order in which authentication methods are executed. +// The lower the priority, the sooner the plugin is executed. +func (s *Session) Priority() int { + return 20000 +} + +// 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(ctx *macaron.Context, sess session.Store) *models.User { + user := SessionUser(sess) + if user != nil { + return user + } + return nil +} + +// init registers the plugin to the list of available SSO methods +func init() { + Register(&Session{}) +} diff --git a/modules/auth/sso/sso.go b/modules/auth/sso/sso.go new file mode 100644 index 000000000000..7413f2daa37a --- /dev/null +++ b/modules/auth/sso/sso.go @@ -0,0 +1,180 @@ +package sso + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "gitea.com/macaron/macaron" + "gitea.com/macaron/session" +) + +var ( + ssoMethods []SingleSignOn +) + +// Methods returns the instances of all registered SSO methods +func Methods() []SingleSignOn { + return ssoMethods +} + +// MethodsByPriority returns the instances of all registered SSO methods, ordered by ascending priority +func MethodsByPriority() []SingleSignOn { + methods := Methods() + sort.Slice(methods, func(i, j int) bool { + return methods[i].Priority() < methods[j].Priority() + }) + return methods +} + +// 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() { + if !method.IsEnabled() { + continue + } + 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() { + if !method.IsEnabled() { + continue + } + 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 session.Store) *models.User { + // Get user ID + uid := sess.Get("uid") + if uid == nil { + return nil + } + 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 + } + return user +} + +// isAPIPath returns true if the specified URL is an API path +func isAPIPath(url string) bool { + return strings.HasPrefix(url, "/api/") +} + +// isPublicResource checks if the url is of a public resource file that should be served +// without authentication (eg. the Web App Manifest, the Service Worker script or the favicon) +func isPublicResource(ctx *macaron.Context) bool { + path := strings.TrimSuffix(ctx.Req.URL.Path, "/") + return path == "/robots.txt" || + path == "/favicon.ico" || + path == "/favicon.png" || + path == "/manifest.json" || + path == "/serviceworker.js" +} + +// isPublicPage checks if the url is of a public page that should not require authentication +func isPublicPage(ctx *macaron.Context) bool { + path := strings.TrimSuffix(ctx.Req.URL.Path, "/") + homePage := strings.TrimSuffix(setting.AppSubURL, "/") + currentURL := homePage + path + return currentURL == homePage || + path == "/user/login" || + path == "/user/login/openid" || + path == "/user/sign_up" || + path == "/user/forgot_password" || + path == "/user/openid/connect" || + path == "/user/openid/register" || + strings.HasPrefix(path, "/user/oauth2") || + path == "/user/link_account" || + path == "/user/link_account_signin" || + path == "/user/link_account_signup" || + path == "/user/two_factor" || + path == "/user/two_factor/scratch" || + path == "/user/u2f" || + path == "/user/u2f/challenge" || + path == "/user/u2f/sign" || + (!setting.Service.RequireSignInView && (path == "/explore/repos" || + path == "/explore/users" || + path == "/explore/organizations" || + path == "/explore/code")) +} + +func handleSignIn(ctx *macaron.Context, sess session.Store, 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 { + user.Language = ctx.Locale.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 + } + } + + ctx.SetCookie("lang", user.Language, nil, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true) + + // Clear whatever CSRF has right now, force to generate a new one + ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true) +} + +// addFlashErr adds an error message to the Flash object mapped to a macaron.Context +func addFlashErr(ctx *macaron.Context, err string) { + fv := ctx.GetVal(reflect.TypeOf(&session.Flash{})) + if !fv.IsValid() { + return + } + flash, ok := fv.Interface().(*session.Flash) + if !ok { + return + } + flash.Error(err) + ctx.Data["Flash"] = flash +} diff --git a/modules/auth/sso/sspi_windows.go b/modules/auth/sso/sspi_windows.go new file mode 100644 index 000000000000..7e0e5e02cdd0 --- /dev/null +++ b/modules/auth/sso/sspi_windows.go @@ -0,0 +1,175 @@ +package sso + +import ( + "errors" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "gitea.com/macaron/macaron" + "gitea.com/macaron/session" + "github.com/quasoft/websspi" + gouuid "github.com/satori/go.uuid" +) + +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 +) + +// 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 { +} + +// Init creates a new global websspi.Authenticator object +func (s *SSPI) Init() error { + config := websspi.NewConfig() + var err error + sspiAuth, err = websspi.New(config) + return err +} + +// 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 { + sources, err := models.ActiveLoginSources(models.LoginSSPI) + if err != nil { + log.Warn("Could not get login sources: %v\n", err) + return false + } + return len(sources) > 0 +} + +// Priority determines the order in which authentication methods are executed. +// The lower the priority, the sooner the plugin is executed. +// The SSPI plugin should be executed last as it returns 401 status code +// if negotiation fails or should continue, which would prevent other +// authentication methods to execute at all. +func (s *SSPI) Priority() int { + return 50000 +} + +// 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(ctx *macaron.Context, sess session.Store) *models.User { + if !s.shouldAuthenticate(ctx) { + return nil + } + + cfg, err := s.getConfig() + if err != nil { + log.Error("could not get SSPI config: %v", err) + return nil + } + + userInfo, outToken, err := sspiAuth.Authenticate(ctx.Req.Request, ctx.Resp) + if err != nil { + log.Warn("Authentication failed with error: %v\n", err) + sspiAuth.AppendAuthenticateHeader(ctx.Resp, outToken) + + // Include the user login page in the 401 response to allow the user + // to login with another authentication method if SSPI authentication + // fails + addFlashErr(ctx, ctx.Tr("auth.sspi_auth_failed")) + ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn + ctx.Data["EnableSSPI"] = true + ctx.HTML(401, string(tplSignIn)) + return nil + } + if outToken != "" { + sspiAuth.AppendAuthenticateHeader(ctx.Resp, outToken) + } + + newSep := cfg.SeparatorReplacement + username := strings.ReplaceAll(userInfo.Username, "\\", newSep) + username = strings.ReplaceAll(username, "/", newSep) + username = strings.ReplaceAll(username, "@", newSep) + log.Info("Authenticated as %s\n", username) + if len(username) == 0 { + return nil + } + + user, err := models.GetUserByName(username) + if err != nil { + if models.IsErrUserNotExist(err) && cfg.AutoCreateUsers { + return s.newUser(ctx, username, cfg) + } + log.Error("GetUserByName: %v", err) + return nil + } + + // Make sure requests to API paths and PWA resources do not create a new session + if !isAPIPath(ctx.Req.URL.Path) { + handleSignIn(ctx, sess, 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") + } + return sources[0].SSPI(), nil +} + +func (s *SSPI) shouldAuthenticate(ctx *macaron.Context) bool { + path := strings.TrimSuffix(ctx.Req.URL.Path, "/") + if path == "/user/login" && ctx.Req.FormValue("user_name") != "" && ctx.Req.FormValue("password") != "" { + return false + } else if ctx.Req.FormValue("auth_with_sspi") == "1" { + return true + } + return !isPublicPage(ctx) && !isPublicResource(ctx) +} + +// 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 *macaron.Context, username string, cfg *models.SSPIConfig) *models.User { + email := gouuid.NewV4().String() + "@example.org" + user := &models.User{ + Name: username, + Email: email, + KeepEmailPrivate: true, + Passwd: gouuid.NewV4().String(), + IsActive: cfg.AutoActivateUsers, + Language: cfg.DefaultLanguage, + UseCustomAvatar: true, + Avatar: base.DefaultAvatarLink(), + EmailNotificationsPreference: models.EmailNotificationsDisabled, + } + if err := models.CreateUser(user); err != nil { + // FIXME: should I create a system notice? + log.Error("CreateUser: %v", err) + return nil + } + return user +} + +// init registers the plugin to the list of available SSO methods +func init() { + Register(&SSPI{}) +} diff --git a/modules/context/context.go b/modules/context/context.go index ef6c19ed125c..4e711c4137b6 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -340,6 +340,9 @@ func Contexter() macaron.Handler { ctx.Data["EnableSwagger"] = setting.API.EnableSwagger ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn + sspiSrc, err := models.ActiveLoginSources(models.LoginSSPI) + ctx.Data["EnableSSPI"] = err == nil && len(sspiSrc) > 0 + c.Map(ctx) } } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b38e909e4888..ce943b7bd906 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -270,6 +270,7 @@ authorize_title = Authorize "%s" to access your account? authorization_failed = Authorization failed authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you've tried to authorize. disable_forgot_password_mail = Account recovery is disabled. Please contact your site administrator. +sspi_auth_failed = SSPI authentication failed [mail] activate_account = Please activate your account @@ -352,6 +353,8 @@ org_still_own_repo = "This organization still owns one or more repositories; del target_branch_not_exist = Target branch does not exist. +SSPISeparatorReplacement = Separator + [user] change_avatar = Change your avatar… join_on = Joined on @@ -1809,6 +1812,14 @@ auths.oauth2_authURL = Authorize URL auths.oauth2_profileURL = Profile URL auths.oauth2_emailURL = Email URL auths.enable_auto_register = Enable Auto Registration +auths.sspi_auto_create_users = Automatically create users +auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new users on the go (default: true) +auths.sspi_auto_activate_users = Automatically activate users +auths.sspi_auto_activate_users_helper = Allow SSPI auth method to automatically activate new users (default: true) +auths.sspi_separator_replacement = Separator to use instead of \, / and @ +auths.sspi_separator_replacement_helper = The character to use (default: "_") 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 auths.tips = Tips auths.tips.oauth2.general = OAuth2 Authentication auths.tips.oauth2.general.tip = When registering a new OAuth2 authentication, the callback/redirect URL should be: /user/oauth2//callback diff --git a/public/js/gitgraph.js b/public/js/gitgraph.js index 6152e14ab359..9b61bc178dbd 100644 --- a/public/js/gitgraph.js +++ b/public/js/gitgraph.js @@ -1,2 +1,2 @@ -(window.webpackJsonp=window.webpackJsonp||[]).push([[0],[,,,,,,,,,,function(t,e,n){(t.exports=n(11)(!1)).push([t.i,"/* This is a customized version of https://github.com/bluef/gitgraph.js/blob/master/gitgraph.css\n Changes include the removal of `body` and `em` styles */\n#git-graph-container, #rel-container {float:left;}\n#rel-container {max-width:30%; overflow-x:auto;}\n#git-graph-container {overflow-x:auto; width:100%}\n#git-graph-container li {list-style-type:none;height:20px;line-height:20px; white-space:nowrap;}\n#git-graph-container li .node-relation {font-family:'Bitstream Vera Sans Mono', 'Courier', monospace;}\n#git-graph-container li .author {color:#666666;}\n#git-graph-container li .time {color:#999999;font-size:80%}\n#git-graph-container li a {color:#000000;}\n#git-graph-container li a:hover {text-decoration:underline;}\n#git-graph-container li a em {color:#BB0000;border-bottom:1px dotted #BBBBBB;text-decoration:none;font-style:normal;}\n#rev-container {width:100%}\n#rev-list {margin:0;padding:0 5px 0 5px;min-width:95%}\n#graph-raw-list {margin:0px;}\n",""])},function(t,e,n){"use strict";t.exports=function(t){var e=[];return e.toString=function(){return this.map((function(e){var n=function(t,e){var n=t[1]||"",i=t[3];if(!i)return n;if(e&&"function"==typeof btoa){var r=(a=i,c=btoa(unescape(encodeURIComponent(JSON.stringify(a)))),s="sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(c),"/*# ".concat(s," */")),o=i.sources.map((function(t){return"/*# sourceURL=".concat(i.sourceRoot).concat(t," */")}));return[n].concat(o).concat([r]).join("\n")}var a,c,s;return[n].join("\n")}(e,t);return e[2]?"@media ".concat(e[2],"{").concat(n,"}"):n})).join("")},e.i=function(t,n){"string"==typeof t&&(t=[[null,t,""]]);for(var i={},r=0;rP.length&&-1!==(x=l("*",E))&&-1===l("_",P)&&-1===l("/",P)&&-1===l("\\",P)&&i.splice(x+1,1)}for(R=P.length,o=0,s=0,T=0,M=-1;oM&&(P[o]="|",r="|")," "===r&&P[o+1]&&"_"===P[o+1]&&P[o-1]&&"|"===P[o-1]&&(P.splice(o,1),P[o]="/",r="/"),-1===_&&"/"===r&&P[o-1]&&"|"===P[o-1]&&i.splice(s,0,h()),("/"===r||"\\"===r)&&("/"!==r||-1!==u(N))&&-1!==(z=Math.max(l("|",P),l("*",P)))&&zk&&i.splice(k,i.length-k),o=0;oP.length&&-1!==(x=l("*",E))&&-1===l("_",P)&&-1===l("/",P)&&-1===l("\\",P)&&i.splice(x+1,1)}for(R=P.length,o=0,s=0,T=0,M=-1;oM&&(P[o]="|",r="|")," "===r&&P[o+1]&&"_"===P[o+1]&&P[o-1]&&"|"===P[o-1]&&(P.splice(o,1),P[o]="/",r="/"),-1===_&&"/"===r&&P[o-1]&&"|"===P[o-1]&&i.splice(s,0,h()),("/"===r||"\\"===r)&&("/"!==r||-1!==u(N))&&-1!==(z=Math.max(l("|",P),l("*",P)))&&zk&&i.splice(k,i.length-k),o=0;o\n // tags it will allow on a page\n\n if (!options.singleton && typeof options.singleton !== 'boolean') {\n options.singleton = isOldIE();\n }\n\n var styles = listToStyles(list, options);\n addStylesToDom(styles, options);\n return function update(newList) {\n var mayRemove = [];\n\n for (var i = 0; i < styles.length; i++) {\n var item = styles[i];\n var domStyle = stylesInDom[item.id];\n\n if (domStyle) {\n domStyle.refs--;\n mayRemove.push(domStyle);\n }\n }\n\n if (newList) {\n var newStyles = listToStyles(newList, options);\n addStylesToDom(newStyles, options);\n }\n\n for (var _i = 0; _i < mayRemove.length; _i++) {\n var _domStyle = mayRemove[_i];\n\n if (_domStyle.refs === 0) {\n for (var j = 0; j < _domStyle.parts.length; j++) {\n _domStyle.parts[j]();\n }\n\n delete stylesInDom[_domStyle.id];\n }\n }\n };\n};","/* This is a customized version of https://github.com/bluef/gitgraph.js/blob/master/gitgraph.js\n Changes include conversion to ES6 and linting fixes */\n\n/*\n * @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD 3-Clause\n * Copyright (c) 2011, Terrence Lee \n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are met:\n * * Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * * Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * * Neither the name of the nor the\n * names of its contributors may be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY\n * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n\nexport default function gitGraph(canvas, rawGraphList, config) {\n if (!canvas.getContext) {\n return;\n }\n\n if (typeof config === 'undefined') {\n config = {\n unitSize: 20,\n lineWidth: 3,\n nodeRadius: 4\n };\n }\n\n const flows = [];\n const graphList = [];\n\n const ctx = canvas.getContext('2d');\n\n const devicePixelRatio = window.devicePixelRatio || 1;\n const backingStoreRatio = ctx.webkitBackingStorePixelRatio\n || ctx.mozBackingStorePixelRatio\n || ctx.msBackingStorePixelRatio\n || ctx.oBackingStorePixelRatio\n || ctx.backingStorePixelRatio || 1;\n\n const ratio = devicePixelRatio / backingStoreRatio;\n\n const init = function () {\n let maxWidth = 0;\n let i;\n const l = rawGraphList.length;\n let row;\n let midStr;\n\n for (i = 0; i < l; i++) {\n midStr = rawGraphList[i].replace(/\\s+/g, ' ').replace(/^\\s+|\\s+$/g, '');\n\n maxWidth = Math.max(midStr.replace(/(_|\\s)/g, '').length, maxWidth);\n\n row = midStr.split('');\n\n graphList.unshift(row);\n }\n\n const width = maxWidth * config.unitSize;\n const height = graphList.length * config.unitSize;\n\n canvas.width = width * ratio;\n canvas.height = height * ratio;\n\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n\n ctx.lineWidth = config.lineWidth;\n ctx.lineJoin = 'round';\n ctx.lineCap = 'round';\n\n ctx.scale(ratio, ratio);\n };\n\n const genRandomStr = function () {\n const chars = '0123456789ABCDEF';\n const stringLength = 6;\n let randomString = '', rnum, i;\n for (i = 0; i < stringLength; i++) {\n rnum = Math.floor(Math.random() * chars.length);\n randomString += chars.substring(rnum, rnum + 1);\n }\n\n return randomString;\n };\n\n const findFlow = function (id) {\n let i = flows.length;\n\n while (i-- && flows[i].id !== id);\n\n return i;\n };\n\n const findColomn = function (symbol, row) {\n let i = row.length;\n\n while (i-- && row[i] !== symbol);\n\n return i;\n };\n\n const findBranchOut = function (row) {\n if (!row) {\n return -1;\n }\n\n let i = row.length;\n\n while (i--\n && !(row[i - 1] && row[i] === '/' && row[i - 1] === '|')\n && !(row[i - 2] && row[i] === '_' && row[i - 2] === '|'));\n\n return i;\n };\n\n const findLineBreak = function (row) {\n if (!row) {\n return -1;\n }\n\n let i = row.length;\n\n while (i--\n && !(row[i - 1] && row[i - 2] && row[i] === ' ' && row[i - 1] === '|' && row[i - 2] === '_'));\n\n return i;\n };\n\n const genNewFlow = function () {\n let newId;\n\n do {\n newId = genRandomStr();\n } while (findFlow(newId) !== -1);\n\n return { id: newId, color: `#${newId}` };\n };\n\n // Draw methods\n const drawLine = function (moveX, moveY, lineX, lineY, color) {\n ctx.strokeStyle = color;\n ctx.beginPath();\n ctx.moveTo(moveX, moveY);\n ctx.lineTo(lineX, lineY);\n ctx.stroke();\n };\n\n const drawLineRight = function (x, y, color) {\n drawLine(x, y + config.unitSize / 2, x + config.unitSize, y + config.unitSize / 2, color);\n };\n\n const drawLineUp = function (x, y, color) {\n drawLine(x, y + config.unitSize / 2, x, y - config.unitSize / 2, color);\n };\n\n const drawNode = function (x, y, color) {\n ctx.strokeStyle = color;\n\n drawLineUp(x, y, color);\n\n ctx.beginPath();\n ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true);\n ctx.fill();\n };\n\n const drawLineIn = function (x, y, color) {\n drawLine(x + config.unitSize, y + config.unitSize / 2, x, y - config.unitSize / 2, color);\n };\n\n const drawLineOut = function (x, y, color) {\n drawLine(x, y + config.unitSize / 2, x + config.unitSize, y - config.unitSize / 2, color);\n };\n\n const draw = function (graphList) {\n let colomn, colomnIndex, prevColomn, condenseIndex, breakIndex = -1;\n let x, y;\n let color;\n let nodePos;\n let tempFlow;\n let prevRowLength = 0;\n let flowSwapPos = -1;\n let lastLinePos;\n let i, l;\n let condenseCurrentLength, condensePrevLength = 0;\n\n let inlineIntersect = false;\n\n // initiate color array for first row\n for (i = 0, l = graphList[0].length; i < l; i++) {\n if (graphList[0][i] !== '_' && graphList[0][i] !== ' ') {\n flows.push(genNewFlow());\n }\n }\n\n y = (canvas.height / ratio) - config.unitSize * 0.5;\n\n // iterate\n for (i = 0, l = graphList.length; i < l; i++) {\n x = config.unitSize * 0.5;\n\n const currentRow = graphList[i];\n const nextRow = graphList[i + 1];\n const prevRow = graphList[i - 1];\n\n flowSwapPos = -1;\n\n condenseCurrentLength = currentRow.filter((val) => {\n return (val !== ' ' && val !== '_');\n }).length;\n\n // pre process begin\n // use last row for analysing\n if (prevRow) {\n if (!inlineIntersect) {\n // intersect might happen\n for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) {\n if (prevRow[colomnIndex + 1]\n && (prevRow[colomnIndex] === '/' && prevRow[colomnIndex + 1] === '|')\n || ((prevRow[colomnIndex] === '_' && prevRow[colomnIndex + 1] === '|')\n && (prevRow[colomnIndex + 2] === '/'))) {\n flowSwapPos = colomnIndex;\n\n // swap two flow\n tempFlow = { id: flows[flowSwapPos].id, color: flows[flowSwapPos].color };\n\n flows[flowSwapPos].id = flows[flowSwapPos + 1].id;\n flows[flowSwapPos].color = flows[flowSwapPos + 1].color;\n\n flows[flowSwapPos + 1].id = tempFlow.id;\n flows[flowSwapPos + 1].color = tempFlow.color;\n }\n }\n }\n\n /* eslint-disable-next-line */\n if (condensePrevLength < condenseCurrentLength\n && ((nodePos = findColomn('*', currentRow)) !== -1\n && (findColomn('_', currentRow) === -1))) {\n flows.splice(nodePos - 1, 0, genNewFlow());\n }\n\n /* eslint-disable-next-line */\n if (prevRowLength > currentRow.length\n && (nodePos = findColomn('*', prevRow)) !== -1) {\n if (findColomn('_', currentRow) === -1\n && findColomn('/', currentRow) === -1\n && findColomn('\\\\', currentRow) === -1) {\n flows.splice(nodePos + 1, 1);\n }\n }\n } // done with the previous row\n\n prevRowLength = currentRow.length; // store for next round\n colomnIndex = 0; // reset index\n condenseIndex = 0;\n condensePrevLength = 0;\n breakIndex = -1; // reset break index\n while (colomnIndex < currentRow.length) {\n colomn = currentRow[colomnIndex];\n\n if (colomn !== ' ' && colomn !== '_') {\n ++condensePrevLength;\n }\n\n // check and fix line break in next row\n if (colomn === '/' && currentRow[colomnIndex - 1] && currentRow[colomnIndex - 1] === '|') {\n /* eslint-disable-next-line */\n if ((breakIndex = findLineBreak(nextRow)) !== -1) {\n nextRow.splice(breakIndex, 1);\n }\n }\n // if line break found replace all '/' with '|' after breakIndex in previous row\n if (breakIndex !== -1 && colomn === '/' && colomnIndex > breakIndex) {\n currentRow[colomnIndex] = '|';\n colomn = '|';\n }\n\n if (colomn === ' '\n && currentRow[colomnIndex + 1]\n && currentRow[colomnIndex + 1] === '_'\n && currentRow[colomnIndex - 1]\n && currentRow[colomnIndex - 1] === '|') {\n currentRow.splice(colomnIndex, 1);\n\n currentRow[colomnIndex] = '/';\n colomn = '/';\n }\n\n // create new flow only when no intersect happened\n if (flowSwapPos === -1\n && colomn === '/'\n && currentRow[colomnIndex - 1]\n && currentRow[colomnIndex - 1] === '|') {\n flows.splice(condenseIndex, 0, genNewFlow());\n }\n\n // change \\ and / to | when it's in the last position of the whole row\n if (colomn === '/' || colomn === '\\\\') {\n if (!(colomn === '/' && findBranchOut(nextRow) === -1)) {\n /* eslint-disable-next-line */\n if ((lastLinePos = Math.max(findColomn('|', currentRow),\n findColomn('*', currentRow))) !== -1\n && (lastLinePos < colomnIndex - 1)) {\n while (currentRow[++lastLinePos] === ' ');\n\n if (lastLinePos === colomnIndex) {\n currentRow[colomnIndex] = '|';\n }\n }\n }\n }\n\n if (colomn === '*'\n && prevRow\n && prevRow[condenseIndex + 1] === '\\\\') {\n flows.splice(condenseIndex + 1, 1);\n }\n\n if (colomn !== ' ') {\n ++condenseIndex;\n }\n\n ++colomnIndex;\n }\n\n condenseCurrentLength = currentRow.filter((val) => {\n return (val !== ' ' && val !== '_');\n }).length;\n\n // do some clean up\n if (flows.length > condenseCurrentLength) {\n flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength);\n }\n\n colomnIndex = 0;\n\n // a little inline analysis and draw process\n while (colomnIndex < currentRow.length) {\n colomn = currentRow[colomnIndex];\n prevColomn = currentRow[colomnIndex - 1];\n\n if (currentRow[colomnIndex] === ' ') {\n currentRow.splice(colomnIndex, 1);\n x += config.unitSize;\n\n continue;\n }\n\n // inline interset\n if ((colomn === '_' || colomn === '/')\n && currentRow[colomnIndex - 1] === '|'\n && currentRow[colomnIndex - 2] === '_') {\n inlineIntersect = true;\n\n tempFlow = flows.splice(colomnIndex - 2, 1)[0];\n flows.splice(colomnIndex - 1, 0, tempFlow);\n currentRow.splice(colomnIndex - 2, 1);\n\n colomnIndex -= 1;\n } else {\n inlineIntersect = false;\n }\n\n color = flows[colomnIndex].color;\n\n switch (colomn) {\n case '_':\n drawLineRight(x, y, color);\n\n x += config.unitSize;\n break;\n\n case '*':\n drawNode(x, y, color);\n break;\n\n case '|':\n drawLineUp(x, y, color);\n break;\n\n case '/':\n if (prevColomn\n && (prevColomn === '/'\n || prevColomn === ' ')) {\n x -= config.unitSize;\n }\n\n drawLineOut(x, y, color);\n\n x += config.unitSize;\n break;\n\n case '\\\\':\n drawLineIn(x, y, color);\n break;\n }\n\n ++colomnIndex;\n }\n\n y -= config.unitSize;\n }\n };\n\n init();\n draw(graphList);\n}\n// @end-license\n","var content = require(\"!!../../node_modules/css-loader/dist/cjs.js!./gitGraph.css\");\n\nif (typeof content === 'string') {\n content = [[module.id, content, '']];\n}\n\nvar options = {}\n\noptions.insert = \"head\";\noptions.singleton = false;\n\nvar update = require(\"!../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\")(content, options);\n\nif (content.locals) {\n module.exports = content.locals;\n}\n"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack:///./web_src/css/gitGraph.css","webpack:///./node_modules/css-loader/dist/runtime/api.js","webpack:///./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js","webpack:///./web_src/js/gitGraph.js","webpack:///./web_src/css/gitGraph.css?6696"],"names":["module","exports","push","i","useSourceMap","list","toString","this","map","item","content","cssMapping","btoa","sourceMapping","sourceMap","base64","unescape","encodeURIComponent","JSON","stringify","data","concat","sourceURLs","sources","source","sourceRoot","join","cssWithMappingToString","modules","mediaQuery","alreadyImportedModules","length","id","_i","memo","stylesInDom","isOldIE","Boolean","window","document","all","atob","getTarget","target","styleTarget","querySelector","HTMLIFrameElement","contentDocument","head","e","listToStyles","options","styles","newStyles","base","part","css","media","parts","addStylesToDom","domStyle","j","refs","addStyle","insertStyleElement","style","createElement","attributes","nonce","Object","keys","forEach","key","setAttribute","insert","Error","appendChild","textStore","replaceText","index","replacement","filter","applyToSingletonTag","remove","obj","styleSheet","cssText","cssNode","createTextNode","childNodes","removeChild","insertBefore","applyToTag","firstChild","singleton","singletonCounter","update","styleIndex","bind","parentNode","removeStyleElement","newObj","newList","mayRemove","_domStyle","gitGraph","canvas","rawGraphList","config","getContext","unitSize","lineWidth","nodeRadius","flows","graphList","ctx","ratio","devicePixelRatio","webkitBackingStorePixelRatio","mozBackingStorePixelRatio","msBackingStorePixelRatio","oBackingStorePixelRatio","backingStorePixelRatio","genRandomStr","rnum","chars","randomString","Math","floor","random","substring","findFlow","findColomn","symbol","row","findBranchOut","findLineBreak","genNewFlow","newId","color","drawLine","moveX","moveY","lineX","lineY","strokeStyle","beginPath","moveTo","lineTo","stroke","drawLineRight","x","y","drawLineUp","drawNode","arc","PI","fill","drawLineIn","drawLineOut","midStr","maxWidth","l","replace","max","split","unshift","width","height","lineJoin","lineCap","scale","init","colomn","colomnIndex","prevColomn","condenseIndex","nodePos","tempFlow","lastLinePos","condenseCurrentLength","breakIndex","prevRowLength","flowSwapPos","condensePrevLength","inlineIntersect","currentRow","nextRow","prevRow","val","splice","draw","locals"],"mappings":"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","file":"gitgraph.js","sourcesContent":["exports = module.exports = require(\"../../node_modules/css-loader/dist/runtime/api.js\")(false);\n// Module\nexports.push([module.id, \"/* This is a customized version of https://github.com/bluef/gitgraph.js/blob/master/gitgraph.css\\r\\n Changes include the removal of `body` and `em` styles */\\r\\n#git-graph-container, #rel-container {float:left;}\\r\\n#rel-container {max-width:30%; overflow-x:auto;}\\r\\n#git-graph-container {overflow-x:auto; width:100%}\\r\\n#git-graph-container li {list-style-type:none;height:20px;line-height:20px; white-space:nowrap;}\\r\\n#git-graph-container li .node-relation {font-family:'Bitstream Vera Sans Mono', 'Courier', monospace;}\\r\\n#git-graph-container li .author {color:#666666;}\\r\\n#git-graph-container li .time {color:#999999;font-size:80%}\\r\\n#git-graph-container li a {color:#000000;}\\r\\n#git-graph-container li a:hover {text-decoration:underline;}\\r\\n#git-graph-container li a em {color:#BB0000;border-bottom:1px dotted #BBBBBB;text-decoration:none;font-style:normal;}\\r\\n#rev-container {width:100%}\\r\\n#rev-list {margin:0;padding:0 5px 0 5px;min-width:95%}\\r\\n#graph-raw-list {margin:0px;}\\r\\n\", \"\"]);\n","\"use strict\";\n\n/*\n MIT License http://www.opensource.org/licenses/mit-license.php\n Author Tobias Koppers @sokra\n*/\n// css base code, injected by the css-loader\n// eslint-disable-next-line func-names\nmodule.exports = function (useSourceMap) {\n var list = []; // return the list of modules as css string\n\n list.toString = function toString() {\n return this.map(function (item) {\n var content = cssWithMappingToString(item, useSourceMap);\n\n if (item[2]) {\n return \"@media \".concat(item[2], \"{\").concat(content, \"}\");\n }\n\n return content;\n }).join('');\n }; // import a list of modules into the list\n // eslint-disable-next-line func-names\n\n\n list.i = function (modules, mediaQuery) {\n if (typeof modules === 'string') {\n // eslint-disable-next-line no-param-reassign\n modules = [[null, modules, '']];\n }\n\n var alreadyImportedModules = {};\n\n for (var i = 0; i < this.length; i++) {\n // eslint-disable-next-line prefer-destructuring\n var id = this[i][0];\n\n if (id != null) {\n alreadyImportedModules[id] = true;\n }\n }\n\n for (var _i = 0; _i < modules.length; _i++) {\n var item = modules[_i]; // skip already imported module\n // this implementation is not 100% perfect for weird media query combinations\n // when a module is imported multiple times with different media queries.\n // I hope this will never occur (Hey this way we have smaller bundles)\n\n if (item[0] == null || !alreadyImportedModules[item[0]]) {\n if (mediaQuery && !item[2]) {\n item[2] = mediaQuery;\n } else if (mediaQuery) {\n item[2] = \"(\".concat(item[2], \") and (\").concat(mediaQuery, \")\");\n }\n\n list.push(item);\n }\n }\n };\n\n return list;\n};\n\nfunction cssWithMappingToString(item, useSourceMap) {\n var content = item[1] || ''; // eslint-disable-next-line prefer-destructuring\n\n var cssMapping = item[3];\n\n if (!cssMapping) {\n return content;\n }\n\n if (useSourceMap && typeof btoa === 'function') {\n var sourceMapping = toComment(cssMapping);\n var sourceURLs = cssMapping.sources.map(function (source) {\n return \"/*# sourceURL=\".concat(cssMapping.sourceRoot).concat(source, \" */\");\n });\n return [content].concat(sourceURLs).concat([sourceMapping]).join('\\n');\n }\n\n return [content].join('\\n');\n} // Adapted from convert-source-map (MIT)\n\n\nfunction toComment(sourceMap) {\n // eslint-disable-next-line no-undef\n var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))));\n var data = \"sourceMappingURL=data:application/json;charset=utf-8;base64,\".concat(base64);\n return \"/*# \".concat(data, \" */\");\n}","\"use strict\";\n\nvar stylesInDom = {};\n\nvar isOldIE = function isOldIE() {\n var memo;\n return function memorize() {\n if (typeof memo === 'undefined') {\n // Test for IE <= 9 as proposed by Browserhacks\n // @see http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805\n // Tests for existence of standard globals is to allow style-loader\n // to operate correctly into non-standard environments\n // @see https://github.com/webpack-contrib/style-loader/issues/177\n memo = Boolean(window && document && document.all && !window.atob);\n }\n\n return memo;\n };\n}();\n\nvar getTarget = function getTarget() {\n var memo = {};\n return function memorize(target) {\n if (typeof memo[target] === 'undefined') {\n var styleTarget = document.querySelector(target); // Special case to return head of iframe instead of iframe itself\n\n if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) {\n try {\n // This will throw an exception if access to iframe is blocked\n // due to cross-origin restrictions\n styleTarget = styleTarget.contentDocument.head;\n } catch (e) {\n // istanbul ignore next\n styleTarget = null;\n }\n }\n\n memo[target] = styleTarget;\n }\n\n return memo[target];\n };\n}();\n\nfunction listToStyles(list, options) {\n var styles = [];\n var newStyles = {};\n\n for (var i = 0; i < list.length; i++) {\n var item = list[i];\n var id = options.base ? item[0] + options.base : item[0];\n var css = item[1];\n var media = item[2];\n var sourceMap = item[3];\n var part = {\n css: css,\n media: media,\n sourceMap: sourceMap\n };\n\n if (!newStyles[id]) {\n styles.push(newStyles[id] = {\n id: id,\n parts: [part]\n });\n } else {\n newStyles[id].parts.push(part);\n }\n }\n\n return styles;\n}\n\nfunction addStylesToDom(styles, options) {\n for (var i = 0; i < styles.length; i++) {\n var item = styles[i];\n var domStyle = stylesInDom[item.id];\n var j = 0;\n\n if (domStyle) {\n domStyle.refs++;\n\n for (; j < domStyle.parts.length; j++) {\n domStyle.parts[j](item.parts[j]);\n }\n\n for (; j < item.parts.length; j++) {\n domStyle.parts.push(addStyle(item.parts[j], options));\n }\n } else {\n var parts = [];\n\n for (; j < item.parts.length; j++) {\n parts.push(addStyle(item.parts[j], options));\n }\n\n stylesInDom[item.id] = {\n id: item.id,\n refs: 1,\n parts: parts\n };\n }\n }\n}\n\nfunction insertStyleElement(options) {\n var style = document.createElement('style');\n\n if (typeof options.attributes.nonce === 'undefined') {\n var nonce = typeof __webpack_nonce__ !== 'undefined' ? __webpack_nonce__ : null;\n\n if (nonce) {\n options.attributes.nonce = nonce;\n }\n }\n\n Object.keys(options.attributes).forEach(function (key) {\n style.setAttribute(key, options.attributes[key]);\n });\n\n if (typeof options.insert === 'function') {\n options.insert(style);\n } else {\n var target = getTarget(options.insert || 'head');\n\n if (!target) {\n throw new Error(\"Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.\");\n }\n\n target.appendChild(style);\n }\n\n return style;\n}\n\nfunction removeStyleElement(style) {\n // istanbul ignore if\n if (style.parentNode === null) {\n return false;\n }\n\n style.parentNode.removeChild(style);\n}\n/* istanbul ignore next */\n\n\nvar replaceText = function replaceText() {\n var textStore = [];\n return function replace(index, replacement) {\n textStore[index] = replacement;\n return textStore.filter(Boolean).join('\\n');\n };\n}();\n\nfunction applyToSingletonTag(style, index, remove, obj) {\n var css = remove ? '' : obj.css; // For old IE\n\n /* istanbul ignore if */\n\n if (style.styleSheet) {\n style.styleSheet.cssText = replaceText(index, css);\n } else {\n var cssNode = document.createTextNode(css);\n var childNodes = style.childNodes;\n\n if (childNodes[index]) {\n style.removeChild(childNodes[index]);\n }\n\n if (childNodes.length) {\n style.insertBefore(cssNode, childNodes[index]);\n } else {\n style.appendChild(cssNode);\n }\n }\n}\n\nfunction applyToTag(style, options, obj) {\n var css = obj.css;\n var media = obj.media;\n var sourceMap = obj.sourceMap;\n\n if (media) {\n style.setAttribute('media', media);\n }\n\n if (sourceMap && btoa) {\n css += \"\\n/*# sourceMappingURL=data:application/json;base64,\".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), \" */\");\n } // For old IE\n\n /* istanbul ignore if */\n\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n while (style.firstChild) {\n style.removeChild(style.firstChild);\n }\n\n style.appendChild(document.createTextNode(css));\n }\n}\n\nvar singleton = null;\nvar singletonCounter = 0;\n\nfunction addStyle(obj, options) {\n var style;\n var update;\n var remove;\n\n if (options.singleton) {\n var styleIndex = singletonCounter++;\n style = singleton || (singleton = insertStyleElement(options));\n update = applyToSingletonTag.bind(null, style, styleIndex, false);\n remove = applyToSingletonTag.bind(null, style, styleIndex, true);\n } else {\n style = insertStyleElement(options);\n update = applyToTag.bind(null, style, options);\n\n remove = function remove() {\n removeStyleElement(style);\n };\n }\n\n update(obj);\n return function updateStyle(newObj) {\n if (newObj) {\n if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap) {\n return;\n }\n\n update(obj = newObj);\n } else {\n remove();\n }\n };\n}\n\nmodule.exports = function (list, options) {\n options = options || {};\n options.attributes = typeof options.attributes === 'object' ? options.attributes : {}; // Force single-tag solution on IE6-9, which has a hard limit on the # of