1// Copyright 2019 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package cmd
5
6import (
7 "context"
8 "fmt"
9 "strings"
10
11 "forgejo.org/models/auth"
12 "forgejo.org/services/auth/source/ldap"
13
14 "github.com/urfave/cli/v2"
15)
16
17type (
18 authService struct {
19 initDB func(ctx context.Context) error
20 createAuthSource func(context.Context, *auth.Source) error
21 updateAuthSource func(context.Context, *auth.Source) error
22 getAuthSourceByID func(ctx context.Context, id int64) (*auth.Source, error)
23 }
24)
25
26var (
27 commonLdapCLIFlags = []cli.Flag{
28 &cli.StringFlag{
29 Name: "name",
30 Usage: "Authentication name.",
31 },
32 &cli.BoolFlag{
33 Name: "not-active",
34 Usage: "Deactivate the authentication source.",
35 },
36 &cli.BoolFlag{
37 Name: "active",
38 Usage: "Activate the authentication source.",
39 },
40 &cli.StringFlag{
41 Name: "security-protocol",
42 Usage: "Security protocol name.",
43 },
44 &cli.BoolFlag{
45 Name: "skip-tls-verify",
46 Usage: "Disable TLS verification.",
47 },
48 &cli.StringFlag{
49 Name: "host",
50 Usage: "The address where the LDAP server can be reached.",
51 },
52 &cli.IntFlag{
53 Name: "port",
54 Usage: "The port to use when connecting to the LDAP server.",
55 },
56 &cli.StringFlag{
57 Name: "user-search-base",
58 Usage: "The LDAP base at which user accounts will be searched for.",
59 },
60 &cli.StringFlag{
61 Name: "user-filter",
62 Usage: "An LDAP filter declaring how to find the user record that is attempting to authenticate.",
63 },
64 &cli.StringFlag{
65 Name: "admin-filter",
66 Usage: "An LDAP filter specifying if a user should be given administrator privileges.",
67 },
68 &cli.StringFlag{
69 Name: "restricted-filter",
70 Usage: "An LDAP filter specifying if a user should be given restricted status.",
71 },
72 &cli.BoolFlag{
73 Name: "allow-deactivate-all",
74 Usage: "Allow empty search results to deactivate all users.",
75 },
76 &cli.StringFlag{
77 Name: "username-attribute",
78 Usage: "The attribute of the user’s LDAP record containing the user name.",
79 },
80 &cli.StringFlag{
81 Name: "firstname-attribute",
82 Usage: "The attribute of the user’s LDAP record containing the user’s first name.",
83 },
84 &cli.StringFlag{
85 Name: "surname-attribute",
86 Usage: "The attribute of the user’s LDAP record containing the user’s surname.",
87 },
88 &cli.StringFlag{
89 Name: "email-attribute",
90 Usage: "The attribute of the user’s LDAP record containing the user’s email address.",
91 },
92 &cli.StringFlag{
93 Name: "public-ssh-key-attribute",
94 Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.",
95 },
96 &cli.BoolFlag{
97 Name: "skip-local-2fa",
98 Usage: "Set to true to skip local 2fa for users authenticated by this source",
99 },
100 &cli.StringFlag{
101 Name: "avatar-attribute",
102 Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
103 },
104 }
105
106 ldapBindDnCLIFlags = append(commonLdapCLIFlags,
107 &cli.StringFlag{
108 Name: "bind-dn",
109 Usage: "The DN to bind to the LDAP server with when searching for the user.",
110 },
111 &cli.StringFlag{
112 Name: "bind-password",
113 Usage: "The password for the Bind DN, if any.",
114 },
115 &cli.BoolFlag{
116 Name: "attributes-in-bind",
117 Usage: "Fetch attributes in bind DN context.",
118 },
119 &cli.BoolFlag{
120 Name: "synchronize-users",
121 Usage: "Enable user synchronization.",
122 },
123 &cli.BoolFlag{
124 Name: "disable-synchronize-users",
125 Usage: "Disable user synchronization.",
126 },
127 &cli.UintFlag{
128 Name: "page-size",
129 Usage: "Search page size.",
130 })
131
132 ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags,
133 &cli.StringFlag{
134 Name: "user-dn",
135 Usage: "The user's DN.",
136 })
137
138 microcmdAuthAddLdapBindDn = &cli.Command{
139 Name: "add-ldap",
140 Usage: "Add new LDAP (via Bind DN) authentication source",
141 Action: func(c *cli.Context) error {
142 return newAuthService().addLdapBindDn(c)
143 },
144 Flags: ldapBindDnCLIFlags,
145 }
146
147 microcmdAuthUpdateLdapBindDn = &cli.Command{
148 Name: "update-ldap",
149 Usage: "Update existing LDAP (via Bind DN) authentication source",
150 Action: func(c *cli.Context) error {
151 return newAuthService().updateLdapBindDn(c)
152 },
153 Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...),
154 }
155
156 microcmdAuthAddLdapSimpleAuth = &cli.Command{
157 Name: "add-ldap-simple",
158 Usage: "Add new LDAP (simple auth) authentication source",
159 Action: func(c *cli.Context) error {
160 return newAuthService().addLdapSimpleAuth(c)
161 },
162 Flags: ldapSimpleAuthCLIFlags,
163 }
164
165 microcmdAuthUpdateLdapSimpleAuth = &cli.Command{
166 Name: "update-ldap-simple",
167 Usage: "Update existing LDAP (simple auth) authentication source",
168 Action: func(c *cli.Context) error {
169 return newAuthService().updateLdapSimpleAuth(c)
170 },
171 Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...),
172 }
173)
174
175// newAuthService creates a service with default functions.
176func newAuthService() *authService {
177 return &authService{
178 initDB: initDB,
179 createAuthSource: auth.CreateSource,
180 updateAuthSource: auth.UpdateSource,
181 getAuthSourceByID: auth.GetSourceByID,
182 }
183}
184
185// parseAuthSource assigns values on authSource according to command line flags.
186func parseAuthSource(c *cli.Context, authSource *auth.Source) {
187 if c.IsSet("name") {
188 authSource.Name = c.String("name")
189 }
190 if c.IsSet("not-active") {
191 authSource.IsActive = !c.Bool("not-active")
192 }
193 if c.IsSet("active") {
194 authSource.IsActive = c.Bool("active")
195 }
196 if c.IsSet("synchronize-users") {
197 authSource.IsSyncEnabled = c.Bool("synchronize-users")
198 }
199 if c.IsSet("disable-synchronize-users") {
200 authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users")
201 }
202}
203
204// parseLdapConfig assigns values on config according to command line flags.
205func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
206 if c.IsSet("name") {
207 config.Name = c.String("name")
208 }
209 if c.IsSet("host") {
210 config.Host = c.String("host")
211 }
212 if c.IsSet("port") {
213 config.Port = c.Int("port")
214 }
215 if c.IsSet("security-protocol") {
216 p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
217 if !ok {
218 return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
219 }
220 config.SecurityProtocol = p
221 }
222 if c.IsSet("skip-tls-verify") {
223 config.SkipVerify = c.Bool("skip-tls-verify")
224 }
225 if c.IsSet("bind-dn") {
226 config.BindDN = c.String("bind-dn")
227 }
228 if c.IsSet("user-dn") {
229 config.UserDN = c.String("user-dn")
230 }
231 if c.IsSet("bind-password") {
232 config.BindPassword = c.String("bind-password")
233 }
234 if c.IsSet("user-search-base") {
235 config.UserBase = c.String("user-search-base")
236 }
237 if c.IsSet("username-attribute") {
238 config.AttributeUsername = c.String("username-attribute")
239 }
240 if c.IsSet("firstname-attribute") {
241 config.AttributeName = c.String("firstname-attribute")
242 }
243 if c.IsSet("surname-attribute") {
244 config.AttributeSurname = c.String("surname-attribute")
245 }
246 if c.IsSet("email-attribute") {
247 config.AttributeMail = c.String("email-attribute")
248 }
249 if c.IsSet("attributes-in-bind") {
250 config.AttributesInBind = c.Bool("attributes-in-bind")
251 }
252 if c.IsSet("public-ssh-key-attribute") {
253 config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
254 }
255 if c.IsSet("avatar-attribute") {
256 config.AttributeAvatar = c.String("avatar-attribute")
257 }
258 if c.IsSet("page-size") {
259 config.SearchPageSize = uint32(c.Uint("page-size"))
260 }
261 if c.IsSet("user-filter") {
262 config.Filter = c.String("user-filter")
263 }
264 if c.IsSet("admin-filter") {
265 config.AdminFilter = c.String("admin-filter")
266 }
267 if c.IsSet("restricted-filter") {
268 config.RestrictedFilter = c.String("restricted-filter")
269 }
270 if c.IsSet("allow-deactivate-all") {
271 config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
272 }
273 if c.IsSet("skip-local-2fa") {
274 config.SkipLocalTwoFA = c.Bool("skip-local-2fa")
275 }
276 return nil
277}
278
279// findLdapSecurityProtocolByName finds security protocol by its name ignoring case.
280// It returns the value of the security protocol and if it was found.
281func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
282 for i, n := range ldap.SecurityProtocolNames {
283 if strings.EqualFold(name, n) {
284 return i, true
285 }
286 }
287 return 0, false
288}
289
290// getAuthSource gets the login source by its id defined in the command line flags.
291// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
292func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authType auth.Type) (*auth.Source, error) {
293 if err := argsSet(c, "id"); err != nil {
294 return nil, err
295 }
296
297 authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
298 if err != nil {
299 return nil, err
300 }
301
302 if authSource.Type != authType {
303 return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
304 }
305
306 return authSource, nil
307}
308
309// addLdapBindDn adds a new LDAP via Bind DN authentication source.
310func (a *authService) addLdapBindDn(c *cli.Context) error {
311 if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
312 return err
313 }
314
315 ctx, cancel := installSignals()
316 defer cancel()
317
318 if err := a.initDB(ctx); err != nil {
319 return err
320 }
321
322 authSource := &auth.Source{
323 Type: auth.LDAP,
324 IsActive: true, // active by default
325 Cfg: &ldap.Source{
326 Enabled: true, // always true
327 },
328 }
329
330 parseAuthSource(c, authSource)
331 if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
332 return err
333 }
334
335 return a.createAuthSource(ctx, authSource)
336}
337
338// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
339func (a *authService) updateLdapBindDn(c *cli.Context) error {
340 ctx, cancel := installSignals()
341 defer cancel()
342
343 if err := a.initDB(ctx); err != nil {
344 return err
345 }
346
347 authSource, err := a.getAuthSource(ctx, c, auth.LDAP)
348 if err != nil {
349 return err
350 }
351
352 parseAuthSource(c, authSource)
353 if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
354 return err
355 }
356
357 return a.updateAuthSource(ctx, authSource)
358}
359
360// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
361func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
362 if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
363 return err
364 }
365
366 ctx, cancel := installSignals()
367 defer cancel()
368
369 if err := a.initDB(ctx); err != nil {
370 return err
371 }
372
373 authSource := &auth.Source{
374 Type: auth.DLDAP,
375 IsActive: true, // active by default
376 Cfg: &ldap.Source{
377 Enabled: true, // always true
378 },
379 }
380
381 parseAuthSource(c, authSource)
382 if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
383 return err
384 }
385
386 return a.createAuthSource(ctx, authSource)
387}
388
389// updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source.
390func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
391 ctx, cancel := installSignals()
392 defer cancel()
393
394 if err := a.initDB(ctx); err != nil {
395 return err
396 }
397
398 authSource, err := a.getAuthSource(ctx, c, auth.DLDAP)
399 if err != nil {
400 return err
401 }
402
403 parseAuthSource(c, authSource)
404 if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
405 return err
406 }
407
408 return a.updateAuthSource(ctx, authSource)
409}