code.gitea.io/gitea@v1.21.7/routers/web/admin/auths.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package admin
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  	"net/url"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"code.gitea.io/gitea/models/auth"
    16  	"code.gitea.io/gitea/modules/auth/pam"
    17  	"code.gitea.io/gitea/modules/base"
    18  	"code.gitea.io/gitea/modules/context"
    19  	"code.gitea.io/gitea/modules/log"
    20  	"code.gitea.io/gitea/modules/setting"
    21  	"code.gitea.io/gitea/modules/util"
    22  	"code.gitea.io/gitea/modules/web"
    23  	auth_service "code.gitea.io/gitea/services/auth"
    24  	"code.gitea.io/gitea/services/auth/source/ldap"
    25  	"code.gitea.io/gitea/services/auth/source/oauth2"
    26  	pam_service "code.gitea.io/gitea/services/auth/source/pam"
    27  	"code.gitea.io/gitea/services/auth/source/smtp"
    28  	"code.gitea.io/gitea/services/auth/source/sspi"
    29  	"code.gitea.io/gitea/services/forms"
    30  
    31  	"xorm.io/xorm/convert"
    32  )
    33  
    34  const (
    35  	tplAuths    base.TplName = "admin/auth/list"
    36  	tplAuthNew  base.TplName = "admin/auth/new"
    37  	tplAuthEdit base.TplName = "admin/auth/edit"
    38  )
    39  
    40  var (
    41  	separatorAntiPattern = regexp.MustCompile(`[^\w-\.]`)
    42  	langCodePattern      = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`)
    43  )
    44  
    45  // Authentications show authentication config page
    46  func Authentications(ctx *context.Context) {
    47  	ctx.Data["Title"] = ctx.Tr("admin.authentication")
    48  	ctx.Data["PageIsAdminAuthentications"] = true
    49  
    50  	var err error
    51  	ctx.Data["Sources"], err = auth.Sources()
    52  	if err != nil {
    53  		ctx.ServerError("auth.Sources", err)
    54  		return
    55  	}
    56  
    57  	ctx.Data["Total"] = auth.CountSources()
    58  	ctx.HTML(http.StatusOK, tplAuths)
    59  }
    60  
    61  type dropdownItem struct {
    62  	Name string
    63  	Type any
    64  }
    65  
    66  var (
    67  	authSources = func() []dropdownItem {
    68  		items := []dropdownItem{
    69  			{auth.LDAP.String(), auth.LDAP},
    70  			{auth.DLDAP.String(), auth.DLDAP},
    71  			{auth.SMTP.String(), auth.SMTP},
    72  			{auth.OAuth2.String(), auth.OAuth2},
    73  			{auth.SSPI.String(), auth.SSPI},
    74  		}
    75  		if pam.Supported {
    76  			items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM})
    77  		}
    78  		return items
    79  	}()
    80  
    81  	securityProtocols = []dropdownItem{
    82  		{ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
    83  		{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
    84  		{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
    85  	}
    86  )
    87  
    88  // NewAuthSource render adding a new auth source page
    89  func NewAuthSource(ctx *context.Context) {
    90  	ctx.Data["Title"] = ctx.Tr("admin.auths.new")
    91  	ctx.Data["PageIsAdminAuthentications"] = true
    92  
    93  	ctx.Data["type"] = auth.LDAP.Int()
    94  	ctx.Data["CurrentTypeName"] = auth.Names[auth.LDAP]
    95  	ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
    96  	ctx.Data["smtp_auth"] = "PLAIN"
    97  	ctx.Data["is_active"] = true
    98  	ctx.Data["is_sync_enabled"] = true
    99  	ctx.Data["AuthSources"] = authSources
   100  	ctx.Data["SecurityProtocols"] = securityProtocols
   101  	ctx.Data["SMTPAuths"] = smtp.Authenticators
   102  	oauth2providers := oauth2.GetOAuth2Providers()
   103  	ctx.Data["OAuth2Providers"] = oauth2providers
   104  
   105  	ctx.Data["SSPIAutoCreateUsers"] = true
   106  	ctx.Data["SSPIAutoActivateUsers"] = true
   107  	ctx.Data["SSPIStripDomainNames"] = true
   108  	ctx.Data["SSPISeparatorReplacement"] = "_"
   109  	ctx.Data["SSPIDefaultLanguage"] = ""
   110  
   111  	// only the first as default
   112  	ctx.Data["oauth2_provider"] = oauth2providers[0].Name()
   113  
   114  	ctx.HTML(http.StatusOK, tplAuthNew)
   115  }
   116  
   117  func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
   118  	var pageSize uint32
   119  	if form.UsePagedSearch {
   120  		pageSize = uint32(form.SearchPageSize)
   121  	}
   122  	return &ldap.Source{
   123  		Name:                  form.Name,
   124  		Host:                  form.Host,
   125  		Port:                  form.Port,
   126  		SecurityProtocol:      ldap.SecurityProtocol(form.SecurityProtocol),
   127  		SkipVerify:            form.SkipVerify,
   128  		BindDN:                form.BindDN,
   129  		UserDN:                form.UserDN,
   130  		BindPassword:          form.BindPassword,
   131  		UserBase:              form.UserBase,
   132  		AttributeUsername:     form.AttributeUsername,
   133  		AttributeName:         form.AttributeName,
   134  		AttributeSurname:      form.AttributeSurname,
   135  		AttributeMail:         form.AttributeMail,
   136  		AttributesInBind:      form.AttributesInBind,
   137  		AttributeSSHPublicKey: form.AttributeSSHPublicKey,
   138  		AttributeAvatar:       form.AttributeAvatar,
   139  		SearchPageSize:        pageSize,
   140  		Filter:                form.Filter,
   141  		GroupsEnabled:         form.GroupsEnabled,
   142  		GroupDN:               form.GroupDN,
   143  		GroupFilter:           form.GroupFilter,
   144  		GroupMemberUID:        form.GroupMemberUID,
   145  		GroupTeamMap:          form.GroupTeamMap,
   146  		GroupTeamMapRemoval:   form.GroupTeamMapRemoval,
   147  		UserUID:               form.UserUID,
   148  		AdminFilter:           form.AdminFilter,
   149  		RestrictedFilter:      form.RestrictedFilter,
   150  		AllowDeactivateAll:    form.AllowDeactivateAll,
   151  		Enabled:               true,
   152  		SkipLocalTwoFA:        form.SkipLocalTwoFA,
   153  	}
   154  }
   155  
   156  func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
   157  	return &smtp.Source{
   158  		Auth:           form.SMTPAuth,
   159  		Host:           form.SMTPHost,
   160  		Port:           form.SMTPPort,
   161  		AllowedDomains: form.AllowedDomains,
   162  		ForceSMTPS:     form.ForceSMTPS,
   163  		SkipVerify:     form.SkipVerify,
   164  		HeloHostname:   form.HeloHostname,
   165  		DisableHelo:    form.DisableHelo,
   166  		SkipLocalTwoFA: form.SkipLocalTwoFA,
   167  	}
   168  }
   169  
   170  func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
   171  	var customURLMapping *oauth2.CustomURLMapping
   172  	if form.Oauth2UseCustomURL {
   173  		customURLMapping = &oauth2.CustomURLMapping{
   174  			TokenURL:   form.Oauth2TokenURL,
   175  			AuthURL:    form.Oauth2AuthURL,
   176  			ProfileURL: form.Oauth2ProfileURL,
   177  			EmailURL:   form.Oauth2EmailURL,
   178  			Tenant:     form.Oauth2Tenant,
   179  		}
   180  	} else {
   181  		customURLMapping = nil
   182  	}
   183  	var scopes []string
   184  	for _, s := range strings.Split(form.Oauth2Scopes, ",") {
   185  		s = strings.TrimSpace(s)
   186  		if s != "" {
   187  			scopes = append(scopes, s)
   188  		}
   189  	}
   190  
   191  	return &oauth2.Source{
   192  		Provider:                      form.Oauth2Provider,
   193  		ClientID:                      form.Oauth2Key,
   194  		ClientSecret:                  form.Oauth2Secret,
   195  		OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
   196  		CustomURLMapping:              customURLMapping,
   197  		IconURL:                       form.Oauth2IconURL,
   198  		Scopes:                        scopes,
   199  		RequiredClaimName:             form.Oauth2RequiredClaimName,
   200  		RequiredClaimValue:            form.Oauth2RequiredClaimValue,
   201  		SkipLocalTwoFA:                form.SkipLocalTwoFA,
   202  		GroupClaimName:                form.Oauth2GroupClaimName,
   203  		RestrictedGroup:               form.Oauth2RestrictedGroup,
   204  		AdminGroup:                    form.Oauth2AdminGroup,
   205  		GroupTeamMap:                  form.Oauth2GroupTeamMap,
   206  		GroupTeamMapRemoval:           form.Oauth2GroupTeamMapRemoval,
   207  	}
   208  }
   209  
   210  func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
   211  	if util.IsEmptyString(form.SSPISeparatorReplacement) {
   212  		ctx.Data["Err_SSPISeparatorReplacement"] = true
   213  		return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
   214  	}
   215  	if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
   216  		ctx.Data["Err_SSPISeparatorReplacement"] = true
   217  		return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error"))
   218  	}
   219  
   220  	if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
   221  		ctx.Data["Err_SSPIDefaultLanguage"] = true
   222  		return nil, errors.New(ctx.Tr("form.lang_select_error"))
   223  	}
   224  
   225  	return &sspi.Source{
   226  		AutoCreateUsers:      form.SSPIAutoCreateUsers,
   227  		AutoActivateUsers:    form.SSPIAutoActivateUsers,
   228  		StripDomainNames:     form.SSPIStripDomainNames,
   229  		SeparatorReplacement: form.SSPISeparatorReplacement,
   230  		DefaultLanguage:      form.SSPIDefaultLanguage,
   231  	}, nil
   232  }
   233  
   234  // NewAuthSourcePost response for adding an auth source
   235  func NewAuthSourcePost(ctx *context.Context) {
   236  	form := *web.GetForm(ctx).(*forms.AuthenticationForm)
   237  	ctx.Data["Title"] = ctx.Tr("admin.auths.new")
   238  	ctx.Data["PageIsAdminAuthentications"] = true
   239  
   240  	ctx.Data["CurrentTypeName"] = auth.Type(form.Type).String()
   241  	ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
   242  	ctx.Data["AuthSources"] = authSources
   243  	ctx.Data["SecurityProtocols"] = securityProtocols
   244  	ctx.Data["SMTPAuths"] = smtp.Authenticators
   245  	oauth2providers := oauth2.GetOAuth2Providers()
   246  	ctx.Data["OAuth2Providers"] = oauth2providers
   247  
   248  	ctx.Data["SSPIAutoCreateUsers"] = true
   249  	ctx.Data["SSPIAutoActivateUsers"] = true
   250  	ctx.Data["SSPIStripDomainNames"] = true
   251  	ctx.Data["SSPISeparatorReplacement"] = "_"
   252  	ctx.Data["SSPIDefaultLanguage"] = ""
   253  
   254  	hasTLS := false
   255  	var config convert.Conversion
   256  	switch auth.Type(form.Type) {
   257  	case auth.LDAP, auth.DLDAP:
   258  		config = parseLDAPConfig(form)
   259  		hasTLS = ldap.SecurityProtocol(form.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
   260  	case auth.SMTP:
   261  		config = parseSMTPConfig(form)
   262  		hasTLS = true
   263  	case auth.PAM:
   264  		config = &pam_service.Source{
   265  			ServiceName:    form.PAMServiceName,
   266  			EmailDomain:    form.PAMEmailDomain,
   267  			SkipLocalTwoFA: form.SkipLocalTwoFA,
   268  		}
   269  	case auth.OAuth2:
   270  		config = parseOAuth2Config(form)
   271  		oauth2Config := config.(*oauth2.Source)
   272  		if oauth2Config.Provider == "openidConnect" {
   273  			discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
   274  			if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
   275  				ctx.Data["Err_DiscoveryURL"] = true
   276  				ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form)
   277  				return
   278  			}
   279  		}
   280  	case auth.SSPI:
   281  		var err error
   282  		config, err = parseSSPIConfig(ctx, form)
   283  		if err != nil {
   284  			ctx.RenderWithErr(err.Error(), tplAuthNew, form)
   285  			return
   286  		}
   287  		existing, err := auth.SourcesByType(auth.SSPI)
   288  		if err != nil || len(existing) > 0 {
   289  			ctx.Data["Err_Type"] = true
   290  			ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
   291  			return
   292  		}
   293  	default:
   294  		ctx.Error(http.StatusBadRequest)
   295  		return
   296  	}
   297  	ctx.Data["HasTLS"] = hasTLS
   298  
   299  	if ctx.HasError() {
   300  		ctx.HTML(http.StatusOK, tplAuthNew)
   301  		return
   302  	}
   303  
   304  	if err := auth.CreateSource(&auth.Source{
   305  		Type:          auth.Type(form.Type),
   306  		Name:          form.Name,
   307  		IsActive:      form.IsActive,
   308  		IsSyncEnabled: form.IsSyncEnabled,
   309  		Cfg:           config,
   310  	}); err != nil {
   311  		if auth.IsErrSourceAlreadyExist(err) {
   312  			ctx.Data["Err_Name"] = true
   313  			ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form)
   314  		} else if oauth2.IsErrOpenIDConnectInitialize(err) {
   315  			ctx.Data["Err_DiscoveryURL"] = true
   316  			unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap()
   317  			ctx.RenderWithErr(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form)
   318  		} else {
   319  			ctx.ServerError("auth.CreateSource", err)
   320  		}
   321  		return
   322  	}
   323  
   324  	log.Trace("Authentication created by admin(%s): %s", ctx.Doer.Name, form.Name)
   325  
   326  	ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
   327  	ctx.Redirect(setting.AppSubURL + "/admin/auths")
   328  }
   329  
   330  // EditAuthSource render editing auth source page
   331  func EditAuthSource(ctx *context.Context) {
   332  	ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
   333  	ctx.Data["PageIsAdminAuthentications"] = true
   334  
   335  	ctx.Data["SecurityProtocols"] = securityProtocols
   336  	ctx.Data["SMTPAuths"] = smtp.Authenticators
   337  	oauth2providers := oauth2.GetOAuth2Providers()
   338  	ctx.Data["OAuth2Providers"] = oauth2providers
   339  
   340  	source, err := auth.GetSourceByID(ctx.ParamsInt64(":authid"))
   341  	if err != nil {
   342  		ctx.ServerError("auth.GetSourceByID", err)
   343  		return
   344  	}
   345  	ctx.Data["Source"] = source
   346  	ctx.Data["HasTLS"] = source.HasTLS()
   347  
   348  	if source.IsOAuth2() {
   349  		type Named interface {
   350  			Name() string
   351  		}
   352  
   353  		for _, provider := range oauth2providers {
   354  			if provider.Name() == source.Cfg.(Named).Name() {
   355  				ctx.Data["CurrentOAuth2Provider"] = provider
   356  				break
   357  			}
   358  		}
   359  	}
   360  
   361  	ctx.HTML(http.StatusOK, tplAuthEdit)
   362  }
   363  
   364  // EditAuthSourcePost response for editing auth source
   365  func EditAuthSourcePost(ctx *context.Context) {
   366  	form := *web.GetForm(ctx).(*forms.AuthenticationForm)
   367  	ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
   368  	ctx.Data["PageIsAdminAuthentications"] = true
   369  
   370  	ctx.Data["SMTPAuths"] = smtp.Authenticators
   371  	oauth2providers := oauth2.GetOAuth2Providers()
   372  	ctx.Data["OAuth2Providers"] = oauth2providers
   373  
   374  	source, err := auth.GetSourceByID(ctx.ParamsInt64(":authid"))
   375  	if err != nil {
   376  		ctx.ServerError("auth.GetSourceByID", err)
   377  		return
   378  	}
   379  	ctx.Data["Source"] = source
   380  	ctx.Data["HasTLS"] = source.HasTLS()
   381  
   382  	if ctx.HasError() {
   383  		ctx.HTML(http.StatusOK, tplAuthEdit)
   384  		return
   385  	}
   386  
   387  	var config convert.Conversion
   388  	switch auth.Type(form.Type) {
   389  	case auth.LDAP, auth.DLDAP:
   390  		config = parseLDAPConfig(form)
   391  	case auth.SMTP:
   392  		config = parseSMTPConfig(form)
   393  	case auth.PAM:
   394  		config = &pam_service.Source{
   395  			ServiceName: form.PAMServiceName,
   396  			EmailDomain: form.PAMEmailDomain,
   397  		}
   398  	case auth.OAuth2:
   399  		config = parseOAuth2Config(form)
   400  		oauth2Config := config.(*oauth2.Source)
   401  		if oauth2Config.Provider == "openidConnect" {
   402  			discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
   403  			if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
   404  				ctx.Data["Err_DiscoveryURL"] = true
   405  				ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form)
   406  				return
   407  			}
   408  		}
   409  	case auth.SSPI:
   410  		config, err = parseSSPIConfig(ctx, form)
   411  		if err != nil {
   412  			ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
   413  			return
   414  		}
   415  	default:
   416  		ctx.Error(http.StatusBadRequest)
   417  		return
   418  	}
   419  
   420  	source.Name = form.Name
   421  	source.IsActive = form.IsActive
   422  	source.IsSyncEnabled = form.IsSyncEnabled
   423  	source.Cfg = config
   424  	if err := auth.UpdateSource(source); err != nil {
   425  		if auth.IsErrSourceAlreadyExist(err) {
   426  			ctx.Data["Err_Name"] = true
   427  			ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form)
   428  		} else if oauth2.IsErrOpenIDConnectInitialize(err) {
   429  			ctx.Flash.Error(err.Error(), true)
   430  			ctx.Data["Err_DiscoveryURL"] = true
   431  			ctx.HTML(http.StatusOK, tplAuthEdit)
   432  		} else {
   433  			ctx.ServerError("UpdateSource", err)
   434  		}
   435  		return
   436  	}
   437  	log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID)
   438  
   439  	ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
   440  	ctx.Redirect(setting.AppSubURL + "/admin/auths/" + strconv.FormatInt(form.ID, 10))
   441  }
   442  
   443  // DeleteAuthSource response for deleting an auth source
   444  func DeleteAuthSource(ctx *context.Context) {
   445  	source, err := auth.GetSourceByID(ctx.ParamsInt64(":authid"))
   446  	if err != nil {
   447  		ctx.ServerError("auth.GetSourceByID", err)
   448  		return
   449  	}
   450  
   451  	if err = auth_service.DeleteSource(source); err != nil {
   452  		if auth.IsErrSourceInUse(err) {
   453  			ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
   454  		} else {
   455  			ctx.Flash.Error(fmt.Sprintf("auth_service.DeleteSource: %v", err))
   456  		}
   457  		ctx.JSONRedirect(setting.AppSubURL + "/admin/auths/" + url.PathEscape(ctx.Params(":authid")))
   458  		return
   459  	}
   460  	log.Trace("Authentication deleted by admin(%s): %d", ctx.Doer.Name, source.ID)
   461  
   462  	ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
   463  	ctx.JSONRedirect(setting.AppSubURL + "/admin/auths")
   464  }