mirror of
https://github.com/go-gitea/gitea.git
synced 2025-05-05 15:32:53 +00:00
Fix #880 Design: 1. A global setting `security.TWO_FACTOR_AUTH`. * To support org-level config, we need to introduce a better "owner setting" system first (in the future) 2. A user without 2FA can login and may explore, but can NOT read or write to any repositories via API/web. 3. Keep things as simple as possible. * This option only aggressively suggest users to enable their 2FA at the moment, it does NOT guarantee that users must have 2FA before all other operations, it should be good enough for real world use cases. * Some details and tests could be improved in the future since this change only adds a check and seems won't affect too much. --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
461 lines
15 KiB
Go
461 lines
15 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package admin
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/models/db"
|
|
"code.gitea.io/gitea/modules/auth/pam"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/templates"
|
|
"code.gitea.io/gitea/modules/util"
|
|
"code.gitea.io/gitea/modules/web"
|
|
auth_service "code.gitea.io/gitea/services/auth"
|
|
"code.gitea.io/gitea/services/auth/source/ldap"
|
|
"code.gitea.io/gitea/services/auth/source/oauth2"
|
|
pam_service "code.gitea.io/gitea/services/auth/source/pam"
|
|
"code.gitea.io/gitea/services/auth/source/smtp"
|
|
"code.gitea.io/gitea/services/auth/source/sspi"
|
|
"code.gitea.io/gitea/services/context"
|
|
"code.gitea.io/gitea/services/forms"
|
|
)
|
|
|
|
const (
|
|
tplAuths templates.TplName = "admin/auth/list"
|
|
tplAuthNew templates.TplName = "admin/auth/new"
|
|
tplAuthEdit templates.TplName = "admin/auth/edit"
|
|
)
|
|
|
|
var (
|
|
separatorAntiPattern = regexp.MustCompile(`[^\w-\.]`)
|
|
langCodePattern = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`)
|
|
)
|
|
|
|
// Authentications show authentication config page
|
|
func Authentications(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("admin.authentication")
|
|
ctx.Data["PageIsAdminAuthentications"] = true
|
|
|
|
var err error
|
|
ctx.Data["Sources"], ctx.Data["Total"], err = db.FindAndCount[auth.Source](ctx, auth.FindSourcesOptions{})
|
|
if err != nil {
|
|
ctx.ServerError("auth.Sources", err)
|
|
return
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, tplAuths)
|
|
}
|
|
|
|
type dropdownItem struct {
|
|
Name string
|
|
Type any
|
|
}
|
|
|
|
var (
|
|
authSources = func() []dropdownItem {
|
|
items := []dropdownItem{
|
|
{auth.LDAP.String(), auth.LDAP},
|
|
{auth.DLDAP.String(), auth.DLDAP},
|
|
{auth.SMTP.String(), auth.SMTP},
|
|
{auth.OAuth2.String(), auth.OAuth2},
|
|
{auth.SSPI.String(), auth.SSPI},
|
|
}
|
|
if pam.Supported {
|
|
items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM})
|
|
}
|
|
return items
|
|
}()
|
|
|
|
securityProtocols = []dropdownItem{
|
|
{ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
|
|
{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
|
|
{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
|
|
}
|
|
)
|
|
|
|
// NewAuthSource render adding a new auth source page
|
|
func NewAuthSource(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
|
|
ctx.Data["PageIsAdminAuthentications"] = true
|
|
|
|
ctx.Data["type"] = auth.LDAP.Int()
|
|
ctx.Data["CurrentTypeName"] = auth.Names[auth.LDAP]
|
|
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
|
|
ctx.Data["smtp_auth"] = "PLAIN"
|
|
ctx.Data["is_active"] = true
|
|
ctx.Data["is_sync_enabled"] = true
|
|
ctx.Data["AuthSources"] = authSources
|
|
ctx.Data["SecurityProtocols"] = securityProtocols
|
|
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
|
oauth2providers := oauth2.GetSupportedOAuth2Providers()
|
|
ctx.Data["OAuth2Providers"] = oauth2providers
|
|
|
|
ctx.Data["SSPIAutoCreateUsers"] = true
|
|
ctx.Data["SSPIAutoActivateUsers"] = true
|
|
ctx.Data["SSPIStripDomainNames"] = true
|
|
ctx.Data["SSPISeparatorReplacement"] = "_"
|
|
ctx.Data["SSPIDefaultLanguage"] = ""
|
|
|
|
// only the first as default
|
|
ctx.Data["oauth2_provider"] = oauth2providers[0].Name()
|
|
|
|
ctx.HTML(http.StatusOK, tplAuthNew)
|
|
}
|
|
|
|
func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
|
|
var pageSize uint32
|
|
if form.UsePagedSearch {
|
|
pageSize = uint32(form.SearchPageSize)
|
|
}
|
|
return &ldap.Source{
|
|
Name: form.Name,
|
|
Host: form.Host,
|
|
Port: form.Port,
|
|
SecurityProtocol: ldap.SecurityProtocol(form.SecurityProtocol),
|
|
SkipVerify: form.SkipVerify,
|
|
BindDN: form.BindDN,
|
|
UserDN: form.UserDN,
|
|
BindPassword: form.BindPassword,
|
|
UserBase: form.UserBase,
|
|
AttributeUsername: form.AttributeUsername,
|
|
AttributeName: form.AttributeName,
|
|
AttributeSurname: form.AttributeSurname,
|
|
AttributeMail: form.AttributeMail,
|
|
AttributesInBind: form.AttributesInBind,
|
|
AttributeSSHPublicKey: form.AttributeSSHPublicKey,
|
|
AttributeAvatar: form.AttributeAvatar,
|
|
SearchPageSize: pageSize,
|
|
Filter: form.Filter,
|
|
GroupsEnabled: form.GroupsEnabled,
|
|
GroupDN: form.GroupDN,
|
|
GroupFilter: form.GroupFilter,
|
|
GroupMemberUID: form.GroupMemberUID,
|
|
GroupTeamMap: form.GroupTeamMap,
|
|
GroupTeamMapRemoval: form.GroupTeamMapRemoval,
|
|
UserUID: form.UserUID,
|
|
AdminFilter: form.AdminFilter,
|
|
RestrictedFilter: form.RestrictedFilter,
|
|
AllowDeactivateAll: form.AllowDeactivateAll,
|
|
Enabled: true,
|
|
}
|
|
}
|
|
|
|
func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
|
|
return &smtp.Source{
|
|
Auth: form.SMTPAuth,
|
|
Host: form.SMTPHost,
|
|
Port: form.SMTPPort,
|
|
AllowedDomains: form.AllowedDomains,
|
|
ForceSMTPS: form.ForceSMTPS,
|
|
SkipVerify: form.SkipVerify,
|
|
HeloHostname: form.HeloHostname,
|
|
DisableHelo: form.DisableHelo,
|
|
}
|
|
}
|
|
|
|
func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
|
|
var customURLMapping *oauth2.CustomURLMapping
|
|
if form.Oauth2UseCustomURL {
|
|
customURLMapping = &oauth2.CustomURLMapping{
|
|
TokenURL: form.Oauth2TokenURL,
|
|
AuthURL: form.Oauth2AuthURL,
|
|
ProfileURL: form.Oauth2ProfileURL,
|
|
EmailURL: form.Oauth2EmailURL,
|
|
Tenant: form.Oauth2Tenant,
|
|
}
|
|
} else {
|
|
customURLMapping = nil
|
|
}
|
|
var scopes []string
|
|
for _, s := range strings.Split(form.Oauth2Scopes, ",") {
|
|
s = strings.TrimSpace(s)
|
|
if s != "" {
|
|
scopes = append(scopes, s)
|
|
}
|
|
}
|
|
|
|
return &oauth2.Source{
|
|
Provider: form.Oauth2Provider,
|
|
ClientID: form.Oauth2Key,
|
|
ClientSecret: form.Oauth2Secret,
|
|
OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
|
|
CustomURLMapping: customURLMapping,
|
|
IconURL: form.Oauth2IconURL,
|
|
Scopes: scopes,
|
|
RequiredClaimName: form.Oauth2RequiredClaimName,
|
|
RequiredClaimValue: form.Oauth2RequiredClaimValue,
|
|
GroupClaimName: form.Oauth2GroupClaimName,
|
|
RestrictedGroup: form.Oauth2RestrictedGroup,
|
|
AdminGroup: form.Oauth2AdminGroup,
|
|
GroupTeamMap: form.Oauth2GroupTeamMap,
|
|
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
|
|
}
|
|
}
|
|
|
|
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
|
|
if util.IsEmptyString(form.SSPISeparatorReplacement) {
|
|
ctx.Data["Err_SSPISeparatorReplacement"] = true
|
|
return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error"))
|
|
}
|
|
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
|
|
ctx.Data["Err_SSPISeparatorReplacement"] = true
|
|
return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error"))
|
|
}
|
|
|
|
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
|
|
ctx.Data["Err_SSPIDefaultLanguage"] = true
|
|
return nil, errors.New(ctx.Locale.TrString("form.lang_select_error"))
|
|
}
|
|
|
|
return &sspi.Source{
|
|
AutoCreateUsers: form.SSPIAutoCreateUsers,
|
|
AutoActivateUsers: form.SSPIAutoActivateUsers,
|
|
StripDomainNames: form.SSPIStripDomainNames,
|
|
SeparatorReplacement: form.SSPISeparatorReplacement,
|
|
DefaultLanguage: form.SSPIDefaultLanguage,
|
|
}, nil
|
|
}
|
|
|
|
// NewAuthSourcePost response for adding an auth source
|
|
func NewAuthSourcePost(ctx *context.Context) {
|
|
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
|
|
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
|
|
ctx.Data["PageIsAdminAuthentications"] = true
|
|
|
|
ctx.Data["CurrentTypeName"] = auth.Type(form.Type).String()
|
|
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
|
|
ctx.Data["AuthSources"] = authSources
|
|
ctx.Data["SecurityProtocols"] = securityProtocols
|
|
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
|
oauth2providers := oauth2.GetSupportedOAuth2Providers()
|
|
ctx.Data["OAuth2Providers"] = oauth2providers
|
|
|
|
ctx.Data["SSPIAutoCreateUsers"] = true
|
|
ctx.Data["SSPIAutoActivateUsers"] = true
|
|
ctx.Data["SSPIStripDomainNames"] = true
|
|
ctx.Data["SSPISeparatorReplacement"] = "_"
|
|
ctx.Data["SSPIDefaultLanguage"] = ""
|
|
|
|
hasTLS := false
|
|
var config auth.Config
|
|
switch auth.Type(form.Type) {
|
|
case auth.LDAP, auth.DLDAP:
|
|
config = parseLDAPConfig(form)
|
|
hasTLS = ldap.SecurityProtocol(form.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
|
|
case auth.SMTP:
|
|
config = parseSMTPConfig(form)
|
|
hasTLS = true
|
|
case auth.PAM:
|
|
config = &pam_service.Source{
|
|
ServiceName: form.PAMServiceName,
|
|
EmailDomain: form.PAMEmailDomain,
|
|
}
|
|
case auth.OAuth2:
|
|
config = parseOAuth2Config(form)
|
|
oauth2Config := config.(*oauth2.Source)
|
|
if oauth2Config.Provider == "openidConnect" {
|
|
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
|
|
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
|
|
ctx.Data["Err_DiscoveryURL"] = true
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form)
|
|
return
|
|
}
|
|
}
|
|
case auth.SSPI:
|
|
var err error
|
|
config, err = parseSSPIConfig(ctx, form)
|
|
if err != nil {
|
|
ctx.RenderWithErr(err.Error(), tplAuthNew, form)
|
|
return
|
|
}
|
|
existing, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{LoginType: auth.SSPI})
|
|
if err != nil || len(existing) > 0 {
|
|
ctx.Data["Err_Type"] = true
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
|
|
return
|
|
}
|
|
default:
|
|
ctx.HTTPError(http.StatusBadRequest)
|
|
return
|
|
}
|
|
ctx.Data["HasTLS"] = hasTLS
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tplAuthNew)
|
|
return
|
|
}
|
|
|
|
if err := auth.CreateSource(ctx, &auth.Source{
|
|
Type: auth.Type(form.Type),
|
|
Name: form.Name,
|
|
IsActive: form.IsActive,
|
|
IsSyncEnabled: form.IsSyncEnabled,
|
|
TwoFactorPolicy: form.TwoFactorPolicy,
|
|
Cfg: config,
|
|
}); err != nil {
|
|
if auth.IsErrSourceAlreadyExist(err) {
|
|
ctx.Data["Err_Name"] = true
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form)
|
|
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
|
|
ctx.Data["Err_DiscoveryURL"] = true
|
|
unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap()
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form)
|
|
} else {
|
|
ctx.ServerError("auth.CreateSource", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
log.Trace("Authentication created by admin(%s): %s", ctx.Doer.Name, form.Name)
|
|
|
|
ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/auths")
|
|
}
|
|
|
|
// EditAuthSource render editing auth source page
|
|
func EditAuthSource(ctx *context.Context) {
|
|
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
|
|
ctx.Data["PageIsAdminAuthentications"] = true
|
|
|
|
ctx.Data["SecurityProtocols"] = securityProtocols
|
|
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
|
oauth2providers := oauth2.GetSupportedOAuth2Providers()
|
|
ctx.Data["OAuth2Providers"] = oauth2providers
|
|
|
|
source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid"))
|
|
if err != nil {
|
|
ctx.ServerError("auth.GetSourceByID", err)
|
|
return
|
|
}
|
|
ctx.Data["Source"] = source
|
|
ctx.Data["HasTLS"] = source.HasTLS()
|
|
|
|
if source.IsOAuth2() {
|
|
type Named interface {
|
|
Name() string
|
|
}
|
|
|
|
for _, provider := range oauth2providers {
|
|
if provider.Name() == source.Cfg.(Named).Name() {
|
|
ctx.Data["CurrentOAuth2Provider"] = provider
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, tplAuthEdit)
|
|
}
|
|
|
|
// EditAuthSourcePost response for editing auth source
|
|
func EditAuthSourcePost(ctx *context.Context) {
|
|
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
|
|
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
|
|
ctx.Data["PageIsAdminAuthentications"] = true
|
|
|
|
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
|
oauth2providers := oauth2.GetSupportedOAuth2Providers()
|
|
ctx.Data["OAuth2Providers"] = oauth2providers
|
|
|
|
source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid"))
|
|
if err != nil {
|
|
ctx.ServerError("auth.GetSourceByID", err)
|
|
return
|
|
}
|
|
ctx.Data["Source"] = source
|
|
ctx.Data["HasTLS"] = source.HasTLS()
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tplAuthEdit)
|
|
return
|
|
}
|
|
|
|
var config auth.Config
|
|
switch auth.Type(form.Type) {
|
|
case auth.LDAP, auth.DLDAP:
|
|
config = parseLDAPConfig(form)
|
|
case auth.SMTP:
|
|
config = parseSMTPConfig(form)
|
|
case auth.PAM:
|
|
config = &pam_service.Source{
|
|
ServiceName: form.PAMServiceName,
|
|
EmailDomain: form.PAMEmailDomain,
|
|
}
|
|
case auth.OAuth2:
|
|
config = parseOAuth2Config(form)
|
|
oauth2Config := config.(*oauth2.Source)
|
|
if oauth2Config.Provider == "openidConnect" {
|
|
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
|
|
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
|
|
ctx.Data["Err_DiscoveryURL"] = true
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form)
|
|
return
|
|
}
|
|
}
|
|
case auth.SSPI:
|
|
config, err = parseSSPIConfig(ctx, form)
|
|
if err != nil {
|
|
ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
|
|
return
|
|
}
|
|
default:
|
|
ctx.HTTPError(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
source.Name = form.Name
|
|
source.IsActive = form.IsActive
|
|
source.IsSyncEnabled = form.IsSyncEnabled
|
|
source.Cfg = config
|
|
source.TwoFactorPolicy = form.TwoFactorPolicy
|
|
if err := auth.UpdateSource(ctx, source); err != nil {
|
|
if auth.IsErrSourceAlreadyExist(err) {
|
|
ctx.Data["Err_Name"] = true
|
|
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form)
|
|
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
|
|
ctx.Flash.Error(err.Error(), true)
|
|
ctx.Data["Err_DiscoveryURL"] = true
|
|
ctx.HTML(http.StatusOK, tplAuthEdit)
|
|
} else {
|
|
ctx.ServerError("UpdateSource", err)
|
|
}
|
|
return
|
|
}
|
|
log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID)
|
|
|
|
ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/auths/" + strconv.FormatInt(form.ID, 10))
|
|
}
|
|
|
|
// DeleteAuthSource response for deleting an auth source
|
|
func DeleteAuthSource(ctx *context.Context) {
|
|
source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid"))
|
|
if err != nil {
|
|
ctx.ServerError("auth.GetSourceByID", err)
|
|
return
|
|
}
|
|
|
|
if err = auth_service.DeleteSource(ctx, source); err != nil {
|
|
if auth.IsErrSourceInUse(err) {
|
|
ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
|
|
} else {
|
|
ctx.Flash.Error(fmt.Sprintf("auth_service.DeleteSource: %v", err))
|
|
}
|
|
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/auths/" + url.PathEscape(ctx.PathParam("authid")))
|
|
return
|
|
}
|
|
log.Trace("Authentication deleted by admin(%s): %d", ctx.Doer.Name, source.ID)
|
|
|
|
ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
|
|
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/auths")
|
|
}
|