mirror of
https://codeberg.org/forgejo/forgejo
synced 2025-10-18 22:50:36 +02:00
Fixes #9433. ``` $ ./gitea admin user create --username blah --must-change-password false Hint: boolean false must be specified as a single arg, eg. '--restricted=false', not '--restricted false' Command error: unexpected arguments: false ``` **Breaking**: CLI sub-commands that only have flags would previously ignore anything that might be considered an "extra" argument, and would proceed without any errors. I've manually tested this change on the single `admin user create` command with positive (ensuring cmd still works) and negative (ensuring errors are reported) test cases. I've attempted to ensure the change is applied only to commands which don't use the CLI `Args()` and avoided touching them, including: - `admin user must-change-password` takes a list of users - `doctor recreate-tables` takes a list of tables - `embedded [list/view/extract]` use a pattern of resources to operate upon - git repo hook subcommands, and the ssh serv command, use arguments and have been omitted from the change ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [x] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9458 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
406 lines
11 KiB
Go
406 lines
11 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package cmd
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"forgejo.org/models/auth"
|
||
"forgejo.org/services/auth/source/ldap"
|
||
|
||
"github.com/urfave/cli/v3"
|
||
)
|
||
|
||
func commonLdapCLIFlags() []cli.Flag {
|
||
return []cli.Flag{
|
||
&cli.StringFlag{
|
||
Name: "name",
|
||
Usage: "Authentication name.",
|
||
},
|
||
&cli.BoolFlag{
|
||
Name: "not-active",
|
||
Usage: "Deactivate the authentication source.",
|
||
},
|
||
&cli.BoolFlag{
|
||
Name: "active",
|
||
Usage: "Activate the authentication source.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "security-protocol",
|
||
Usage: "Security protocol name.",
|
||
},
|
||
&cli.BoolFlag{
|
||
Name: "skip-tls-verify",
|
||
Usage: "Disable TLS verification.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "host",
|
||
Usage: "The address where the LDAP server can be reached.",
|
||
},
|
||
&cli.IntFlag{
|
||
Name: "port",
|
||
Usage: "The port to use when connecting to the LDAP server.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "user-search-base",
|
||
Usage: "The LDAP base at which user accounts will be searched for.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "user-filter",
|
||
Usage: "An LDAP filter declaring how to find the user record that is attempting to authenticate.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "admin-filter",
|
||
Usage: "An LDAP filter specifying if a user should be given administrator privileges.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "restricted-filter",
|
||
Usage: "An LDAP filter specifying if a user should be given restricted status.",
|
||
},
|
||
&cli.BoolFlag{
|
||
Name: "allow-deactivate-all",
|
||
Usage: "Allow empty search results to deactivate all users.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "username-attribute",
|
||
Usage: "The attribute of the user’s LDAP record containing the user name.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "firstname-attribute",
|
||
Usage: "The attribute of the user’s LDAP record containing the user’s first name.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "surname-attribute",
|
||
Usage: "The attribute of the user’s LDAP record containing the user’s surname.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "email-attribute",
|
||
Usage: "The attribute of the user’s LDAP record containing the user’s email address.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "public-ssh-key-attribute",
|
||
Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.",
|
||
},
|
||
&cli.BoolFlag{
|
||
Name: "skip-local-2fa",
|
||
Usage: "Set to true to skip local 2fa for users authenticated by this source",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "avatar-attribute",
|
||
Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
|
||
},
|
||
}
|
||
}
|
||
|
||
func ldapBindDnCLIFlags() []cli.Flag {
|
||
return append(commonLdapCLIFlags(),
|
||
&cli.StringFlag{
|
||
Name: "bind-dn",
|
||
Usage: "The DN to bind to the LDAP server with when searching for the user.",
|
||
},
|
||
&cli.StringFlag{
|
||
Name: "bind-password",
|
||
Usage: "The password for the Bind DN, if any.",
|
||
},
|
||
&cli.BoolFlag{
|
||
Name: "attributes-in-bind",
|
||
Usage: "Fetch attributes in bind DN context.",
|
||
},
|
||
&cli.BoolFlag{
|
||
Name: "synchronize-users",
|
||
Usage: "Enable user synchronization.",
|
||
},
|
||
&cli.BoolFlag{
|
||
Name: "disable-synchronize-users",
|
||
Usage: "Disable user synchronization.",
|
||
},
|
||
&cli.UintFlag{
|
||
Name: "page-size",
|
||
Usage: "Search page size.",
|
||
})
|
||
}
|
||
|
||
func ldapSimpleAuthCLIFlags() []cli.Flag {
|
||
return append(commonLdapCLIFlags(),
|
||
&cli.StringFlag{
|
||
Name: "user-dn",
|
||
Usage: "The user's DN.",
|
||
})
|
||
}
|
||
|
||
func microcmdAuthAddLdapBindDn() *cli.Command {
|
||
return &cli.Command{
|
||
Name: "add-ldap",
|
||
Usage: "Add new LDAP (via Bind DN) authentication source",
|
||
Before: noDanglingArgs,
|
||
Action: func(ctx context.Context, cli *cli.Command) error {
|
||
return newAuthService().addLdapBindDn(ctx, cli)
|
||
},
|
||
Flags: ldapBindDnCLIFlags(),
|
||
}
|
||
}
|
||
|
||
func microcmdAuthUpdateLdapBindDn() *cli.Command {
|
||
return &cli.Command{
|
||
Name: "update-ldap",
|
||
Usage: "Update existing LDAP (via Bind DN) authentication source",
|
||
Before: noDanglingArgs,
|
||
Action: func(ctx context.Context, cli *cli.Command) error {
|
||
return newAuthService().updateLdapBindDn(ctx, cli)
|
||
},
|
||
Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...),
|
||
}
|
||
}
|
||
|
||
func microcmdAuthAddLdapSimpleAuth() *cli.Command {
|
||
return &cli.Command{
|
||
Name: "add-ldap-simple",
|
||
Usage: "Add new LDAP (simple auth) authentication source",
|
||
Before: noDanglingArgs,
|
||
Action: func(ctx context.Context, cli *cli.Command) error {
|
||
return newAuthService().addLdapSimpleAuth(ctx, cli)
|
||
},
|
||
Flags: ldapSimpleAuthCLIFlags(),
|
||
}
|
||
}
|
||
|
||
func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
|
||
return &cli.Command{
|
||
Name: "update-ldap-simple",
|
||
Usage: "Update existing LDAP (simple auth) authentication source",
|
||
Before: noDanglingArgs,
|
||
Action: func(ctx context.Context, cli *cli.Command) error {
|
||
return newAuthService().updateLdapSimpleAuth(ctx, cli)
|
||
},
|
||
Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...),
|
||
}
|
||
}
|
||
|
||
// parseAuthSource assigns values on authSource according to command line flags.
|
||
func parseAuthSource(c *cli.Command, authSource *auth.Source) {
|
||
if c.IsSet("name") {
|
||
authSource.Name = c.String("name")
|
||
}
|
||
if c.IsSet("not-active") {
|
||
authSource.IsActive = !c.Bool("not-active")
|
||
}
|
||
if c.IsSet("active") {
|
||
authSource.IsActive = c.Bool("active")
|
||
}
|
||
if c.IsSet("synchronize-users") {
|
||
authSource.IsSyncEnabled = c.Bool("synchronize-users")
|
||
}
|
||
if c.IsSet("disable-synchronize-users") {
|
||
authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users")
|
||
}
|
||
}
|
||
|
||
// parseLdapConfig assigns values on config according to command line flags.
|
||
func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
|
||
if c.IsSet("name") {
|
||
config.Name = c.String("name")
|
||
}
|
||
if c.IsSet("host") {
|
||
config.Host = c.String("host")
|
||
}
|
||
if c.IsSet("port") {
|
||
config.Port = c.Int("port")
|
||
}
|
||
if c.IsSet("security-protocol") {
|
||
p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
|
||
if !ok {
|
||
return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
|
||
}
|
||
config.SecurityProtocol = p
|
||
}
|
||
if c.IsSet("skip-tls-verify") {
|
||
config.SkipVerify = c.Bool("skip-tls-verify")
|
||
}
|
||
if c.IsSet("bind-dn") {
|
||
config.BindDN = c.String("bind-dn")
|
||
}
|
||
if c.IsSet("user-dn") {
|
||
config.UserDN = c.String("user-dn")
|
||
}
|
||
if c.IsSet("bind-password") {
|
||
config.BindPassword = c.String("bind-password")
|
||
}
|
||
if c.IsSet("user-search-base") {
|
||
config.UserBase = c.String("user-search-base")
|
||
}
|
||
if c.IsSet("username-attribute") {
|
||
config.AttributeUsername = c.String("username-attribute")
|
||
}
|
||
if c.IsSet("firstname-attribute") {
|
||
config.AttributeName = c.String("firstname-attribute")
|
||
}
|
||
if c.IsSet("surname-attribute") {
|
||
config.AttributeSurname = c.String("surname-attribute")
|
||
}
|
||
if c.IsSet("email-attribute") {
|
||
config.AttributeMail = c.String("email-attribute")
|
||
}
|
||
if c.IsSet("attributes-in-bind") {
|
||
config.AttributesInBind = c.Bool("attributes-in-bind")
|
||
}
|
||
if c.IsSet("public-ssh-key-attribute") {
|
||
config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
|
||
}
|
||
if c.IsSet("avatar-attribute") {
|
||
config.AttributeAvatar = c.String("avatar-attribute")
|
||
}
|
||
if c.IsSet("page-size") {
|
||
config.SearchPageSize = uint32(c.Uint("page-size"))
|
||
}
|
||
if c.IsSet("user-filter") {
|
||
config.Filter = c.String("user-filter")
|
||
}
|
||
if c.IsSet("admin-filter") {
|
||
config.AdminFilter = c.String("admin-filter")
|
||
}
|
||
if c.IsSet("restricted-filter") {
|
||
config.RestrictedFilter = c.String("restricted-filter")
|
||
}
|
||
if c.IsSet("allow-deactivate-all") {
|
||
config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
|
||
}
|
||
if c.IsSet("skip-local-2fa") {
|
||
config.SkipLocalTwoFA = c.Bool("skip-local-2fa")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// findLdapSecurityProtocolByName finds security protocol by its name ignoring case.
|
||
// It returns the value of the security protocol and if it was found.
|
||
func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
|
||
for i, n := range ldap.SecurityProtocolNames {
|
||
if strings.EqualFold(name, n) {
|
||
return i, true
|
||
}
|
||
}
|
||
return 0, false
|
||
}
|
||
|
||
// getAuthSource gets the login source by its id defined in the command line flags.
|
||
// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
|
||
func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
|
||
if err := argsSet(c, "id"); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if authSource.Type != authType {
|
||
return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
|
||
}
|
||
|
||
return authSource, nil
|
||
}
|
||
|
||
// addLdapBindDn adds a new LDAP via Bind DN authentication source.
|
||
func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
|
||
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
|
||
return err
|
||
}
|
||
|
||
ctx, cancel := installSignals(ctx)
|
||
defer cancel()
|
||
|
||
if err := a.initDB(ctx); err != nil {
|
||
return err
|
||
}
|
||
|
||
authSource := &auth.Source{
|
||
Type: auth.LDAP,
|
||
IsActive: true, // active by default
|
||
Cfg: &ldap.Source{
|
||
Enabled: true, // always true
|
||
},
|
||
}
|
||
|
||
parseAuthSource(c, authSource)
|
||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||
return err
|
||
}
|
||
|
||
return a.createAuthSource(ctx, authSource)
|
||
}
|
||
|
||
// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
|
||
func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error {
|
||
ctx, cancel := installSignals(ctx)
|
||
defer cancel()
|
||
|
||
if err := a.initDB(ctx); err != nil {
|
||
return err
|
||
}
|
||
|
||
authSource, err := a.getAuthSource(ctx, c, auth.LDAP)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
parseAuthSource(c, authSource)
|
||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||
return err
|
||
}
|
||
|
||
return a.updateAuthSource(ctx, authSource)
|
||
}
|
||
|
||
// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
|
||
func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
|
||
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
|
||
return err
|
||
}
|
||
|
||
ctx, cancel := installSignals(ctx)
|
||
defer cancel()
|
||
|
||
if err := a.initDB(ctx); err != nil {
|
||
return err
|
||
}
|
||
|
||
authSource := &auth.Source{
|
||
Type: auth.DLDAP,
|
||
IsActive: true, // active by default
|
||
Cfg: &ldap.Source{
|
||
Enabled: true, // always true
|
||
},
|
||
}
|
||
|
||
parseAuthSource(c, authSource)
|
||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||
return err
|
||
}
|
||
|
||
return a.createAuthSource(ctx, authSource)
|
||
}
|
||
|
||
// updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source.
|
||
func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
|
||
ctx, cancel := installSignals(ctx)
|
||
defer cancel()
|
||
|
||
if err := a.initDB(ctx); err != nil {
|
||
return err
|
||
}
|
||
|
||
authSource, err := a.getAuthSource(ctx, c, auth.DLDAP)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
parseAuthSource(c, authSource)
|
||
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
|
||
return err
|
||
}
|
||
|
||
return a.updateAuthSource(ctx, authSource)
|
||
}
|