From 8466e50cc31e684c30d819752128cda375681b6a Mon Sep 17 00:00:00 2001 From: Thomas Miceli <27960254+thomiceli@users.noreply.github.com> Date: Mon, 18 Dec 2023 01:35:44 +0100 Subject: [PATCH] Add GitLab OAuth provider (#174) --- config.yml | 8 +- docs/administration/oauth-providers.md | 13 +++ docs/configuration/cheat-sheet.md | 49 ++++++----- internal/config/config.go | 6 +- internal/db/user.go | 25 +++--- internal/i18n/locales/en-US.yml | 3 + internal/web/auth.go | 115 +++++++++++++------------ internal/web/server.go | 1 + templates/pages/auth_form.html | 7 +- templates/pages/settings.html | 17 +++- 10 files changed, 150 insertions(+), 94 deletions(-) diff --git a/config.yml b/config.yml index f48d79e..a22a895 100644 --- a/config.yml +++ b/config.yml @@ -55,12 +55,18 @@ ssh.keygen-executable: ssh-keygen # OAuth2 configuration -# The callback/redirect URL must be http://opengist.domain/oauth//callback +# The callback/redirect URL must be http://opengist.domain/oauth//callback # To create a new OAuth2 application using GitHub : https://github.com/settings/applications/new github.client-key: github.secret: +# To create a new OAuth2 application using Gitlab : https://gitlab.com/-/user_settings/applications +gitlab.client-key: +gitlab.secret: +# URL of the Gitlab instance. Default: https://gitlab.com/ +gitlab.url: https://gitlab.com/ + # To create a new OAuth2 application using Gitea : https://gitea.domain/user/settings/applications gitea.client-key: gitea.secret: diff --git a/docs/administration/oauth-providers.md b/docs/administration/oauth-providers.md index 9b1e73e..22735d4 100644 --- a/docs/administration/oauth-providers.md +++ b/docs/administration/oauth-providers.md @@ -13,6 +13,19 @@ Opengist can be configured to use OAuth to authenticate users, with GitHub, Gite ``` +## GitLab + +* Add a new OAuth app in Application settings from the [GitLab instance](https://gitlab.com/-/user_settings/applications) +* Set 'Redirect URI' to `http://opengist.domain/oauth/gitlab/callback` +* Copy the 'Client ID' and 'Client Secret' and add them to the [configuration](/docs/configuration/cheat-sheet.md) : + ```yaml + gitlab.client-key: + gitlab.secret: + # URL of the Gitlab instance. Default: https://gitlab.com/ + gitlab.url: https://gitlab.com/ + ``` + + ## Gitea * Add a new OAuth app in Application settings from the [Gitea instance](https://gitea.com/user/settings/applications) diff --git a/docs/configuration/cheat-sheet.md b/docs/configuration/cheat-sheet.md index 5400570..9b9510d 100644 --- a/docs/configuration/cheat-sheet.md +++ b/docs/configuration/cheat-sheet.md @@ -1,25 +1,28 @@ # Configuration Cheat Sheet -| YAML Config Key | Environment Variable | Default value | Description | -|-----------------------|--------------------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. | -| external-url | OG_EXTERNAL_URL | none | Public URL for the Git HTTP/SSH connection. If not set, uses the URL from the request. | -| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. | -| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. | -| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) | -| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. | -| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. | -| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) | -| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) | -| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. | -| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. | -| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. | -| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. | -| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. | -| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. | -| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. | -| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. | -| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. | -| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. | -| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. | -| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. | +| YAML Config Key | Environment Variable | Default value | Description | +|-----------------------|--------------------------|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. | +| external-url | OG_EXTERNAL_URL | none | Public URL for the Git HTTP/SSH connection. If not set, uses the URL from the request. | +| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. | +| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. | +| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) | +| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. | +| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. | +| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) | +| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) | +| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. | +| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. | +| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. | +| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. | +| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. | +| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. | +| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. | +| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. | +| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. | +| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. | +| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. | +| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. | +| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. | +| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. | +| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. | diff --git a/internal/config/config.go b/internal/config/config.go index 1eaefb8..c42e447 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -42,6 +42,10 @@ type config struct { GithubClientKey string `yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY"` GithubSecret string `yaml:"github.secret" env:"OG_GITHUB_SECRET"` + GitlabClientKey string `yaml:"gitlab.client-key" env:"OG_GITLAB_CLIENT_KEY"` + GitlabSecret string `yaml:"gitlab.secret" env:"OG_GITLAB_SECRET"` + GitlabUrl string `yaml:"gitlab.url" env:"OG_GITLAB_URL"` + GiteaClientKey string `yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY"` GiteaSecret string `yaml:"gitea.secret" env:"OG_GITEA_SECRET"` GiteaUrl string `yaml:"gitea.url" env:"OG_GITEA_URL"` @@ -69,7 +73,7 @@ func configWithDefaults() (*config, error) { c.SshPort = "2222" c.SshKeygen = "ssh-keygen" - c.GiteaUrl = "http://gitea.com" + c.GiteaUrl = "https://gitea.com" return c, nil } diff --git a/internal/db/user.go b/internal/db/user.go index 341b240..7641a90 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -14,6 +14,7 @@ type User struct { MD5Hash string // for gravatar, if no Email is specified, the value is random AvatarURL string GithubID string + GitlabID string GiteaID string OIDCID string `gorm:"column:oidc_id"` @@ -128,6 +129,8 @@ func GetUserByProvider(id string, provider string) (*User, error) { switch provider { case "github": err = db.Where("github_id = ?", id).First(&user).Error + case "gitlab": + err = db.Where("gitlab_id = ?", id).First(&user).Error case "gitea": err = db.Where("gitea_id = ?", id).First(&user).Error case "openid-connect": @@ -166,20 +169,16 @@ func (user *User) HasLiked(gist *Gist) (bool, error) { } func (user *User) DeleteProviderID(provider string) error { - switch provider { - case "github": + providerIDFields := map[string]string{ + "github": "github_id", + "gitlab": "gitlab_id", + "gitea": "gitea_id", + "openid-connect": "oidc_id", + } + + if providerIDField, ok := providerIDFields[provider]; ok { return db.Model(&user). - Update("github_id", nil). - Update("avatar_url", nil). - Error - case "gitea": - return db.Model(&user). - Update("gitea_id", nil). - Update("avatar_url", nil). - Error - case "openid-connect": - return db.Model(&user). - Update("oidc_id", nil). + Update(providerIDField, nil). Update("avatar_url", nil). Error } diff --git a/internal/i18n/locales/en-US.yml b/internal/i18n/locales/en-US.yml index 6551238..cfdc0ee 100644 --- a/internal/i18n/locales/en-US.yml +++ b/internal/i18n/locales/en-US.yml @@ -92,8 +92,10 @@ settings.email-help: Used for commits and Gravatar settings.email-set: Set email settings.link-accounts: Link accounts settings.link-github-account: Link GitHub account +settings.link-gitlab-account: Link Gitlab account settings.link-gitea-account: Link Gitea account settings.unlink-github-account: Unlink GitHub account +settings.unlink-gitlab-account: Unlink Gitlab account settings.unlink-gitea-account: Unlink Gitea account settings.delete-account: Delete account settings.delete-account-confirm: Are you sure you want to delete your account ? @@ -121,6 +123,7 @@ auth.password: Password auth.register-instead: Register instead auth.login-instead: Login instead auth.github-oauth: Continue with GitHub account +auth.gitlab-oauth: Continue with Gitlab account auth.gitea-oauth: Continue with Gitea account error: Error diff --git a/internal/web/auth.go b/internal/web/auth.go index e1f7fb2..ece9067 100644 --- a/internal/web/auth.go +++ b/internal/web/auth.go @@ -16,6 +16,7 @@ import ( "github.com/markbates/goth/gothic" "github.com/markbates/goth/providers/gitea" "github.com/markbates/goth/providers/github" + "github.com/markbates/goth/providers/gitlab" "github.com/markbates/goth/providers/openidConnect" "github.com/rs/zerolog/log" "github.com/thomiceli/opengist/internal/config" @@ -25,6 +26,13 @@ import ( "gorm.io/gorm" ) +const ( + GitHubProvider = "github" + GitLabProvider = "gitlab" + GiteaProvider = "gitea" + OpenIDConnect = "openid-connect" +) + var title = cases.Title(language.English) func register(ctx echo.Context) error { @@ -146,17 +154,7 @@ func oauthCallback(ctx echo.Context) error { currUser := getUserLogged(ctx) if currUser != nil { // if user is logged in, link account to user and update its avatar URL - switch user.Provider { - case "github": - currUser.GithubID = user.UserID - currUser.AvatarURL = getAvatarUrlFromProvider("github", user.UserID) - case "gitea": - currUser.GiteaID = user.UserID - currUser.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName) - case "openid-connect": - currUser.OIDCID = user.UserID - currUser.AvatarURL = user.AvatarURL - } + updateUserProviderInfo(currUser, user.Provider, user) if err = currUser.Update(); err != nil { return errorRes(500, "Cannot update user "+title.String(user.Provider)+" id", err) @@ -184,17 +182,7 @@ func oauthCallback(ctx echo.Context) error { } // set provider id and avatar URL - switch user.Provider { - case "github": - userDB.GithubID = user.UserID - userDB.AvatarURL = getAvatarUrlFromProvider("github", user.UserID) - case "gitea": - userDB.GiteaID = user.UserID - userDB.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName) - case "openid-connect": - userDB.OIDCID = user.UserID - userDB.AvatarURL = user.AvatarURL - } + updateUserProviderInfo(userDB, user.Provider, user) if err = userDB.Create(); err != nil { if db.IsUniqueConstraintViolation(err) { @@ -213,11 +201,13 @@ func oauthCallback(ctx echo.Context) error { var resp *http.Response switch user.Provider { - case "github": + case GitHubProvider: resp, err = http.Get("https://github.com/" + user.NickName + ".keys") - case "gitea": + case GitLabProvider: + resp, err = http.Get(urlJoin(config.C.GitlabUrl, user.NickName+".keys")) + case GiteaProvider: resp, err = http.Get(urlJoin(config.C.GiteaUrl, user.NickName+".keys")) - case "openid-connect": + case OpenIDConnect: err = errors.New("cannot get keys from OIDC provider") } @@ -273,7 +263,7 @@ func oauth(ctx echo.Context) error { } switch provider { - case "github": + case GitHubProvider: goth.UseProviders( github.New( config.C.GithubClientKey, @@ -282,7 +272,19 @@ func oauth(ctx echo.Context) error { ), ) - case "gitea": + case GitLabProvider: + goth.UseProviders( + gitlab.NewCustomisedURL( + config.C.GitlabClientKey, + config.C.GitlabSecret, + urlJoin(opengistUrl, "/oauth/gitlab/callback"), + urlJoin(config.C.GitlabUrl, "/oauth/authorize"), + urlJoin(config.C.GitlabUrl, "/oauth/token"), + urlJoin(config.C.GitlabUrl, "/api/v4/user"), + ), + ) + + case GiteaProvider: goth.UseProviders( gitea.NewCustomisedURL( config.C.GiteaClientKey, @@ -293,7 +295,7 @@ func oauth(ctx echo.Context) error { urlJoin(config.C.GiteaUrl, "/api/v1/user"), ), ) - case "openid-connect": + case OpenIDConnect: oidcProvider, err := openidConnect.New( config.C.OIDCClientKey, config.C.OIDCSecret, @@ -313,31 +315,21 @@ func oauth(ctx echo.Context) error { currUser := getUserLogged(ctx) if currUser != nil { - isDelete := false - var err error - switch provider { - case "github": - if currUser.GithubID != "" { - isDelete = true - err = currUser.DeleteProviderID(provider) - } - case "gitea": - if currUser.GiteaID != "" { - isDelete = true - err = currUser.DeleteProviderID(provider) - } - case "openid-connect": - if currUser.OIDCID != "" { - isDelete = true - err = currUser.DeleteProviderID(provider) - } + // Map each provider to a function that checks the relevant ID in currUser + providerIDCheckMap := map[string]func() bool{ + GitHubProvider: func() bool { return currUser.GithubID != "" }, + GitLabProvider: func() bool { return currUser.GitlabID != "" }, + GiteaProvider: func() bool { return currUser.GiteaID != "" }, + OpenIDConnect: func() bool { return currUser.OIDCID != "" }, } - if err != nil { - return errorRes(500, "Cannot unlink account from "+title.String(provider), err) - } + // Check if the provider is valid and if the user has a linked ID + // Means that the user wants to unlink the account + if checkFunc, exists := providerIDCheckMap[provider]; exists && checkFunc() { + if err := currUser.DeleteProviderID(provider); err != nil { + return errorRes(500, "Cannot unlink account from "+title.String(provider), err) + } - if isDelete { addFlash(ctx, "Account unlinked from "+title.String(provider), "success") return redirect(ctx, "/settings") } @@ -345,7 +337,7 @@ func oauth(ctx echo.Context) error { ctxValue := context.WithValue(ctx.Request().Context(), gothic.ProviderParamKey, provider) ctx.SetRequest(ctx.Request().WithContext(ctxValue)) - if provider != "github" && provider != "gitea" && provider != "openid-connect" { + if provider != GitHubProvider && provider != GitLabProvider && provider != GiteaProvider && provider != OpenIDConnect { return errorRes(400, "Unsupported provider", nil) } @@ -368,11 +360,28 @@ func urlJoin(base string, elem ...string) string { return joined } +func updateUserProviderInfo(userDB *db.User, provider string, user goth.User) { + userDB.AvatarURL = getAvatarUrlFromProvider(provider, user.UserID) + switch provider { + case GitHubProvider: + userDB.GithubID = user.UserID + case GitLabProvider: + userDB.GitlabID = user.UserID + case GiteaProvider: + userDB.GiteaID = user.UserID + case OpenIDConnect: + userDB.OIDCID = user.UserID + userDB.AvatarURL = user.AvatarURL + } +} + func getAvatarUrlFromProvider(provider string, identifier string) string { switch provider { - case "github": + case GitHubProvider: return "https://avatars.githubusercontent.com/u/" + identifier + "?v=4" - case "gitea": + case GitLabProvider: + return urlJoin(config.C.GitlabUrl, "/uploads/-/system/user/avatar/", identifier, "/avatar.png") + "?width=400" + case GiteaProvider: resp, err := http.Get(urlJoin(config.C.GiteaUrl, "/api/v1/users/", identifier)) if err != nil { log.Error().Err(err).Msg("Cannot get user from Gitea") diff --git a/internal/web/server.go b/internal/web/server.go index 0c563c1..880f7ac 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -312,6 +312,7 @@ func dataInit(next echo.HandlerFunc) echo.HandlerFunc { setData(ctx, "c", config.C) setData(ctx, "githubOauth", config.C.GithubClientKey != "" && config.C.GithubSecret != "") + setData(ctx, "gitlabOauth", config.C.GitlabClientKey != "" && config.C.GitlabSecret != "") setData(ctx, "giteaOauth", config.C.GiteaClientKey != "" && config.C.GiteaSecret != "") setData(ctx, "oidcOauth", config.C.OIDCClientKey != "" && config.C.OIDCSecret != "" && config.C.OIDCDiscoveryUrl != "") diff --git a/templates/pages/auth_form.html b/templates/pages/auth_form.html index 63695ca..61162c2 100644 --- a/templates/pages/auth_form.html +++ b/templates/pages/auth_form.html @@ -51,7 +51,7 @@ {{ .csrfHtml }} {{ end }} - {{ if or .githubOauth .giteaOauth .oidcOauth }} + {{ if or .githubOauth .gitlabOauth .giteaOauth .oidcOauth }} {{ if not .disableForm }}