diff options
Diffstat (limited to '')
-rw-r--r-- | routers/web/admin/auths.go | 465 |
1 files changed, 465 insertions, 0 deletions
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go new file mode 100644 index 0000000..799b7e8 --- /dev/null +++ b/routers/web/admin/auths.go @@ -0,0 +1,465 @@ +// 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/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "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" + + "xorm.io/xorm/convert" +) + +const ( + tplAuths base.TplName = "admin/auth/list" + tplAuthNew base.TplName = "admin/auth/new" + tplAuthEdit base.TplName = "admin/auth/edit" +) + +var ( + separatorAntiPattern = regexp.MustCompile(`[^\w-\.]`) + langCodePattern = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`) +) + +// Authentications show authentication config page +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, + DefaultDomainName: form.DefaultDomainName, + 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, + SkipLocalTwoFA: form.SkipLocalTwoFA, + } +} + +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, + SkipLocalTwoFA: form.SkipLocalTwoFA, + } +} + +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, + SkipLocalTwoFA: form.SkipLocalTwoFA, + 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 convert.Conversion + 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, + SkipLocalTwoFA: form.SkipLocalTwoFA, + } + 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.Error(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, + 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.ParamsInt64(":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.ParamsInt64(":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 convert.Conversion + 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.Error(http.StatusBadRequest) + return + } + + source.Name = form.Name + source.IsActive = form.IsActive + source.IsSyncEnabled = form.IsSyncEnabled + source.Cfg = config + 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.ParamsInt64(":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.Params(":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") +} |