mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-04 10:00:10 -05:00
switch to go vendoring
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/vendor
|
||||
/bin
|
||||
/golint.txt
|
||||
/govet.txt
|
||||
/dist
|
||||
/test/tests.*
|
||||
/3rdparty-LICENSES.md
|
||||
/.vscode
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
run:
|
||||
modules-download-mode: vendor
|
||||
issues-exit-code: 0
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
settings:
|
||||
printf:
|
||||
funcs:
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
|
||||
gocyclo:
|
||||
min-complexity: 10
|
||||
maligned:
|
||||
suggest-new: true
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 140
|
||||
goimports:
|
||||
local-prefixes: github.com/libregraph/idm
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
revive:
|
||||
min-confidence: 0
|
||||
|
||||
linters:
|
||||
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- dupl
|
||||
- errcheck
|
||||
- exportloopref
|
||||
- funlen
|
||||
- gochecknoinits
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- revive
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
|
||||
# don't enable:
|
||||
# - depguard - until https://github.com/OpenPeeDeeP/depguard/issues/7 gets fixed
|
||||
# - maligned,prealloc
|
||||
# - gochecknoglobals
|
||||
|
||||
severity:
|
||||
default-severity: warning
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"mode": "mod-vendor",
|
||||
"header": "# LibreGraph Identity Management 3rd party notices\n\nCopyright 2021 The LibreGraph Authors. See LICENSE.txt for license information. This document contains a list of open source components used in this project.\n",
|
||||
"manual": {
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# This is the official list of LibreGraph authors for copyright purposes.
|
||||
# This file is distinct from the CONTRIBUTORS files.
|
||||
# See the latter for an explanation.
|
||||
|
||||
# Names should be added to this file as one of
|
||||
# Organization's name
|
||||
# Individual's name <submission email address>
|
||||
# Individual's name <submission email address> <email2> <emailN>
|
||||
# See CONTRIBUTORS for the meaning of multiple email addresses.
|
||||
|
||||
# Please keep the list sorted.
|
||||
|
||||
Kopano b.v.
|
||||
ownCloud GmbH
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
# CHANGELOG
|
||||
|
||||
## Unreleased
|
||||
|
||||
|
||||
|
||||
## v0.4.0 (2022-11-30)
|
||||
|
||||
- Migrate to Go rndm module from GitHub
|
||||
- Bump github.com/prometheus/client_golang from 1.13.0 to 1.14.0
|
||||
- Bump github.com/coreos/go-systemd/v22 from 22.4.0 to 22.5.0
|
||||
- Bump github.com/spf13/cobra from 1.6.0 to 1.6.1
|
||||
- Bump github.com/bombsimon/logrusr/v3 from 3.0.0 to 3.1.0
|
||||
- Bump golang.org/x/text from 0.3.8 to 0.4.0
|
||||
- Bump stash.kopano.io/kgol/rndm from 1.1.1 to 1.1.2
|
||||
- Bump github.com/spf13/cobra from 1.5.0 to 1.6.0
|
||||
- Bump golang.org/x/text from 0.3.7 to 0.3.8
|
||||
- Bump github.com/coreos/go-systemd/v22 from 22.3.2 to 22.4.0
|
||||
- Bump github.com/prometheus/client_golang from 1.12.2 to 1.13.0
|
||||
- Bump github.com/go-ldap/ldap/v3 from 3.4.3 to 3.4.4
|
||||
- Switch pkg/ldapserver to logr
|
||||
- Set custom logger for go-ldap/ldap
|
||||
- Bump github.com/sirupsen/logrus from 1.8.1 to 1.9.0
|
||||
- Make substring filter case-insensitve
|
||||
- Return proper error code when exceeding size limit
|
||||
- Fix normalized DN attribute escaping
|
||||
- Switch to go-ldap/ldap for filter (de-)compilation
|
||||
- Fix DN compoare condition
|
||||
- Switch github action to use `make test`
|
||||
- improve DN comparison
|
||||
- pass through unparsed DN
|
||||
- Address a few linter complaints
|
||||
- Implement modify password extended operation for boltdb backend
|
||||
- Add backend plumbing for password modify extended operation
|
||||
- pwexop: Add support of generating a random password
|
||||
- Groundwork for password modify extended operation
|
||||
- Bump github.com/spf13/cobra from 1.4.0 to 1.5.0
|
||||
- Bump github.com/Songmu/prompter from 0.5.0 to 0.5.1
|
||||
- Bump github.com/prometheus/client_golang from 1.12.1 to 1.12.2
|
||||
- Bump github.com/go-ldap/ldap/v3 from 3.4.2 to 3.4.3
|
||||
- Bump github.com/go-asn1-ber/asn1-ber from 1.5.3 to 1.5.4
|
||||
- boltdb: Fix modify replace on RDN Attribute
|
||||
- Bump github.com/spf13/cobra from 1.3.0 to 1.4.0
|
||||
- boltdb bind: attributeTypes are case-insensitive
|
||||
- Tone down debug logging
|
||||
- encodeSearchDone might be called with nil doneControls
|
||||
- Bump go-crypt to latest master
|
||||
- Allow to disable go-crypt related code
|
||||
- Fix build on Darwin
|
||||
- Bump github.com/go-ldap/ldap/v3 from 3.4.1 to 3.4.2
|
||||
- Cleanup logging in boltdb handler
|
||||
- Bump github.com/prometheus/client_golang from 1.12.0 to 1.12.1
|
||||
- Bump github.com/prometheus/client_golang from 1.11.0 to 1.12.0
|
||||
- Introduce new parameter "ldap-admin-dn"
|
||||
- Normalize BaseDN and BindDN
|
||||
- LDAP Modify support for boltdb Handler
|
||||
- Add utils to apply LDAP Modify Request on Entries
|
||||
- Create ldapentry and ldapdn helper modules
|
||||
- Add shortcut for normalizing DN string
|
||||
- Parse and validate incoming LDAP Modify Requests
|
||||
- fix typo
|
||||
- boltdb: Add getEntryByID method
|
||||
- boltdb: Make internal helper methods private
|
||||
- Bump all unversioned dependencies to their latest code
|
||||
- Implement Delete Support for boltdb Handler
|
||||
- Parse and validate incoming LDAP Delete Requests
|
||||
- Bump github.com/sirupsen/logrus from 1.6.0 to 1.8.1
|
||||
- Bump github.com/spf13/cobra from 1.2.1 to 1.3.0
|
||||
- Bump github.com/prometheus/client_golang from 0.9.3 to 1.11.0
|
||||
- Bump golang.org/x/text from 0.3.5 to 0.3.7
|
||||
- Initial LDAPAdd Support for the boltdb Handler
|
||||
- LDAPAdd support for the backend handlers
|
||||
- boltdb: Disallow adding an already existing Entry
|
||||
- Bump github.com/spf13/cobra from 1.1.3 to 1.2.1
|
||||
- Bump github.com/coreos/go-systemd/v22 from 22.3.0 to 22.3.2
|
||||
- Enable dependabot for go modules
|
||||
- Don't consider linter failures fatal
|
||||
- Parse and validate incoming LDAP Add Requests
|
||||
- Add basic plumbing for LDAP Add support
|
||||
- Update to latest bbolt release
|
||||
- Add some initial unit tests for boltdb backend ([#23](https://github.com/libregraph/idm/issues/23/))
|
||||
- Tone down golangci-lint annotation to warnings ([#24](https://github.com/libregraph/idm/issues/24/))
|
||||
- Add "boltdb export" subcommand
|
||||
- Set a default log-level for the boltdb related subcommands
|
||||
- Add ability to pass bolt.Options on database
|
||||
- Add SimpleBind support for BoltDB
|
||||
- Introduce a BoltDB based Database Handler
|
||||
- Add options to use other backends than 'ldif'
|
||||
- Add TLS support
|
||||
- Adjust golangci-lint config
|
||||
- Add initial Github Action as a starting point for CI
|
||||
- Bump go-ldap to v3.4.1
|
||||
|
||||
|
||||
## v0.3.0 (2021-09-29)
|
||||
|
||||
- Add new contributor/authors
|
||||
- Fix loading of LDIF directory
|
||||
- Change license to Apache License 2.0
|
||||
- review comments
|
||||
- review comments
|
||||
- Update readme for usage from compiled binary
|
||||
- Rewrite readme
|
||||
- Remove Kopano wording from readme file
|
||||
- Change copyright headers from Kopano to LibreGraph Authors
|
||||
- Add A+C files
|
||||
- Avoid duplicate index entries when using sub and pres
|
||||
- Index mail pres and sub for mail attribute
|
||||
- Cure potential panic in search without pagination
|
||||
- Apply search BaseDN when returning values from index
|
||||
- Introduce proper way to set defaults with option to override
|
||||
- Remove Kopano specific defaults and naming for white label rename
|
||||
- Rename public stuttering API functions
|
||||
- Make internal ldappasswd package importable
|
||||
- Make internal ldapserver package importable
|
||||
- Remove Jenkinsfile to prepare for external CI
|
||||
- Move project to github.com/libregraph/idm
|
||||
- Add proper LICENSE file
|
||||
- Add readme file
|
||||
|
||||
|
||||
## v0.2.7 (2021-05-31)
|
||||
|
||||
- Skip loading nil LDIF entries
|
||||
|
||||
|
||||
## v0.2.6 (2021-05-26)
|
||||
|
||||
- Use correct parts count for glibc2 CRYPT
|
||||
- Ignore case when selecting password crypt algo
|
||||
- Use absolute path for kill command
|
||||
|
||||
|
||||
## v0.2.5 (2021-05-26)
|
||||
|
||||
- Fix file loading in newusers sub command
|
||||
|
||||
|
||||
## v0.2.4 (2021-04-29)
|
||||
|
||||
- Fix missing variable in default LDIF main config template
|
||||
|
||||
|
||||
## v0.2.3 (2021-04-29)
|
||||
|
||||
- Ensure to setup folders with correct permissions
|
||||
|
||||
|
||||
## v0.2.2 (2021-04-29)
|
||||
|
||||
- Add setup step for systemd based startup
|
||||
|
||||
|
||||
## v0.2.1 (2021-04-29)
|
||||
|
||||
- Fix refactoring error for hash based password checks
|
||||
|
||||
|
||||
## v0.2.0 (2021-04-29)
|
||||
|
||||
- Move password hash functionality to internal module
|
||||
- Add password strength checks
|
||||
- Add gen passwd subcommand
|
||||
- Consolidate password hashing functions
|
||||
- Ignore commented lines when processing templates
|
||||
- Support relative paths in templates
|
||||
- Include demo LDIF generator script
|
||||
- Only load files in templates which are in a base folder
|
||||
- Unify config and commandline options
|
||||
- Add binscript, systemd service and config
|
||||
- Add reload support via SIGHUP
|
||||
- Enable index and index lookup for objectClass only filters
|
||||
- Add sub index support
|
||||
- Add present index support
|
||||
- Add proper license headers and origin reference
|
||||
- Add some AD attributres for equality indexing
|
||||
|
||||
|
||||
## v0.1.0 (2021-04-22)
|
||||
|
||||
- Improve string comparison performance
|
||||
- Improve LDIF parse logging
|
||||
- Prevent duplicates from multiple search equality index matches
|
||||
- Allow negative search equality index match
|
||||
- Add support to load LDIF data from folder
|
||||
- Implement gen newusers sub command with LDIF output
|
||||
- Add support for argon2 password hashing
|
||||
- Implement more LDAP server metrics
|
||||
- Add metrics support
|
||||
- Fix LDAP server stats support
|
||||
- Log LDAP close
|
||||
- Remove unsupported Unbinder
|
||||
- Fix debug log formatting
|
||||
- Use better anonymous bind for standard compliance
|
||||
- Add pprof support
|
||||
- Implement difference between startup and runtime errors
|
||||
- Add environment variables to set default config values
|
||||
- Move serve command into sub folder to prepare for other sub commands
|
||||
- Use template syntax in demo users generator
|
||||
- Apply ldif template defaults
|
||||
- Move LDIF template functionality into its own file
|
||||
- Improve flexibility of template support
|
||||
- Support setting current value in AutoIncrement template function
|
||||
- Improve commandline parameter naming
|
||||
- Use better names for example ldif
|
||||
- Allow configuration of LDIF template defaults
|
||||
- Add support to allow local anonymoys LDAP bind and search
|
||||
- Load LDIF files with template support
|
||||
- Actually allow LDIF middleware bind to succeed
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# This is the official list of people who can contribute
|
||||
# (and typically have contributed) code to the LibreGraph repository.
|
||||
# The AUTHORS file lists the copyright holders; this file
|
||||
# lists people. For example, Kopano employees are listed here
|
||||
# but not in AUTHORS, because Kopano holds the copyright.
|
||||
#
|
||||
# Names should be added to this file like so:
|
||||
# Individual's name <submission email address>
|
||||
# Individual's name <submission email address> <email2> <emailN>
|
||||
#
|
||||
# An entry with multiple email addresses specifies that the
|
||||
# first address should be used in the submit logs.
|
||||
|
||||
# Please keep the list sorted.
|
||||
|
||||
Felix Bartels <f.bartels@kopano.com>
|
||||
Ralf Haferkamp <rhaferkamp@owncloud.com>
|
||||
Simon Eisenmann <s.eisenmann@kopano.com> <simon@longsleep.org>
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#
|
||||
# License: Apache-2.0
|
||||
# Copyright 2021 The LibreGraph Authors.
|
||||
#
|
||||
|
||||
FROM golang:1.16.3-buster
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
ARG GOLANGCI_LINT_TAG=v1.38.0
|
||||
RUN curl -sfL \
|
||||
https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
|
||||
sh -s -- -b /usr/local/bin ${GOLANGCI_LINT_TAG}
|
||||
|
||||
RUN GOBIN=/usr/local/bin go get -v \
|
||||
github.com/tebeka/go2xunit \
|
||||
&& go clean -cache && rm -rf /root/go
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ENV GOCACHE=/tmp/go-build
|
||||
ENV GOPATH=""
|
||||
ENV HOME=/tmp
|
||||
|
||||
CMD ["make", "DATE=reproducible"]
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
PACKAGE = github.com/libregraph/idm
|
||||
PACKAGE_NAME = libregraph-$(shell basename $(PACKAGE))
|
||||
|
||||
# Tools
|
||||
|
||||
GO ?= go
|
||||
GOFMT ?= gofmt
|
||||
GOLINT ?= golangci-lint
|
||||
|
||||
GO2XUNIT ?= go2xunit
|
||||
|
||||
CHGLOG ?= git-chglog
|
||||
CURL ?= curl
|
||||
|
||||
# Cgo
|
||||
|
||||
CGO_ENABLED ?= 1
|
||||
|
||||
# Go modules
|
||||
|
||||
GO111MODULE ?= on
|
||||
|
||||
# Variables
|
||||
|
||||
export CGO_ENABLED GO111MODULE
|
||||
unexport GOPATH
|
||||
|
||||
ARGS ?=
|
||||
PWD := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2>/dev/null | sed 's/^v//' || \
|
||||
cat $(CURDIR)/.version 2> /dev/null || echo 0.0.0-unreleased)
|
||||
PKGS = $(or $(PKG),$(shell $(GO) list -mod=readonly ./... | grep -v "^$(PACKAGE)/vendor/"))
|
||||
TESTPKGS = $(shell $(GO) list -mod=readonly -f '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' $(PKGS) 2>/dev/null)
|
||||
CMDS = $(or $(CMD),$(addprefix cmd/,$(notdir $(shell find "$(PWD)/cmd/" -maxdepth 1 -type d))))
|
||||
TIMEOUT = 30
|
||||
BUILD_TAGS ?=
|
||||
|
||||
# Build
|
||||
|
||||
.PHONY: all
|
||||
all: fmt | $(CMDS) $(PLUGINS)
|
||||
|
||||
plugins: fmt | $(PLUGINS)
|
||||
|
||||
.PHONY: $(CMDS)
|
||||
$(CMDS): vendor ; $(info building $@ ...) @
|
||||
CGO_ENABLED=$(CGO_ENABLED) $(GO) build \
|
||||
-mod=vendor \
|
||||
-trimpath \
|
||||
-tags "release $(BUILD_TAGS)" \
|
||||
-buildmode=exe \
|
||||
-ldflags '-s -w -buildid=reproducible/$(VERSION) -X $(PACKAGE)/version.Version=$(VERSION) -X $(PACKAGE)/version.BuildDate=$(DATE) -extldflags -static' \
|
||||
-o bin/$(notdir $@) ./$@
|
||||
|
||||
# Helpers
|
||||
|
||||
.PHONY: lint
|
||||
lint: vendor ; $(info running $(GOLINT) ...) @
|
||||
$(GOLINT) run
|
||||
|
||||
.PHONY: lint-checkstyle
|
||||
lint-checkstyle: vendor ; $(info running $(GOLINT) checkstyle ...) @
|
||||
@mkdir -p test
|
||||
$(GOLINT) run --out-format checkstyle --issues-exit-code 0 > test/tests.lint.xml
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ; $(info running gofmt ...) @
|
||||
@ret=0 && for d in $$($(GO) list -mod=readonly -f '{{.Dir}}' ./... | grep -v /vendor/); do \
|
||||
$(GOFMT) -l -w $$d/*.go || ret=$$? ; \
|
||||
done ; exit $$ret
|
||||
|
||||
.PHONY: check
|
||||
check: ; $(info checking dependencies ...) @
|
||||
@$(GO) mod verify && echo OK
|
||||
|
||||
# Tests
|
||||
|
||||
TEST_TARGETS := test-default test-bench test-short test-race test-verbose
|
||||
.PHONY: $(TEST_TARGETS)
|
||||
test-bench: ARGS=-run=_Bench* -test.benchmem -bench=.
|
||||
test-short: ARGS=-short
|
||||
test-race: ARGS=-race
|
||||
test-race: CGO_ENABLED=1
|
||||
test-verbose: ARGS=-v
|
||||
$(TEST_TARGETS): NAME=$(MAKECMDGOALS:test-%=%)
|
||||
$(TEST_TARGETS): test
|
||||
|
||||
.PHONY: test
|
||||
test: ; $(info running $(NAME:%=% )tests ...) @
|
||||
@CGO_ENABLED=$(CGO_ENABLED) $(GO) test -timeout $(TIMEOUT)s $(ARGS) $(TESTPKGS)
|
||||
|
||||
TEST_XML_TARGETS := test-xml-default test-xml-short test-xml-race
|
||||
.PHONY: $(TEST_XML_TARGETS)
|
||||
test-xml-short: ARGS=-short
|
||||
test-xml-race: ARGS=-race
|
||||
test-xml-race: CGO_ENABLED=1
|
||||
$(TEST_XML_TARGETS): NAME=$(MAKECMDGOALS:test-%=%)
|
||||
$(TEST_XML_TARGETS): test-xml
|
||||
|
||||
.PHONY: test-xml
|
||||
test-xml: ; $(info running $(NAME:%=% )tests ...) @
|
||||
@mkdir -p test
|
||||
2>&1 CGO_ENABLED=$(CGO_ENABLED) $(GO) test -timeout $(TIMEOUT)s $(ARGS) -v $(TESTPKGS) | tee test/tests.output
|
||||
$(shell test -s test/tests.output && $(GO2XUNIT) -fail -input test/tests.output -output test/tests.xml)
|
||||
|
||||
# Mod
|
||||
|
||||
go.sum: go.mod ; $(info updating dependencies ...)
|
||||
@$(GO) mod tidy -v
|
||||
@touch $@
|
||||
|
||||
.PHONY: vendor
|
||||
vendor: go.sum ; $(info retrieving dependencies ...)
|
||||
@$(GO) mod vendor -v
|
||||
@touch $@
|
||||
|
||||
# Dist
|
||||
|
||||
.PHONY: licenses
|
||||
licenses: vendor ; $(info building licenses files ...)
|
||||
$(CURDIR)/scripts/go-license-ranger.py > $(CURDIR)/3rdparty-LICENSES.md
|
||||
|
||||
3rdparty-LICENSES.md: licenses
|
||||
|
||||
.PHONY: dist
|
||||
dist: 3rdparty-LICENSES.md ; $(info building dist tarball ...)
|
||||
@rm -rf "dist/${PACKAGE_NAME}-${VERSION}"
|
||||
@mkdir -p "dist/${PACKAGE_NAME}-${VERSION}"
|
||||
@mkdir -p "dist/${PACKAGE_NAME}-${VERSION}/scripts"
|
||||
@mkdir -p "dist/${PACKAGE_NAME}-${VERSION}/docs"
|
||||
@cd dist && \
|
||||
cp -avf ../LICENSE.txt "${PACKAGE_NAME}-${VERSION}" && \
|
||||
cp -avf ../README.md "${PACKAGE_NAME}-${VERSION}" && \
|
||||
cp -avf ../3rdparty-LICENSES.md "${PACKAGE_NAME}-${VERSION}" && \
|
||||
cp -avf ../bin/* "${PACKAGE_NAME}-${VERSION}" && \
|
||||
cp -avf ../docs/example-template.ldif "${PACKAGE_NAME}-${VERSION}/docs" && \
|
||||
cp -avf ../scripts/libregraph-idmd.binscript "${PACKAGE_NAME}-${VERSION}/scripts" && \
|
||||
cp -avf ../scripts/libregraph-idmd.service "${PACKAGE_NAME}-${VERSION}/scripts" && \
|
||||
cp -avf ../scripts/idmd.cfg "${PACKAGE_NAME}-${VERSION}/scripts" && \
|
||||
cp -avf ../scripts/*.ldif.in "${PACKAGE_NAME}-${VERSION}/scripts" && \
|
||||
tar --owner=0 --group=0 -czvf ${PACKAGE_NAME}-${VERSION}.tar.gz "${PACKAGE_NAME}-${VERSION}" && \
|
||||
cd ..
|
||||
|
||||
.PHONE: changelog
|
||||
changelog: ; $(info updating changelog ...)
|
||||
$(CHGLOG) --output CHANGELOG.md $(ARGS) v0.1.0..
|
||||
|
||||
# Rest
|
||||
|
||||
.PHONY: clean
|
||||
clean: ; $(info cleaning ...) @
|
||||
@rm -rf bin
|
||||
@rm -rf test/test.*
|
||||
|
||||
.PHONY: version
|
||||
version:
|
||||
@echo $(VERSION)
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
## LibreGraph Identity Management
|
||||
|
||||
The LibreGraph Identity Management provides a LDAP server, which is easy to configure, does not have external dependencies and is tailored to work perfectly with other LibreGraph software.
|
||||
|
||||
The goal is that everyone who does not already have or needs an LDAP server, uses IDM.
|
||||
|
||||
Thus, IDM is a (currently read-only) drop in replacement for an existing LDAP server and does provide an LDAP interface if none is there already. IDM uses hard coded indexes and supports LDAP search, bind and unbind operations.
|
||||
|
||||
### Running idmd from a source build
|
||||
|
||||
Until packages and containers for more environments are available it is the easiest to just create a local build of `idmd`. For this just run `make`.
|
||||
|
||||
IDM uses a mixture of environment variables and parameters for configuration and needs to be at least passed a the location of an individual ldif file or a directory containing multiple ldif files.
|
||||
|
||||
```bash
|
||||
$ ./idmd serve --ldif-main ./export.ldif
|
||||
INFO[0000] LDAP listener started listen_addr="127.0.0.1:10389"
|
||||
INFO[0000] ready
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
The default base DN of IDM is `dc=lg,dc=local`. There is usually no need to change, it if you don't use the LDAP data for anything else. The value needs to match what the clients have configured. Similarly, the default mail domain is `lg.local`.
|
||||
|
||||
Both values can be changed by passing `--ldap-base-dn` or `--ldif-template-default-mail-domain` respectively.
|
||||
|
||||
IDM uses ldif files for its data source and those files, the location of these files needs to be passed at startup using the `--ldif-main` parameter.
|
||||
|
||||
#### Adding a service user for LDAP access
|
||||
|
||||
By default IDM does not have any users and anonymous bind is disabled. You can enable anonymous bind support for local requests by passing `--ldap-allow-local-anonymous` when running `idmd`. Alternatively a service user can be specified in the following way:
|
||||
|
||||
```bash
|
||||
cat <<EOF > ./config.ldif
|
||||
dn: cn=readonly,{{.BaseDN}}
|
||||
cn: readonly
|
||||
description: LDAP read only service user
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: organizationalRole
|
||||
userPassword: readonly
|
||||
EOF
|
||||
```
|
||||
|
||||
And then passed as an additional parameter when starting `idmd` by passing `--ldif-config ./config.ldif`. The `config.ldif` is for service users only and the data in there is used for bind requests only, but never returned for search requests.
|
||||
|
||||
#### Add users to the ldap service
|
||||
|
||||
`idmd` serves all ldif files from the folder specified by `--ldif-main` (loaded in lexical order and parsed as templates). Whenever any of the ldif files are changed, added or removed, make sure to restart `idmd`.
|
||||
|
||||
`idmd` listens on `127.0.0.1:10389` by default and does not ship with any default users. Example configuration can be found in the [scripts directory](https://github.com/libregraph/idm/tree/master/scripts) of this repository.
|
||||
|
||||
##### Add new users using the `gen newusers` command
|
||||
|
||||
IDM provides a way to create ldif data for new users using batch mode similar to the unix `newusers` command using the following standard password file format:
|
||||
|
||||
```bash
|
||||
uid:userPassword:uidNumber:gidNumber:cn,[mail][,mailAlternateAddress...]:ignored:ignored
|
||||
```
|
||||
|
||||
For example, like this:
|
||||
|
||||
```bash
|
||||
cat << EOF | ./idmd gen newusers - --min-password-strength=4 > ./ldif/50-users.ldif
|
||||
jonas:passwordOfJonas123:::Jonas Brekke,jonas@lg.local::
|
||||
timmothy:passwordOfTimmothy456:::Timmothy Schöwalter::
|
||||
EOF
|
||||
```
|
||||
|
||||
This outputs an LDIF template file which you can modify as needed. When done run restart `idmd` to make the new users available. Keep in mind that some of the attributes must be unique.
|
||||
|
||||
##### Replace existing OpenLDAP with IDM
|
||||
|
||||
On the LDAP server export all its data using `slapcat` and write the resulting ldif to for example `./ldif/10-main.ldif`. This is a drop in replacement and all what was in OpenLDAP is now also in IDM.
|
||||
|
||||
Either stop `slapd` and change the IDM configuration to listen where `slapd` used to listen or change the clients to connect to where `idmd` listens to migrate.
|
||||
|
||||
### Extra goodies
|
||||
|
||||
#### Template support
|
||||
|
||||
All ldif files loaded by IDM support template syntax as defined in https://golang.org/pkg/text/template to allow auto generation and replacement of various values. You can find example templates in the [scripts directory](https://github.com/libregraph/idm/tree/master/scripts) as well. All the `gen` commands output template syntax if applicable.
|
||||
|
||||
#### Generate secure password hash using the `gen passwd` command
|
||||
|
||||
IDM supports secure password hashing using ARGON2. To create such password hashes either use `gen newusers` or the interactive `gen passwd` which is very similar to `slappasswd` from OpenLDAP.
|
||||
|
||||
```bash
|
||||
./idmd gen passwd
|
||||
New password:
|
||||
Re-enter new password:
|
||||
{ARGON2}$argon2id$v=19$m=65536,t=1,p=2$MaB5gX2BI484dATbGFyEIg$h2X8rbPowzZ/Exsz4W20Z/Zk54C30YnY+YbivSIRpcI
|
||||
```
|
||||
|
||||
#### Test IDM
|
||||
|
||||
Since `idmd` provides a standard LDAP interface, also standard LDAP tools can be used to interact with it for testing. Run `apt install ldap-utils` to install LDAP commandline tools.
|
||||
|
||||
```bash
|
||||
ldapsearch -x -H ldap://127.0.0.1:10389 -b "dc=lg,dc=local" -D "cn=readonly,dc=lg,dc=local" -w 'readonly'
|
||||
```
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package idm
|
||||
|
||||
// Defaults as used by multiple sub packages.
|
||||
var (
|
||||
DefaultLDAPBaseDN = "dc=lg,dc=local"
|
||||
DefaultMailDomain = "lg.local"
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package idm // import "github.com/libregraph/idm"
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package ldapdn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/text/cases"
|
||||
)
|
||||
|
||||
// Normalize takes an ldap.DN struct and turns it into a "normalized" DN string
|
||||
// by cases folding all RDN (attributetypes and values). Note: This currently
|
||||
// handles all attributes as caseIgnoreStrings ignoring the Syntax the Attribute
|
||||
// Type might have assigned.
|
||||
func Normalize(dn *ldap.DN) string {
|
||||
var nDN string
|
||||
caseFold := cases.Fold()
|
||||
for r, rdn := range dn.RDNs {
|
||||
// FIXME to really normalize multivalued RDNs we'd need
|
||||
// to normalize the order of Attributes here as well
|
||||
for a, ava := range rdn.Attributes {
|
||||
if a > 0 {
|
||||
// This is a multivalued RDN.
|
||||
nDN += "+"
|
||||
} else if r > 0 {
|
||||
nDN += ","
|
||||
}
|
||||
nDN = nDN + caseFold.String(ava.Type) + "=" + encodeRDNValue(caseFold.String(ava.Value))
|
||||
}
|
||||
}
|
||||
return nDN
|
||||
}
|
||||
|
||||
// encodeRDNValue applies the DN escaping rules (RFC4514) to the supplied
|
||||
// string (the value part of an RDN). Returns the escaped string.
|
||||
// Note: This function is taken from https://github.com/go-ldap/ldap/pull/104
|
||||
func encodeRDNValue(rDNValue string) string {
|
||||
encodedBuf := bytes.Buffer{}
|
||||
|
||||
escapeChar := func(c byte) {
|
||||
encodedBuf.WriteByte('\\')
|
||||
encodedBuf.WriteByte(c)
|
||||
}
|
||||
|
||||
escapeHex := func(c byte) {
|
||||
encodedBuf.WriteByte('\\')
|
||||
encodedBuf.WriteString(hex.EncodeToString([]byte{c}))
|
||||
}
|
||||
|
||||
for i := 0; i < len(rDNValue); i++ {
|
||||
char := rDNValue[i]
|
||||
if i == 0 && char == ' ' || char == '#' {
|
||||
// Special case leading space or number sign.
|
||||
escapeChar(char)
|
||||
continue
|
||||
}
|
||||
if i == len(rDNValue)-1 && char == ' ' {
|
||||
// Special case trailing space.
|
||||
escapeChar(char)
|
||||
continue
|
||||
}
|
||||
|
||||
switch char {
|
||||
case '"', '+', ',', ';', '<', '>', '\\':
|
||||
// Each of these special characters must be escaped.
|
||||
escapeChar(char)
|
||||
continue
|
||||
}
|
||||
|
||||
if char < ' ' || char > '~' {
|
||||
// All special character escapes are handled first
|
||||
// above. All bytes less than ASCII SPACE and all bytes
|
||||
// greater than ASCII TILDE must be hex-escaped.
|
||||
escapeHex(char)
|
||||
continue
|
||||
}
|
||||
|
||||
// Any other character does not require escaping.
|
||||
encodedBuf.WriteByte(char)
|
||||
}
|
||||
|
||||
return encodedBuf.String()
|
||||
}
|
||||
|
||||
// ParseNormalize normalizes the passed LDAP DN string by first parsing it (using ldap.ParseDN)
|
||||
// and then casefolding all RDN using ldapdn.Normalize(). ParseNormalize will return an error
|
||||
// when parsing the DN fails.
|
||||
func ParseNormalize(dn string) (string, error) {
|
||||
parsed, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return Normalize(parsed), nil
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package ldapentry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/text/cases"
|
||||
)
|
||||
|
||||
func ApplyModify(old *ldap.Entry, mod *ldap.ModifyRequest) (newEntry *ldap.Entry, err error) {
|
||||
oldDN, err := ldap.ParseDN(old.DN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rdn := oldDN.RDNs[0]
|
||||
modDN, err := ldap.ParseDN(mod.DN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// This shouldn't happen if we ge here (TM)
|
||||
if !oldDN.EqualFold(modDN) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("DNs do not match"))
|
||||
}
|
||||
|
||||
casefold := cases.Fold()
|
||||
newEntry = ldap.NewEntry(old.DN, map[string][]string{})
|
||||
newEntry.Attributes = old.Attributes
|
||||
for _, c := range mod.Changes {
|
||||
nType := casefold.String(c.Modification.Type)
|
||||
switch c.Operation {
|
||||
case ldap.AddAttribute:
|
||||
newValues := entryApplyModAdd(newEntry.GetEqualFoldAttributeValues(nType), c.Modification.Vals)
|
||||
newEntry.Attributes = entryReplaceValues(newEntry.Attributes, c.Modification.Type, newValues)
|
||||
|
||||
case ldap.ReplaceAttribute:
|
||||
// Modifies on RDN attributes need special care to make sure that the rdn Value is not removed
|
||||
for _, rdnAttr := range rdn.Attributes {
|
||||
if nType == casefold.String(rdnAttr.Type) {
|
||||
rdnPresent := false
|
||||
nRdnVal := casefold.String(rdnAttr.Value)
|
||||
for _, newVal := range c.Modification.Vals {
|
||||
if nRdnVal == casefold.String(newVal) {
|
||||
rdnPresent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !rdnPresent {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotAllowedOnRDN, errors.New(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
newEntry.Attributes = entryReplaceValues(newEntry.Attributes, c.Modification.Type, c.Modification.Vals)
|
||||
case ldap.DeleteAttribute:
|
||||
for _, rdnAttr := range rdn.Attributes {
|
||||
// Modifies on RDN attributes need special care
|
||||
if nType == casefold.String(rdnAttr.Type) {
|
||||
if len(c.Modification.Vals) == 0 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotAllowedOnRDN, errors.New(""))
|
||||
}
|
||||
nRdnVal := casefold.String(rdnAttr.Value)
|
||||
for _, delVal := range c.Modification.Vals {
|
||||
if nRdnVal == casefold.String(delVal) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotAllowedOnRDN, errors.New(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
newValues := entryApplyModDelete(old.GetEqualFoldAttributeValues(nType), c.Modification.Vals)
|
||||
newEntry.Attributes = entryReplaceValues(newEntry.Attributes, c.Modification.Type, newValues)
|
||||
}
|
||||
}
|
||||
return newEntry, nil
|
||||
}
|
||||
|
||||
func entryReplaceValues(ea []*ldap.EntryAttribute, attrType string, newValues []string) (updatedAttrs []*ldap.EntryAttribute) {
|
||||
casefold := cases.Fold()
|
||||
nType := casefold.String(attrType)
|
||||
updated := false
|
||||
for _, attr := range ea {
|
||||
if casefold.String(attr.Name) == nType {
|
||||
updated = true
|
||||
if len(newValues) == 0 {
|
||||
continue
|
||||
}
|
||||
updatedAttrs = append(updatedAttrs, ldap.NewEntryAttribute(attr.Name, newValues))
|
||||
} else {
|
||||
updatedAttrs = append(updatedAttrs, attr)
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
if len(newValues) != 0 {
|
||||
updatedAttrs = append(updatedAttrs, ldap.NewEntryAttribute(attrType, newValues))
|
||||
}
|
||||
}
|
||||
return updatedAttrs
|
||||
}
|
||||
|
||||
func entryApplyModAdd(curVals, addVals []string) (newVals []string) {
|
||||
newVals = curVals
|
||||
casefold := cases.Fold()
|
||||
for _, newVal := range addVals {
|
||||
present := false
|
||||
for _, val := range curVals {
|
||||
if casefold.String(newVal) == casefold.String(val) {
|
||||
present = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !present {
|
||||
newVals = append(newVals, newVal)
|
||||
}
|
||||
}
|
||||
return newVals
|
||||
}
|
||||
|
||||
func entryApplyModDelete(curVals, delVals []string) (newVals []string) {
|
||||
casefold := cases.Fold()
|
||||
if len(delVals) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
for _, curVal := range curVals {
|
||||
nCurVal := casefold.String(curVal)
|
||||
keep := true
|
||||
for _, del := range delVals {
|
||||
if nCurVal == casefold.String(del) {
|
||||
keep = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if keep {
|
||||
newVals = append(newVals, curVal)
|
||||
}
|
||||
}
|
||||
return newVals
|
||||
}
|
||||
|
||||
func EntryFromAddRequest(add *ldap.AddRequest) *ldap.Entry {
|
||||
attrs := map[string][]string{}
|
||||
|
||||
for _, a := range add.Attributes {
|
||||
attrs[a.Type] = a.Vals
|
||||
}
|
||||
return ldap.NewEntry(add.DN, attrs)
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
//go:build !disable_crypt && (linux || freebsd || netbsd)
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldappassword
|
||||
|
||||
import (
|
||||
gocrypt "github.com/amoghe/go-crypt"
|
||||
)
|
||||
|
||||
func crypt(pass, salt string) (string, error) {
|
||||
return gocrypt.Crypt(pass, salt)
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
//go:build disable_crypt || darwin || windows
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldappassword
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func crypt(pass, salt string) (string, error) {
|
||||
return "", errors.New("CRYPT unsupported on this platform")
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldappassword
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha1" //nolint,gosec
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"github.com/trustelem/zxcvbn"
|
||||
)
|
||||
|
||||
var Argon2DefaultParams = argon2id.DefaultParams
|
||||
|
||||
func Validate(password string, hash string) (bool, error) {
|
||||
algorithm := ""
|
||||
if hash[0] == '{' {
|
||||
algorithmEnd := strings.Index(hash[0:], "}")
|
||||
if algorithmEnd >= 1 {
|
||||
algorithm = hash[0 : algorithmEnd+1]
|
||||
hash = hash[algorithmEnd+1:]
|
||||
}
|
||||
}
|
||||
|
||||
hashBytes := []byte(hash)
|
||||
var passwordBytes []byte
|
||||
|
||||
switch strings.ToUpper(algorithm) {
|
||||
case "":
|
||||
// No password scheme, direct comparison.
|
||||
passwordBytes = []byte(password)
|
||||
|
||||
case "{ARGON2}":
|
||||
// Follows the format used by the Argon2 reference C implementation and looks like this:
|
||||
// $argon2id$v=19$m=65536,t=3,p=2$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
|
||||
match, err := argon2id.ComparePasswordAndHash(password, hash)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("argon2 error: %w", err)
|
||||
}
|
||||
if !match {
|
||||
return false, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
return true, nil
|
||||
|
||||
case "{CRYPT}":
|
||||
// By default the salt is a two character string.
|
||||
salt := hash[:2]
|
||||
if hash[0] == '$' {
|
||||
// In the glibc2 version, salt format for additional encryption
|
||||
// $id$salt$encrypted.
|
||||
hashParts := strings.SplitN(hash, "$", 4)
|
||||
if len(hashParts) == 4 {
|
||||
salt = strings.Join(hashParts[:4], "$")
|
||||
}
|
||||
}
|
||||
encrypted, err := crypt(password, salt)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("crypt error: %w", err)
|
||||
}
|
||||
passwordBytes = []byte(encrypted)
|
||||
|
||||
case "{SSHA}":
|
||||
// BASE64(SHA-1(clear_text + salt) + salt)
|
||||
// The salt is 4 bytes long.
|
||||
decodedBytes, err := base64.StdEncoding.DecodeString(hash)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("ssha error: %w", err)
|
||||
}
|
||||
salt := decodedBytes[len(decodedBytes)-4:]
|
||||
h := sha1.New() //nolint,gosec
|
||||
h.Write([]byte(password))
|
||||
h.Write(salt)
|
||||
passwordBytes = h.Sum(nil)
|
||||
passwordBytes = append(passwordBytes, salt...)
|
||||
hashBytes = decodedBytes
|
||||
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported password algorithm: %s", algorithm)
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(hashBytes, passwordBytes) != 1 {
|
||||
return false, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func Hash(password string, algorithm string) (string, error) {
|
||||
var result string
|
||||
|
||||
switch algorithm {
|
||||
case "", "{CLEARTEXT}":
|
||||
result = password
|
||||
|
||||
case "{ARGON2}":
|
||||
hash, hashErr := argon2id.CreateHash(password, Argon2DefaultParams)
|
||||
if hashErr != nil {
|
||||
return "", fmt.Errorf("password hash error: %w", hashErr)
|
||||
}
|
||||
result = "{ARGON2}" + hash
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("password hash alg not supported: %s", algorithm)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func EstimatePasswordStrength(password string, userInputs []string) int {
|
||||
result := zxcvbn.PasswordStrength(password, userInputs)
|
||||
return result.Score
|
||||
}
|
||||
|
||||
func GenerateRandomPassword(length int) (string, error) {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-=+!@#$%^&*."
|
||||
ret := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ret[i] = chars[num.Int64()]
|
||||
}
|
||||
|
||||
return string(ret), nil
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
Copyright (c) 2021 The LibreGraph Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# LDAP server library for Golang
|
||||
|
||||
This library provides LDAP server v3 functionality for the GO programming
|
||||
language.
|
||||
|
||||
The server implementation is based on github.com/nmcclain/ldap and is enhanced
|
||||
so it can be used together with github.com/go-ldap/ldap/v3.
|
||||
|
||||
From the server perspective, all of RFC4510 is implemented except:
|
||||
|
||||
4.5.1.3. SearchRequest.derefAliases
|
||||
4.5.1.5. SearchRequest.timeLimit
|
||||
4.5.1.6. SearchRequest.typesOnly
|
||||
4.14. StartTLS Operation
|
||||
|
||||
The purpose of this library is not a general LDAP server implementation but to
|
||||
provide enough of an LDAP server for Kopano compatible identity management.
|
||||
|
||||
## License
|
||||
|
||||
See `LICENSE.txt` for licensing information of this module.
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleAddRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
|
||||
if boundDN == "" {
|
||||
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
|
||||
}
|
||||
addReq, err := parseAddRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fnNames := []string{}
|
||||
for k := range server.AddFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(addReq.DN, fnNames)
|
||||
var adder Adder
|
||||
if adder = server.AddFns[fn]; adder == nil {
|
||||
if fn == "" {
|
||||
err = fmt.Errorf("no suitable handler found for dn: '%s'", addReq.DN)
|
||||
} else {
|
||||
err = fmt.Errorf("handler '%s' does not support add", fn)
|
||||
}
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
|
||||
}
|
||||
code, err := adder.Add(boundDN, addReq, conn)
|
||||
return ldap.NewError(uint16(code), err)
|
||||
}
|
||||
|
||||
func parseAddRequest(req *ber.Packet) (*ldap.AddRequest, error) {
|
||||
addReq := ldap.AddRequest{}
|
||||
// LDAP Add request have 2 Elements (DN and AttributeList)
|
||||
if len(req.Children) != 2 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid add request"))
|
||||
}
|
||||
|
||||
dn, ok := req.Children[0].Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry DN"))
|
||||
}
|
||||
|
||||
_, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
|
||||
}
|
||||
addReq.DN = dn
|
||||
|
||||
al, err := parseAttributeList(req.Children[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addReq.Attributes = al
|
||||
|
||||
return &addReq, nil
|
||||
}
|
||||
|
||||
func parseAttributeList(req *ber.Packet) ([]ldap.Attribute, error) {
|
||||
ldapAttrs := []ldap.Attribute{}
|
||||
|
||||
if req.ClassType != ber.ClassUniversal || req.TagType != ber.TypeConstructed || req.Tag != ber.TagSequence {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute List"))
|
||||
}
|
||||
|
||||
for _, a := range req.Children {
|
||||
attr, err := parseAttribute(a, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ldapAttrs = append(ldapAttrs, *attr)
|
||||
|
||||
}
|
||||
return ldapAttrs, nil
|
||||
}
|
||||
|
||||
func parseAttribute(attr *ber.Packet, partial bool) (*ldap.Attribute, error) {
|
||||
var la ldap.Attribute
|
||||
var ok bool
|
||||
var err error
|
||||
|
||||
// Partial attributes, might just contain a type and allow the Value to be absent
|
||||
if partial && (len(attr.Children) < 1 || len(attr.Children) > 2) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding partial Attribute"))
|
||||
} else if !partial && len(attr.Children) != 2 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute"))
|
||||
}
|
||||
|
||||
ad := attr.Children[0]
|
||||
if ad.ClassType != ber.ClassUniversal || ad.TagType != ber.TypePrimitive || ad.Tag != ber.TagOctetString {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Description"))
|
||||
}
|
||||
la.Type, ok = ad.Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Description"))
|
||||
}
|
||||
|
||||
// We can return here if this is a Partial Attribute without values
|
||||
if partial && len(attr.Children) == 1 {
|
||||
return &la, nil
|
||||
}
|
||||
if la.Vals, err = parseAttributeValues(attr.Children[1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &la, nil
|
||||
}
|
||||
|
||||
func parseAttributeValues(values *ber.Packet) ([]string, error) {
|
||||
var strVals []string
|
||||
if values.ClassType != ber.ClassUniversal || values.TagType != ber.TypeConstructed || values.Tag != ber.TagSet {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Values"))
|
||||
}
|
||||
for _, value := range values.Children {
|
||||
strVal, ok := value.Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Value"))
|
||||
}
|
||||
strVals = append(strVals, strVal)
|
||||
}
|
||||
return strVals, nil
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleBindRequest(req *ber.Packet, fns map[string]Binder, conn net.Conn) (resultCode LDAPResultCode) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
}
|
||||
}()
|
||||
|
||||
// we only support ldapv3
|
||||
ldapVersion, ok := req.Children[0].Value.(int64)
|
||||
if !ok {
|
||||
return ldap.LDAPResultProtocolError
|
||||
}
|
||||
if ldapVersion != 3 {
|
||||
logger.V(1).Info("Unsupported LDAP version", "version", ldapVersion)
|
||||
return ldap.LDAPResultInappropriateAuthentication
|
||||
}
|
||||
|
||||
// auth types
|
||||
bindDN, ok := req.Children[1].Value.(string)
|
||||
if !ok {
|
||||
return ldap.LDAPResultProtocolError
|
||||
}
|
||||
bindAuth := req.Children[2]
|
||||
switch bindAuth.Tag {
|
||||
default:
|
||||
logger.V(1).Info("Unknown LDAP authentication method", "tag", bindAuth.Tag)
|
||||
return ldap.LDAPResultInappropriateAuthentication
|
||||
case LDAPBindAuthSimple:
|
||||
if len(req.Children) == 3 {
|
||||
fnNames := []string{}
|
||||
for k := range fns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(bindDN, fnNames)
|
||||
resultCode, err := fns[fn].Bind(bindDN, bindAuth.Data.String(), conn)
|
||||
if err != nil {
|
||||
logger.Error(err, "BindFn Error")
|
||||
return ldap.LDAPResultOperationsError
|
||||
}
|
||||
return resultCode
|
||||
} else {
|
||||
logger.V(1).Info("Simple bind request has wrong # children. len(req.Children) != 3")
|
||||
return ldap.LDAPResultInappropriateAuthentication
|
||||
}
|
||||
case LDAPBindAuthSASL:
|
||||
logger.V(1).Info("SASL authentication is not supported")
|
||||
return ldap.LDAPResultInappropriateAuthentication
|
||||
}
|
||||
return ldap.LDAPResultOperationsError
|
||||
}
|
||||
|
||||
func encodeBindResponse(messageID int64, ldapResultCode LDAPResultCode) *ber.Packet {
|
||||
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
|
||||
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
|
||||
|
||||
bindReponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ldap.ApplicationBindResponse, nil, "Bind Response")
|
||||
bindReponse.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: "))
|
||||
bindReponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
|
||||
bindReponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "errorMessage: "))
|
||||
|
||||
responsePacket.AppendChild(bindReponse)
|
||||
|
||||
// ber.PrintPacket(responsePacket)
|
||||
return responsePacket
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleDeleteRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
|
||||
if boundDN == "" {
|
||||
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
|
||||
}
|
||||
delReq, err := parseDeleteRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fnNames := []string{}
|
||||
for k := range server.DeleteFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(delReq.DN, fnNames)
|
||||
var del Deleter
|
||||
if del = server.DeleteFns[fn]; del == nil {
|
||||
if fn == "" {
|
||||
err = fmt.Errorf("no suitable handler found for dn: '%s'", delReq.DN)
|
||||
} else {
|
||||
err = fmt.Errorf("handler '%s' does not support add", fn)
|
||||
}
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
|
||||
}
|
||||
code, err := del.Delete(boundDN, delReq, conn)
|
||||
return ldap.NewError(uint16(code), err)
|
||||
}
|
||||
|
||||
func parseDeleteRequest(req *ber.Packet) (*ldap.DelRequest, error) {
|
||||
delReq := ldap.DelRequest{}
|
||||
// LDAP Delete requests contain just the DN (no Sequence, or set)
|
||||
// i.e. they have no childre
|
||||
if len(req.Children) != 0 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid delete request"))
|
||||
}
|
||||
dn := req.Data.String()
|
||||
|
||||
_, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
|
||||
}
|
||||
delReq.DN = dn
|
||||
|
||||
return &delReq, nil
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type ExopHandler func(req *ber.Packet, boundDN string, server *Server, conn net.Conn) (*ber.Packet, error)
|
||||
|
||||
type ExtendedRequest struct {
|
||||
OID string
|
||||
Body *ber.Packet
|
||||
}
|
||||
|
||||
var exopRegistry = map[string]ExopHandler{}
|
||||
|
||||
func RegisterExtendedOperation(oid string, handler ExopHandler) {
|
||||
exopRegistry[oid] = handler
|
||||
}
|
||||
|
||||
func HandleExtendedRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) (*ber.Packet, error) {
|
||||
extReq, err := parseExtendedRequest(req)
|
||||
if err != nil {
|
||||
logger.V(1).Info("parsing extened request failed", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
if handler, ok := exopRegistry[extReq.OID]; ok {
|
||||
innerBer, err := handler(extReq.Body, boundDN, server, conn)
|
||||
var resCode LDAPResultCode = ldap.LDAPResultSuccess
|
||||
msg := ""
|
||||
if err != nil {
|
||||
if lerr, ok := err.(*ldap.Error); ok {
|
||||
msg = lerr.Err.Error()
|
||||
resCode = LDAPResultCode(lerr.ResultCode)
|
||||
} else {
|
||||
msg = err.Error()
|
||||
resCode = ldap.LDAPResultOther
|
||||
}
|
||||
}
|
||||
return encodeExtendedResponse(resCode, msg, extReq.OID, innerBer), nil
|
||||
} else {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Unsupported Extented Operations"))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func parseExtendedRequest(req *ber.Packet) (*ExtendedRequest, error) {
|
||||
// RFC 4511 Extended Operation:
|
||||
// ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
|
||||
// requestName [0] LDAPOID,
|
||||
// requestValue [1] OCTET STRING OPTIONAL }
|
||||
extReq := ExtendedRequest{}
|
||||
if len(req.Children) < 1 || len(req.Children) > 2 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid extented request"))
|
||||
}
|
||||
|
||||
if req.Children[0].Identifier.ClassType != ber.ClassContext || req.Children[0].Identifier.Tag != 0 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("error decoding extented request OID"))
|
||||
}
|
||||
|
||||
extReq.OID = req.Children[0].Data.String()
|
||||
if len(req.Children) == 2 {
|
||||
if req.Children[1].Identifier.ClassType != ber.ClassContext || req.Children[1].Identifier.Tag != 1 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("error decoding extented request OID"))
|
||||
}
|
||||
extReq.Body = req.Children[1]
|
||||
}
|
||||
|
||||
logger.V(1).Info("Extended Request", "oid", extReq.OID)
|
||||
return &extReq, nil
|
||||
}
|
||||
|
||||
func encodeExtendedResponse(rescode LDAPResultCode, msg, oid string, responseValue *ber.Packet) *ber.Packet {
|
||||
respBer := ber.Encode(ber.ClassApplication, ber.TypeConstructed,
|
||||
ber.Tag(ldap.ApplicationExtendedResponse), nil,
|
||||
ldap.ApplicationMap[ldap.ApplicationExtendedResponse])
|
||||
respBer.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(rescode), "resultCode: "))
|
||||
respBer.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
|
||||
respBer.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, msg, "errorMessage: "))
|
||||
respBer.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 10, oid, "responseName"))
|
||||
if responseValue != nil {
|
||||
encValue := ber.Encode(ber.ClassContext, ber.TypePrimitive, 11, nil, "responseValue")
|
||||
encValue.AppendChild(responseValue)
|
||||
respBer.AppendChild(encValue)
|
||||
}
|
||||
return respBer
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/text/cases"
|
||||
)
|
||||
|
||||
const (
|
||||
FilterAnd = ldap.FilterAnd
|
||||
FilterOr = ldap.FilterOr
|
||||
FilterNot = ldap.FilterNot
|
||||
FilterEqualityMatch = ldap.FilterEqualityMatch
|
||||
FilterSubstrings = ldap.FilterSubstrings
|
||||
FilterGreaterOrEqual = ldap.FilterGreaterOrEqual
|
||||
FilterLessOrEqual = ldap.FilterLessOrEqual
|
||||
FilterPresent = ldap.FilterPresent
|
||||
FilterApproxMatch = ldap.FilterApproxMatch
|
||||
FilterExtensibleMatch = ldap.FilterExtensibleMatch
|
||||
)
|
||||
|
||||
var (
|
||||
FilterMap = ldap.FilterMap
|
||||
casefold = cases.Fold()
|
||||
)
|
||||
|
||||
const (
|
||||
FilterSubstringsInitial = ldap.FilterSubstringsInitial
|
||||
FilterSubstringsAny = ldap.FilterSubstringsAny
|
||||
FilterSubstringsFinal = ldap.FilterSubstringsFinal
|
||||
)
|
||||
|
||||
func CompileFilter(filter string) (*ber.Packet, error) {
|
||||
return ldap.CompileFilter(filter)
|
||||
}
|
||||
|
||||
func DecompileFilter(packet *ber.Packet) (ret string, err error) {
|
||||
return ldap.DecompileFilter(packet)
|
||||
}
|
||||
|
||||
func ServerApplyFilter(f *ber.Packet, entry *ldap.Entry) (bool, LDAPResultCode) {
|
||||
switch FilterMap[uint64(f.Tag)] {
|
||||
default:
|
||||
//log.Fatalf("Unknown LDAP filter code: %d", f.Tag)
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
case "Equality Match":
|
||||
if len(f.Children) != 2 {
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
attribute := f.Children[0].Value.(string)
|
||||
value := f.Children[1].Value.(string)
|
||||
for _, a := range entry.Attributes {
|
||||
if strings.EqualFold(a.Name, attribute) {
|
||||
for _, v := range a.Values {
|
||||
if strings.EqualFold(v, value) {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "Present":
|
||||
for _, a := range entry.Attributes {
|
||||
if strings.EqualFold(a.Name, f.Data.String()) {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
}
|
||||
case "And":
|
||||
for _, child := range f.Children {
|
||||
ok, exitCode := ServerApplyFilter(child, entry)
|
||||
if exitCode != ldap.LDAPResultSuccess {
|
||||
return false, exitCode
|
||||
}
|
||||
if !ok {
|
||||
return false, ldap.LDAPResultSuccess
|
||||
}
|
||||
}
|
||||
return true, ldap.LDAPResultSuccess
|
||||
case "Or":
|
||||
anyOk := false
|
||||
for _, child := range f.Children {
|
||||
ok, exitCode := ServerApplyFilter(child, entry)
|
||||
if exitCode != ldap.LDAPResultSuccess {
|
||||
return false, exitCode
|
||||
} else if ok {
|
||||
anyOk = true
|
||||
}
|
||||
}
|
||||
if anyOk {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
case "Not":
|
||||
if len(f.Children) != 1 {
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
ok, exitCode := ServerApplyFilter(f.Children[0], entry)
|
||||
if exitCode != ldap.LDAPResultSuccess {
|
||||
return false, exitCode
|
||||
} else if !ok {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
case "Substrings":
|
||||
if len(f.Children) != 2 {
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
attribute := f.Children[0].Value.(string)
|
||||
bytes := f.Children[1].Children[0].Data.Bytes()
|
||||
value := casefold.String(string(bytes))
|
||||
for _, a := range entry.Attributes {
|
||||
if strings.EqualFold(a.Name, attribute) {
|
||||
for _, v := range a.Values {
|
||||
v = casefold.String(v)
|
||||
switch f.Children[1].Children[0].Tag {
|
||||
case FilterSubstringsInitial:
|
||||
if strings.HasPrefix(v, value) {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
case FilterSubstringsAny:
|
||||
if strings.Contains(v, value) {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
case FilterSubstringsFinal:
|
||||
if strings.HasSuffix(v, value) {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "FilterGreaterOrEqual": // TODO
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
case "FilterLessOrEqual": // TODO
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
case "FilterApproxMatch": // TODO
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
case "FilterExtensibleMatch": // TODO
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
|
||||
return false, ldap.LDAPResultSuccess
|
||||
}
|
||||
|
||||
func ServerFilterScope(baseDN string, scope int, entry *ldap.Entry) (bool, LDAPResultCode) {
|
||||
// constrained search scope
|
||||
switch scope {
|
||||
case ldap.ScopeWholeSubtree: // The scope is constrained to the entry named by baseObject and to all its subordinates.
|
||||
case ldap.ScopeBaseObject: // The scope is constrained to the entry named by baseObject.
|
||||
if entry.DN != baseDN {
|
||||
return false, ldap.LDAPResultSuccess
|
||||
}
|
||||
case ldap.ScopeSingleLevel: // The scope is constrained to the immediate subordinates of the entry named by baseObject.
|
||||
parts := strings.Split(entry.DN, ",")
|
||||
if len(parts) < 2 && entry.DN != baseDN {
|
||||
return false, ldap.LDAPResultSuccess
|
||||
}
|
||||
if dn := strings.Join(parts[1:], ","); dn != baseDN {
|
||||
return false, ldap.LDAPResultSuccess
|
||||
}
|
||||
}
|
||||
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
|
||||
func ServerFilterAttributes(attributes []string, entry *ldap.Entry) (LDAPResultCode, error) {
|
||||
// attributes
|
||||
if len(attributes) > 1 || (len(attributes) == 1 && len(attributes[0]) > 0) {
|
||||
_, err := filterAttributes(entry, attributes)
|
||||
if err != nil {
|
||||
return ldap.LDAPResultOperationsError, err
|
||||
}
|
||||
}
|
||||
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
type LDAPResultCode uint8
|
||||
|
||||
const (
|
||||
LDAPBindAuthSimple = 0
|
||||
LDAPBindAuthSASL = 3
|
||||
)
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleModifyRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
|
||||
if boundDN == "" {
|
||||
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
|
||||
}
|
||||
modReq, err := parseModifyRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.V(1).Info("Parsed Modification", "request", dumpModRequest(modReq))
|
||||
|
||||
fnNames := []string{}
|
||||
for k := range server.ModifyFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(modReq.DN, fnNames)
|
||||
var modifier Modifier
|
||||
if modifier = server.ModifyFns[fn]; modifier == nil {
|
||||
if fn == "" {
|
||||
err = fmt.Errorf("no suitable handler found for dn: '%s'", modReq.DN)
|
||||
} else {
|
||||
err = fmt.Errorf("handler '%s' does not support modify", fn)
|
||||
}
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
|
||||
}
|
||||
code, err := modifier.Modify(boundDN, modReq, conn)
|
||||
return ldap.NewError(uint16(code), err)
|
||||
}
|
||||
|
||||
func dumpModRequest(mr *ldap.ModifyRequest) string {
|
||||
str := fmt.Sprintf("dn: %s\n", mr.DN)
|
||||
for _, change := range mr.Changes {
|
||||
str += fmt.Sprintf("op: %d\n attr: %s values: %v\n", change.Operation, change.Modification.Type, change.Modification.Vals)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func parseModifyRequest(req *ber.Packet) (*ldap.ModifyRequest, error) {
|
||||
modReq := ldap.ModifyRequest{}
|
||||
// LDAP Modify requests have 2 Elements (DN and AttributeList)
|
||||
if len(req.Children) != 2 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid modify request"))
|
||||
}
|
||||
|
||||
dn, ok := req.Children[0].Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry DN"))
|
||||
}
|
||||
|
||||
_, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
|
||||
}
|
||||
modReq.DN = dn
|
||||
|
||||
ml, err := parseModList(req.Children[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modReq.Changes = ml
|
||||
|
||||
return &modReq, nil
|
||||
}
|
||||
|
||||
func parseModList(req *ber.Packet) ([]ldap.Change, error) {
|
||||
var changes []ldap.Change
|
||||
|
||||
if req.ClassType != ber.ClassUniversal || req.TagType != ber.TypeConstructed || req.Tag != ber.TagSequence {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Changes List"))
|
||||
}
|
||||
|
||||
for _, c := range req.Children {
|
||||
var change ldap.Change
|
||||
switch c.Children[0].Data.Bytes()[0] {
|
||||
default:
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid change operation"))
|
||||
case ldap.AddAttribute:
|
||||
change.Operation = ldap.AddAttribute
|
||||
logger.V(1).Info("op=add")
|
||||
case ldap.ReplaceAttribute:
|
||||
change.Operation = ldap.ReplaceAttribute
|
||||
logger.V(1).Info("op=replace")
|
||||
case ldap.DeleteAttribute:
|
||||
change.Operation = ldap.DeleteAttribute
|
||||
logger.V(1).Info("op=delete")
|
||||
}
|
||||
attr, err := parseAttribute(c.Children[1], true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
change.Modification = ldap.PartialAttribute{Type: attr.Type, Vals: attr.Vals}
|
||||
changes = append(changes, change)
|
||||
|
||||
}
|
||||
return changes, nil
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleModifyDNRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
|
||||
if boundDN == "" {
|
||||
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
|
||||
}
|
||||
|
||||
modDNReq, err := parseModifyDNRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fnNames := []string{}
|
||||
for k := range server.ModifyDNFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(modDNReq.DN, fnNames)
|
||||
var rename Renamer
|
||||
if rename = server.ModifyDNFns[fn]; rename == nil {
|
||||
if fn == "" {
|
||||
err = fmt.Errorf("no suitable handler found for dn: '%s'", modDNReq.DN)
|
||||
} else {
|
||||
err = fmt.Errorf("handler '%s' does not support rename", fn)
|
||||
}
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
|
||||
}
|
||||
code, err := rename.ModifyDN(boundDN, modDNReq, conn)
|
||||
return ldap.NewError(uint16(code), err)
|
||||
return ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid ModifyDN request"))
|
||||
}
|
||||
|
||||
func parseModifyDNRequest(req *ber.Packet) (*ldap.ModifyDNRequest, error) {
|
||||
modDNReq := ldap.ModifyDNRequest{}
|
||||
// LDAP ModifyDN requests have up to 4 Elements (DN, newRDN, deleteOld flag and new superior)
|
||||
if len(req.Children) < 3 || len(req.Children) > 4 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid ModifyDN request"))
|
||||
}
|
||||
|
||||
dn, ok := req.Children[0].Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry DN"))
|
||||
}
|
||||
|
||||
_, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
|
||||
}
|
||||
modDNReq.DN = dn
|
||||
|
||||
newRDN, ok := req.Children[1].Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry new RDN"))
|
||||
}
|
||||
|
||||
_, err = ldap.ParseDN(newRDN)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
|
||||
}
|
||||
modDNReq.NewRDN = newRDN
|
||||
|
||||
removeOld, ok := req.Children[2].Value.(bool)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding 'deleteOldRDN` flag"))
|
||||
}
|
||||
|
||||
modDNReq.DeleteOldRDN = removeOld
|
||||
|
||||
// moving to a new subtree is not yet supported
|
||||
if len(req.Children) == 4 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("moving to 'newSuperior' is not implemented"))
|
||||
}
|
||||
|
||||
return &modDNReq, nil
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
)
|
||||
|
||||
const pwmodOID = "1.3.6.1.4.1.4203.1.11.1"
|
||||
|
||||
const (
|
||||
TagReqIdentity = 0
|
||||
TagReqOldPW = 1
|
||||
TagReqNewPW = 2
|
||||
TagRespGenPW = 0
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterExtendedOperation(pwmodOID, HandlePasswordModifyExOp)
|
||||
}
|
||||
|
||||
func HandlePasswordModifyExOp(req *ber.Packet, boundDN string, server *Server, conn net.Conn) (*ber.Packet, error) {
|
||||
var passwordGenerated bool
|
||||
logger.V(1).Info("HandlePasswordModifyExOp")
|
||||
if boundDN == "" {
|
||||
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("authentication required"))
|
||||
}
|
||||
|
||||
pwReq, err := parsePasswordModifyExop(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If `UserIdentity` is empty, this is a request to update the bound user's own password
|
||||
if pwReq.UserIdentity == "" {
|
||||
pwReq.UserIdentity = boundDN
|
||||
}
|
||||
|
||||
if pwReq.NewPassword == "" {
|
||||
// New password empty means, we're requested to generate a new password
|
||||
var err error
|
||||
if pwReq.NewPassword, err = ldappassword.GenerateRandomPassword(server.GeneratedPasswordLength); err != nil {
|
||||
logger.Error(err, "Failed to generate new password")
|
||||
return nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("Failed to generate new Password"))
|
||||
}
|
||||
passwordGenerated = true
|
||||
}
|
||||
pwReq.NewPassword, err = ldappassword.Hash(pwReq.NewPassword, "{ARGON2}")
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultOperationsError, err)
|
||||
}
|
||||
|
||||
logger.V(1).Info("Modify password extended operation", "dn", pwReq.UserIdentity)
|
||||
|
||||
fnNames := []string{}
|
||||
for k := range server.PasswordExOpFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(pwReq.UserIdentity, fnNames)
|
||||
var pwUpdatefn PasswordUpdater
|
||||
if pwUpdatefn = server.PasswordExOpFns[fn]; pwUpdatefn == nil {
|
||||
if fn == "" {
|
||||
err = fmt.Errorf("no suitable handler found for dn: '%s'", pwReq.UserIdentity)
|
||||
} else {
|
||||
err = fmt.Errorf("handler '%s' does not support add", fn)
|
||||
}
|
||||
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
|
||||
}
|
||||
code, err := pwUpdatefn.ModifyPasswordExop(boundDN, pwReq, conn)
|
||||
if code != ldap.LDAPResultSuccess {
|
||||
return nil, ldap.NewError(uint16(code), err)
|
||||
}
|
||||
var response *ber.Packet
|
||||
if passwordGenerated {
|
||||
response = ber.NewSequence("PasswdModifyResponseValue")
|
||||
response.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, pwReq.NewPassword, "genPasswd"))
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func parsePasswordModifyExop(req *ber.Packet) (*ldap.PasswordModifyRequest, error) {
|
||||
pwReq := ldap.PasswordModifyRequest{}
|
||||
|
||||
// An absent (or empty) body of the request is valid. Translates into: "generate a new password for
|
||||
// for the current user"
|
||||
if req == nil {
|
||||
return &pwReq, nil
|
||||
}
|
||||
|
||||
inner := ber.DecodePacket(req.Data.Bytes())
|
||||
if inner == nil {
|
||||
return &pwReq, nil
|
||||
}
|
||||
|
||||
if len(inner.Children) > 3 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid request"))
|
||||
}
|
||||
|
||||
for _, kid := range inner.Children {
|
||||
if kid.ClassType != ber.ClassContext {
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid request"))
|
||||
}
|
||||
switch kid.Tag {
|
||||
default:
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid request"))
|
||||
case TagReqIdentity:
|
||||
pwReq.UserIdentity = kid.Data.String()
|
||||
case TagReqOldPW:
|
||||
pwReq.OldPassword = kid.Data.String()
|
||||
case TagReqNewPW:
|
||||
pwReq.NewPassword = kid.Data.String()
|
||||
}
|
||||
}
|
||||
return &pwReq, nil
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleSearchRequest(req *ber.Packet, controls *[]ldap.Control, messageID int64, boundDN string, server *Server, conn net.Conn) (doneControls *[]ldap.Control, resultErr error) {
|
||||
searchReq, err := parseSearchRequest(boundDN, req, controls)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultOperationsError, err)
|
||||
}
|
||||
|
||||
var filterPacket *ber.Packet
|
||||
if server.EnforceLDAP {
|
||||
filterPacket, err = ldap.CompileFilter(searchReq.Filter)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultOperationsError, err)
|
||||
}
|
||||
}
|
||||
|
||||
fnNames := []string{}
|
||||
for k := range server.SearchFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(searchReq.BaseDN, fnNames)
|
||||
searchResp, err := server.SearchFns[fn].Search(boundDN, searchReq, conn)
|
||||
if err != nil {
|
||||
return &searchResp.Controls, ldap.NewError(uint16(searchResp.ResultCode), err)
|
||||
}
|
||||
|
||||
if server.EnforceLDAP {
|
||||
if searchReq.DerefAliases != ldap.NeverDerefAliases { // [-a {never|always|search|find}
|
||||
// TODO: Server DerefAliases not supported: RFC4511 4.5.1.3
|
||||
}
|
||||
if searchReq.TimeLimit > 0 {
|
||||
// TODO: Server TimeLimit not implemented
|
||||
}
|
||||
}
|
||||
|
||||
i := 0
|
||||
for _, entry := range searchResp.Entries {
|
||||
if server.EnforceLDAP {
|
||||
// filter
|
||||
keep, resultCode := ServerApplyFilter(filterPacket, entry)
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
return &searchResp.Controls, ldap.NewError(uint16(resultCode), errors.New("ServerApplyFilter error"))
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
|
||||
keep, resultCode = ServerFilterScope(searchReq.BaseDN, searchReq.Scope, entry)
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
return &searchResp.Controls, ldap.NewError(uint16(resultCode), errors.New("ServerApplyScope error"))
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
|
||||
resultCode, err = ServerFilterAttributes(searchReq.Attributes, entry)
|
||||
if err != nil {
|
||||
return &searchResp.Controls, ldap.NewError(uint16(resultCode), err)
|
||||
}
|
||||
|
||||
// size limit
|
||||
if searchReq.SizeLimit > 0 && i >= searchReq.SizeLimit {
|
||||
resultErr = ldap.NewError(
|
||||
ldap.LDAPResultSizeLimitExceeded,
|
||||
errors.New(ldap.LDAPResultCodeMap[ldap.LDAPResultSizeLimitExceeded]),
|
||||
)
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// respond
|
||||
responsePacket := encodeSearchResponse(messageID, searchReq, entry)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
return &searchResp.Controls, ldap.NewError(ldap.LDAPResultOperationsError, err)
|
||||
}
|
||||
}
|
||||
return &searchResp.Controls, resultErr
|
||||
}
|
||||
|
||||
func parseSearchRequest(boundDN string, req *ber.Packet, controls *[]ldap.Control) (*ldap.SearchRequest, error) {
|
||||
if len(req.Children) != 8 {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("Bad search request"))
|
||||
}
|
||||
|
||||
// Parse the request.
|
||||
baseObject, ok := req.Children[0].Value.(string)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
s, ok := req.Children[1].Value.(int64)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
scope := int(s)
|
||||
d, ok := req.Children[2].Value.(int64)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
derefAliases := int(d)
|
||||
s, ok = req.Children[3].Value.(int64)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
sizeLimit := int(s)
|
||||
t, ok := req.Children[4].Value.(int64)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
timeLimit := int(t)
|
||||
typesOnly := false
|
||||
if req.Children[5].Value != nil {
|
||||
typesOnly, ok = req.Children[5].Value.(bool)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
}
|
||||
filter, err := DecompileFilter(req.Children[6])
|
||||
if err != nil {
|
||||
return &ldap.SearchRequest{}, err
|
||||
}
|
||||
attributes := []string{}
|
||||
for _, attr := range req.Children[7].Children {
|
||||
a, ok := attr.Value.(string)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
attributes = append(attributes, a)
|
||||
}
|
||||
searchReq := &ldap.SearchRequest{baseObject, scope,
|
||||
derefAliases, sizeLimit, timeLimit,
|
||||
typesOnly, filter, attributes, *controls}
|
||||
|
||||
return searchReq, nil
|
||||
}
|
||||
|
||||
func filterAttributes(entry *ldap.Entry, attributes []string) (*ldap.Entry, error) {
|
||||
// Only return requested attributes.
|
||||
newAttributes := []*ldap.EntryAttribute{}
|
||||
|
||||
for _, attr := range entry.Attributes {
|
||||
for _, requested := range attributes {
|
||||
if requested == "*" || strings.EqualFold(attr.Name, requested) {
|
||||
newAttributes = append(newAttributes, attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.Attributes = newAttributes
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func encodeSearchResponse(messageID int64, req *ldap.SearchRequest, res *ldap.Entry) *ber.Packet {
|
||||
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
|
||||
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
|
||||
|
||||
searchEntry := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ldap.ApplicationSearchResultEntry, nil, "Search Result Entry")
|
||||
searchEntry.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, res.DN, "Object Name"))
|
||||
|
||||
attrs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes: ")
|
||||
for _, attribute := range res.Attributes {
|
||||
attrs.AppendChild(encodeSearchAttribute(attribute.Name, attribute.Values))
|
||||
}
|
||||
|
||||
searchEntry.AppendChild(attrs)
|
||||
responsePacket.AppendChild(searchEntry)
|
||||
|
||||
return responsePacket
|
||||
}
|
||||
|
||||
func encodeSearchAttribute(name string, values []string) *ber.Packet {
|
||||
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute")
|
||||
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, name, "Attribute Name"))
|
||||
|
||||
valuesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "Attribute Values: ")
|
||||
for _, value := range values {
|
||||
valuesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Attribute Value"))
|
||||
}
|
||||
|
||||
packet.AppendChild(valuesPacket)
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
func encodeSearchDone(messageID int64, ldapResultCode LDAPResultCode, doneControls *[]ldap.Control) *ber.Packet {
|
||||
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
|
||||
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
|
||||
donePacket := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ldap.ApplicationSearchResultDone, nil, "Search result done")
|
||||
donePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: "))
|
||||
donePacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
|
||||
donePacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "errorMessage: "))
|
||||
responsePacket.AppendChild(donePacket)
|
||||
|
||||
if doneControls != nil {
|
||||
contextPacket := ber.Encode(ber.ClassContext, ber.TypeConstructed, ber.TagEOC, nil, "Controls: ")
|
||||
for _, control := range *doneControls {
|
||||
contextPacket.AppendChild(control.Encode())
|
||||
}
|
||||
responsePacket.AppendChild(contextPacket)
|
||||
}
|
||||
// ber.PrintPacket(responsePacket)
|
||||
return responsePacket
|
||||
}
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/go-logr/stdr"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapdn"
|
||||
)
|
||||
|
||||
type Adder interface {
|
||||
Add(boundDN string, req *ldap.AddRequest, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type Binder interface {
|
||||
Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type Deleter interface {
|
||||
Delete(boundDN string, req *ldap.DelRequest, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type Modifier interface {
|
||||
Modify(boundDN string, req *ldap.ModifyRequest, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type PasswordUpdater interface {
|
||||
ModifyPasswordExop(boundDN string, req *ldap.PasswordModifyRequest, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type Renamer interface {
|
||||
ModifyDN(boundDN string, req *ldap.ModifyDNRequest, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type Searcher interface {
|
||||
Search(boundDN string, req *ldap.SearchRequest, conn net.Conn) (ServerSearchResult, error)
|
||||
}
|
||||
|
||||
type Closer interface {
|
||||
Close(boundDN string, conn net.Conn) error
|
||||
}
|
||||
|
||||
var logger logr.Logger = stdr.New(log.Default())
|
||||
|
||||
type Server struct {
|
||||
AddFns map[string]Adder
|
||||
BindFns map[string]Binder
|
||||
DeleteFns map[string]Deleter
|
||||
ModifyFns map[string]Modifier
|
||||
ModifyDNFns map[string]Renamer
|
||||
PasswordExOpFns map[string]PasswordUpdater
|
||||
SearchFns map[string]Searcher
|
||||
CloseFns map[string]Closer
|
||||
Quit chan bool
|
||||
EnforceLDAP bool
|
||||
GeneratedPasswordLength int
|
||||
Stats *Stats
|
||||
}
|
||||
|
||||
type ServerSearchResult struct {
|
||||
Entries []*ldap.Entry
|
||||
Referrals []string
|
||||
Controls []ldap.Control
|
||||
ResultCode LDAPResultCode
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
s := new(Server)
|
||||
s.Quit = make(chan bool)
|
||||
|
||||
d := defaultHandler{}
|
||||
s.AddFns = make(map[string]Adder)
|
||||
s.BindFns = make(map[string]Binder)
|
||||
s.DeleteFns = make(map[string]Deleter)
|
||||
s.ModifyFns = make(map[string]Modifier)
|
||||
s.ModifyDNFns = make(map[string]Renamer)
|
||||
s.PasswordExOpFns = make(map[string]PasswordUpdater)
|
||||
s.SearchFns = make(map[string]Searcher)
|
||||
s.CloseFns = make(map[string]Closer)
|
||||
s.BindFunc("", d)
|
||||
s.SearchFunc("", d)
|
||||
s.CloseFunc("", d)
|
||||
s.GeneratedPasswordLength = 16
|
||||
s.Stats = nil
|
||||
return s
|
||||
}
|
||||
|
||||
func Logger(l logr.Logger) {
|
||||
logger = l
|
||||
}
|
||||
|
||||
func (server *Server) AddFunc(baseDN string, f Adder) {
|
||||
server.AddFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) BindFunc(baseDN string, f Binder) {
|
||||
server.BindFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) DeleteFunc(baseDN string, f Deleter) {
|
||||
server.DeleteFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) ModifyFunc(baseDN string, f Modifier) {
|
||||
server.ModifyFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) ModifyDNFunc(baseDN string, f Renamer) {
|
||||
server.ModifyDNFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) PasswordExOpFunc(baseDN string, f PasswordUpdater) {
|
||||
server.PasswordExOpFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) SearchFunc(baseDN string, f Searcher) {
|
||||
server.SearchFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) CloseFunc(baseDN string, f Closer) {
|
||||
server.CloseFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) QuitChannel(quit chan bool) {
|
||||
server.Quit = quit
|
||||
}
|
||||
|
||||
func (server *Server) ListenAndServeTLS(listenString string, certFile string, keyFile string) error {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
tlsConfig.ServerName = "localhost"
|
||||
ln, err := tls.Listen("tcp", listenString, &tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = server.Serve(ln)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) SetStats(enable bool) {
|
||||
if enable {
|
||||
server.Stats = &Stats{}
|
||||
} else {
|
||||
server.Stats = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) GetStats() Stats {
|
||||
return *server.Stats.Clone()
|
||||
}
|
||||
|
||||
func (server *Server) ListenAndServe(listenString string) error {
|
||||
ln, err := net.Listen("tcp", listenString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = server.Serve(ln)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) Serve(ln net.Listener) error {
|
||||
newConn := make(chan net.Conn)
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
if !strings.HasSuffix(err.Error(), "use of closed network connection") {
|
||||
logger.Error(err, "Error accepting network connection")
|
||||
}
|
||||
break
|
||||
}
|
||||
logger.V(1).Info("New Connection", "addr", ln.Addr())
|
||||
newConn <- conn
|
||||
}
|
||||
}()
|
||||
|
||||
listener:
|
||||
for {
|
||||
select {
|
||||
case c := <-newConn:
|
||||
server.Stats.countConns(1)
|
||||
go server.handleConnection(c)
|
||||
case <-server.Quit:
|
||||
ln.Close()
|
||||
break listener
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
func (server *Server) handleConnection(conn net.Conn) {
|
||||
boundDN := "" // "" == anonymous
|
||||
|
||||
handler:
|
||||
for {
|
||||
// Read incoming LDAP packet.
|
||||
packet, err := ber.ReadPacket(conn)
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF { // Client closed connection.
|
||||
break
|
||||
} else if err != nil {
|
||||
logger.Error(err, "handleConnection ber.ReadPacket")
|
||||
break
|
||||
}
|
||||
|
||||
// Sanity check this packet.
|
||||
if len(packet.Children) < 2 {
|
||||
logger.V(1).Info("len(packet.Children) < 2")
|
||||
break
|
||||
}
|
||||
// Check the message ID and ClassType.
|
||||
messageID, ok := packet.Children[0].Value.(int64)
|
||||
if !ok {
|
||||
logger.V(1).Info("malformed messageID")
|
||||
break
|
||||
}
|
||||
req := packet.Children[1]
|
||||
if req.ClassType != ber.ClassApplication {
|
||||
logger.V(1).Info("req.ClassType != ber.ClassApplication")
|
||||
break
|
||||
}
|
||||
// Handle controls if present.
|
||||
controls := []ldap.Control{}
|
||||
if len(packet.Children) > 2 {
|
||||
for _, child := range packet.Children[2].Children {
|
||||
c, err := ldap.DecodeControl(child)
|
||||
if err != nil {
|
||||
logger.Error(err, "handleConnection decode control")
|
||||
continue
|
||||
}
|
||||
controls = append(controls, c)
|
||||
}
|
||||
}
|
||||
|
||||
// log.Printf("DEBUG: handling operation: %s [%d]", ldap.ApplicationMap[uint8(req.Tag)], req.Tag)
|
||||
// ber.PrintPacket(packet) // DEBUG
|
||||
|
||||
// Dispatch the LDAP operation.
|
||||
switch req.Tag { // LDAP op code.
|
||||
default:
|
||||
op, ok := ldap.ApplicationMap[uint8(req.Tag)]
|
||||
if !ok {
|
||||
op = "unknown"
|
||||
}
|
||||
|
||||
logger.V(1).Info("Unhandled operation", "type", op, "tag", req.Tag)
|
||||
break handler
|
||||
|
||||
case ldap.ApplicationAddRequest:
|
||||
server.Stats.countAdds(1)
|
||||
|
||||
resultCode := uint16(ldap.LDAPResultSuccess)
|
||||
resultMsg := ""
|
||||
if err = HandleAddRequest(req, boundDN, server, conn); err != nil {
|
||||
var lErr *ldap.Error
|
||||
if errors.As(err, &lErr) {
|
||||
resultCode = lErr.ResultCode
|
||||
if lErr.Err != nil {
|
||||
resultMsg = lErr.Err.Error()
|
||||
}
|
||||
} else {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
resultMsg = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationAddResponse, LDAPResultCode(resultCode), resultMsg)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationBindRequest:
|
||||
server.Stats.countBinds(1)
|
||||
ldapResultCode := HandleBindRequest(req, server.BindFns, conn)
|
||||
if ldapResultCode == ldap.LDAPResultSuccess {
|
||||
boundDN, ok = req.Children[1].Value.(string)
|
||||
if !ok {
|
||||
logger.V(1).Info("Malformed Bind DN")
|
||||
break handler
|
||||
}
|
||||
if boundDN, err = ldapdn.ParseNormalize(boundDN); err != nil {
|
||||
logger.V(1).Info("Error normalizing Bind DN", "error", err.Error())
|
||||
break handler
|
||||
}
|
||||
|
||||
}
|
||||
responsePacket := encodeBindResponse(messageID, ldapResultCode)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationCompareRequest:
|
||||
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationCompareRequest, ldap.LDAPResultOperationsError, "Unsupported operation: compare")
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
}
|
||||
logger.V(1).Info("Unhandled operation", "type", ldap.ApplicationMap[uint8(req.Tag)], "tag", req.Tag)
|
||||
break handler
|
||||
|
||||
case ldap.ApplicationDelRequest:
|
||||
server.Stats.countDeletes(1)
|
||||
resultCode := uint16(ldap.LDAPResultSuccess)
|
||||
resultMsg := ""
|
||||
if err = HandleDeleteRequest(req, boundDN, server, conn); err != nil {
|
||||
var lErr *ldap.Error
|
||||
if errors.As(err, &lErr) {
|
||||
resultCode = lErr.ResultCode
|
||||
if lErr.Err != nil {
|
||||
resultMsg = lErr.Err.Error()
|
||||
}
|
||||
} else {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
resultMsg = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationDelResponse, LDAPResultCode(resultCode), resultMsg)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationExtendedRequest:
|
||||
resultCode := uint16(ldap.LDAPResultSuccess)
|
||||
resultMsg := ""
|
||||
var innerBer, responsePacket *ber.Packet
|
||||
if innerBer, err = HandleExtendedRequest(req, boundDN, server, conn); err != nil {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
resultMsg = err.Error()
|
||||
responsePacket = encodeLDAPResponse(messageID, ldap.ApplicationExtendedResponse, LDAPResultCode(resultCode), resultMsg)
|
||||
} else {
|
||||
responsePacket = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
|
||||
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
|
||||
responsePacket.AppendChild(innerBer)
|
||||
}
|
||||
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationModifyDNRequest:
|
||||
server.Stats.countModifyDNs(1)
|
||||
resultCode := uint16(ldap.LDAPResultSuccess)
|
||||
resultMsg := ""
|
||||
if err = HandleModifyDNRequest(req, boundDN, server, conn); err != nil {
|
||||
var lErr *ldap.Error
|
||||
if errors.As(err, &lErr) {
|
||||
resultCode = lErr.ResultCode
|
||||
if lErr.Err != nil {
|
||||
resultMsg = lErr.Err.Error()
|
||||
}
|
||||
} else {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
resultMsg = err.Error()
|
||||
}
|
||||
}
|
||||
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationModifyDNResponse, LDAPResultCode(resultCode), resultMsg)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationModifyRequest:
|
||||
server.Stats.countModifies(1)
|
||||
resultCode := uint16(ldap.LDAPResultSuccess)
|
||||
resultMsg := ""
|
||||
if err = HandleModifyRequest(req, boundDN, server, conn); err != nil {
|
||||
var lErr *ldap.Error
|
||||
if errors.As(err, &lErr) {
|
||||
resultCode = lErr.ResultCode
|
||||
if lErr.Err != nil {
|
||||
resultMsg = lErr.Err.Error()
|
||||
}
|
||||
} else {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
resultMsg = err.Error()
|
||||
}
|
||||
}
|
||||
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationModifyResponse, LDAPResultCode(resultCode), resultMsg)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationSearchRequest:
|
||||
server.Stats.countSearches(1)
|
||||
if doneControls, err := HandleSearchRequest(req, &controls, messageID, boundDN, server, conn); err != nil {
|
||||
// TODO: make this more testable/better err handling - stop using log, stop using breaks?
|
||||
logger.V(1).Info("handleSearchRequest", "error", err.Error())
|
||||
e := err.(*ldap.Error)
|
||||
if err = sendPacket(conn, encodeSearchDone(messageID, LDAPResultCode(e.ResultCode), doneControls)); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
break handler
|
||||
} else {
|
||||
if err = sendPacket(conn, encodeSearchDone(messageID, ldap.LDAPResultSuccess, doneControls)); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
}
|
||||
case ldap.ApplicationUnbindRequest:
|
||||
server.Stats.countUnbinds(1)
|
||||
break handler // Simply disconnect.
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range server.CloseFns {
|
||||
c.Close(boundDN, conn)
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
server.Stats.countConnsClose(1)
|
||||
}
|
||||
|
||||
func sendPacket(conn net.Conn, packet *ber.Packet) error {
|
||||
_, err := conn.Write(packet.Bytes())
|
||||
if err != nil {
|
||||
logger.Error(err, "Error Sending Message")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func routeFunc(dn string, funcNames []string) string {
|
||||
bestPick := ""
|
||||
for _, fn := range funcNames {
|
||||
if strings.HasSuffix(dn, fn) {
|
||||
l := len(strings.Split(bestPick, ","))
|
||||
if bestPick == "" {
|
||||
l = 0
|
||||
}
|
||||
if len(strings.Split(fn, ",")) > l {
|
||||
bestPick = fn
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestPick
|
||||
}
|
||||
|
||||
func encodeLDAPResponse(messageID int64, responseType uint8, ldapResultCode LDAPResultCode, message string) *ber.Packet {
|
||||
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
|
||||
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
|
||||
response := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ber.Tag(responseType), nil, ldap.ApplicationMap[responseType])
|
||||
response.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: "))
|
||||
response.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
|
||||
response.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, message, "errorMessage: "))
|
||||
responsePacket.AppendChild(response)
|
||||
return responsePacket
|
||||
}
|
||||
|
||||
type defaultHandler struct {
|
||||
}
|
||||
|
||||
func (h defaultHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) {
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
func (h defaultHandler) Search(boundDN string, req *ldap.SearchRequest, conn net.Conn) (ServerSearchResult, error) {
|
||||
return ServerSearchResult{make([]*ldap.Entry, 0), []string{}, []ldap.Control{}, ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
func (h defaultHandler) Close(boundDN string, conn net.Conn) error {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
Conns uint64
|
||||
ConnsCurrent uint64
|
||||
ConnsMax uint64
|
||||
Adds uint64
|
||||
Binds uint64
|
||||
Deletes uint64
|
||||
ModifyDNs uint64
|
||||
Modifies uint64
|
||||
Unbinds uint64
|
||||
Searches uint64
|
||||
statsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (stats *Stats) countConns(delta uint64) {
|
||||
if stats != nil {
|
||||
stats.statsMutex.Lock()
|
||||
stats.Conns += delta
|
||||
stats.ConnsCurrent += delta
|
||||
if stats.ConnsCurrent > stats.ConnsMax {
|
||||
stats.ConnsMax = stats.ConnsCurrent
|
||||
}
|
||||
stats.statsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (stats *Stats) countConnsClose(delta uint64) {
|
||||
if stats != nil {
|
||||
stats.statsMutex.Lock()
|
||||
stats.ConnsCurrent -= delta
|
||||
stats.statsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (stats *Stats) countAdds(delta uint64) {
|
||||
if stats != nil {
|
||||
stats.statsMutex.Lock()
|
||||
stats.Adds += delta
|
||||
stats.statsMutex.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
func (stats *Stats) countBinds(delta uint64) {
|
||||
if stats != nil {
|
||||
stats.statsMutex.Lock()
|
||||
stats.Binds += delta
|
||||
stats.statsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (stats *Stats) countDeletes(delta uint64) {
|
||||
if stats != nil {
|
||||
stats.statsMutex.Lock()
|
||||
stats.Deletes += delta
|
||||
stats.statsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (stats *Stats) countModifyDNs(delta uint64) {
|
||||
if stats != nil {
|
||||
stats.statsMutex.Lock()
|
||||
stats.ModifyDNs += delta
|
||||
stats.statsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (stats *Stats) countModifies(delta uint64) {
|
||||
if stats != nil {
|
||||
stats.statsMutex.Lock()
|
||||
stats.Modifies += delta
|
||||
stats.statsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (stats *Stats) countUnbinds(delta uint64) {
|
||||
if stats != nil {
|
||||
stats.statsMutex.Lock()
|
||||
stats.Unbinds += delta
|
||||
stats.statsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (stats *Stats) countSearches(delta uint64) {
|
||||
if stats != nil {
|
||||
stats.statsMutex.Lock()
|
||||
stats.Searches += delta
|
||||
stats.statsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (stats *Stats) Clone() *Stats {
|
||||
var s2 *Stats
|
||||
if stats != nil {
|
||||
s2 = &Stats{}
|
||||
stats.statsMutex.RLock()
|
||||
s2.Conns = stats.Conns
|
||||
s2.ConnsCurrent = stats.ConnsCurrent
|
||||
s2.Adds = stats.Adds
|
||||
s2.Binds = stats.Binds
|
||||
s2.Deletes = stats.Deletes
|
||||
s2.ModifyDNs = stats.ModifyDNs
|
||||
s2.Modifies = stats.Modifies
|
||||
s2.Unbinds = stats.Unbinds
|
||||
s2.Searches = stats.Searches
|
||||
stats.statsMutex.RUnlock()
|
||||
}
|
||||
return s2
|
||||
}
|
||||
+485
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
// Package ldbbolt provides the lower-level Database functions for managing LDAP Entries
|
||||
// in a BoltDB database. Some implementation details:
|
||||
//
|
||||
// The database is currently separated in these three buckets
|
||||
//
|
||||
// - id2entry: This bucket contains the GOB encoded ldap.Entry instances keyed
|
||||
// by a unique 64bit ID
|
||||
//
|
||||
// - dn2id: This bucket is used as an index to lookup the ID of an entry by its DN. The DN
|
||||
// is used in an normalized (case-folded) form here.
|
||||
//
|
||||
// - id2children: This bucket uses the entry-ids as and index and the values contain a list
|
||||
// of the entry ids of its direct childdren
|
||||
//
|
||||
// Additional buckets will likely be added in the future to create efficient search indexes
|
||||
package ldbbolt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapdn"
|
||||
"github.com/libregraph/idm/pkg/ldapentry"
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
)
|
||||
|
||||
type LdbBolt struct {
|
||||
logger logrus.FieldLogger
|
||||
db *bolt.DB
|
||||
options *bolt.Options
|
||||
base string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrEntryAlreadyExists = errors.New("entry already exists")
|
||||
ErrEntryNotFound = errors.New("entry does not exist")
|
||||
ErrNonLeafEntry = errors.New("entry is not a leaf entry")
|
||||
)
|
||||
|
||||
func (bdb *LdbBolt) Configure(logger logrus.FieldLogger, baseDN, dbfile string, options *bolt.Options) error {
|
||||
bdb.logger = logger
|
||||
logger = logger.WithField("db", dbfile)
|
||||
logger.Debug("Open boltdb")
|
||||
db, err := bolt.Open(dbfile, 0o600, options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error opening database")
|
||||
return err
|
||||
}
|
||||
bdb.db = db
|
||||
bdb.options = options
|
||||
bdb.base, _ = ldapdn.ParseNormalize(baseDN)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize() opens the Database file and create the required buckets if they do not
|
||||
// exist yet. After calling initialize the database is ready to process transactions
|
||||
func (bdb *LdbBolt) Initialize() error {
|
||||
var err error
|
||||
logger := bdb.logger.WithField("db", bdb.db.Path())
|
||||
if bdb.options == nil || !bdb.options.ReadOnly {
|
||||
logger.Debug("Adding default buckets")
|
||||
err = bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
_, err = tx.CreateBucketIfNotExists([]byte("dn2id"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create bucket 'dn2id': %w", err)
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte("id2children"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create bucket 'dn2id': %w", err)
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte("id2entry"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create bucket 'id2entry': %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error creating default buckets")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Performs basic LDAP searches, using the dn2id and id2children buckets to generate
|
||||
// a list of Result entries. Currently this does strip of the non-request attribute
|
||||
// Neither does it support LDAP filters. For now we rely on the frontent (LDAPServer)
|
||||
// to both.
|
||||
func (bdb *LdbBolt) Search(base string, scope int) ([]*ldap.Entry, error) {
|
||||
entries := []*ldap.Entry{}
|
||||
nDN, err := ldapdn.ParseNormalize(base)
|
||||
if err != nil {
|
||||
return entries, err
|
||||
}
|
||||
|
||||
err = bdb.db.View(func(tx *bolt.Tx) error {
|
||||
entryID := bdb.getIDByDN(tx, nDN)
|
||||
var entryIDs []uint64
|
||||
if entryID == 0 {
|
||||
return fmt.Errorf("not found")
|
||||
}
|
||||
switch scope {
|
||||
case ldap.ScopeBaseObject:
|
||||
entryIDs = append(entryIDs, entryID)
|
||||
case ldap.ScopeSingleLevel:
|
||||
entryIDs = bdb.getChildrenIDs(tx, entryID)
|
||||
case ldap.ScopeWholeSubtree:
|
||||
entryIDs = append(entryIDs, entryID)
|
||||
entryIDs = append(entryIDs, bdb.getSubtreeIDs(tx, entryID)...)
|
||||
}
|
||||
for _, id := range entryIDs {
|
||||
entry, err := bdb.getEntryByID(tx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return entries, err
|
||||
}
|
||||
|
||||
func idToBytes(id uint64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, id)
|
||||
return b
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) getChildrenIDs(tx *bolt.Tx, parent uint64) []uint64 {
|
||||
id2Children := tx.Bucket([]byte("id2children"))
|
||||
children := id2Children.Get(idToBytes(parent))
|
||||
r := bytes.NewReader(children)
|
||||
ids := make([]uint64, len(children)/8)
|
||||
if err := binary.Read(r, binary.LittleEndian, &ids); err != nil {
|
||||
bdb.logger.Error(err)
|
||||
}
|
||||
// This logging it too verbose even for the "debug" level. Leaving
|
||||
// it here commented out as it can be helpful during development.
|
||||
// bdb.logger.WithFields(logrus.Fields{
|
||||
// "parentid": parent,
|
||||
// "children": ids,
|
||||
// }).Debug("getChildrenIDs")
|
||||
return ids
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) getSubtreeIDs(tx *bolt.Tx, root uint64) []uint64 {
|
||||
var res []uint64
|
||||
children := bdb.getChildrenIDs(tx, root)
|
||||
res = append(res, children...)
|
||||
for _, child := range children {
|
||||
res = append(res, bdb.getSubtreeIDs(tx, child)...)
|
||||
}
|
||||
// This logging it too verbose even for the "debug" level. Leaving
|
||||
// it here commented out as it can be helpful during development.
|
||||
// bdb.logger.WithFields(logrus.Fields{
|
||||
// "rootid": root,
|
||||
// "subtree": res,
|
||||
// }).Debug("getSubtreeIDs")
|
||||
return res
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) EntryPut(e *ldap.Entry) error {
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
if err := enc.Encode(e); err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dn, _ := ldap.ParseDN(e.DN)
|
||||
parentDN := &ldap.DN{
|
||||
RDNs: dn.RDNs[1:],
|
||||
}
|
||||
nDN := ldapdn.Normalize(dn)
|
||||
|
||||
if !strings.HasSuffix(nDN, bdb.base) {
|
||||
return fmt.Errorf("'%s' is not a descendant of '%s'", e.DN, bdb.base)
|
||||
}
|
||||
|
||||
nParentDN := ldapdn.Normalize(parentDN)
|
||||
err := bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
id2entry := tx.Bucket([]byte("id2entry"))
|
||||
id := bdb.getIDByDN(tx, nDN)
|
||||
if id != 0 {
|
||||
return ErrEntryAlreadyExists
|
||||
}
|
||||
var err error
|
||||
if id, err = id2entry.NextSequence(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := id2entry.Put(idToBytes(id), buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
if nDN != bdb.base {
|
||||
if err := bdb.addID2Children(tx, nParentDN, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
dn2id := tx.Bucket([]byte("dn2id"))
|
||||
if err := dn2id.Put([]byte(nDN), idToBytes(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) EntryDelete(dn string) error {
|
||||
parsed, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pparentDN := &ldap.DN{
|
||||
RDNs: parsed.RDNs[1:],
|
||||
}
|
||||
pdn := ldapdn.Normalize(pparentDN)
|
||||
|
||||
ndn := ldapdn.Normalize(parsed)
|
||||
err = bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
// Does this entry even exist?
|
||||
entryID := bdb.getIDByDN(tx, ndn)
|
||||
if entryID == 0 {
|
||||
return ErrEntryNotFound
|
||||
}
|
||||
|
||||
// Refuse to delete if the entry has childs
|
||||
id2Children := tx.Bucket([]byte("id2children"))
|
||||
children := id2Children.Get(idToBytes(entryID))
|
||||
if len(children) != 0 {
|
||||
return ErrNonLeafEntry
|
||||
}
|
||||
|
||||
// Update id2children bucket (remove entryid from parent)
|
||||
parentid := bdb.getIDByDN(tx, pdn)
|
||||
if parentid == 0 {
|
||||
return ErrEntryNotFound
|
||||
}
|
||||
children = id2Children.Get(idToBytes(parentid))
|
||||
r := bytes.NewReader(children)
|
||||
var newids []byte
|
||||
idBytes := make([]byte, 8)
|
||||
for _, err = io.ReadFull(r, idBytes); err == nil; _, err = io.ReadFull(r, idBytes) {
|
||||
if entryID != binary.LittleEndian.Uint64(idBytes) {
|
||||
newids = append(newids, idBytes...)
|
||||
}
|
||||
}
|
||||
if err = id2Children.Put(idToBytes(parentid), newids); err != nil {
|
||||
return fmt.Errorf("error updating id2Children index for %d: %w", parentid, err)
|
||||
}
|
||||
|
||||
// Remove entry from dn2id bucket
|
||||
dn2id := tx.Bucket([]byte("dn2id"))
|
||||
err = dn2id.Delete([]byte(ndn))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id2entry := tx.Bucket([]byte("id2entry"))
|
||||
err = id2entry.Delete(idToBytes(entryID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) EntryModify(req *ldap.ModifyRequest) error {
|
||||
ndn, err := ldapdn.ParseNormalize(req.DN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
oldEntry, id, innerErr := bdb.getEntryByDN(tx, ndn)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
return bdb.entryModifyWithTxn(tx, id, oldEntry, req)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) entryModifyWithTxn(tx *bolt.Tx, id uint64, entry *ldap.Entry, req *ldap.ModifyRequest) error {
|
||||
newEntry, innerErr := ldapentry.ApplyModify(entry, req)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
if innerErr := enc.Encode(newEntry); innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
id2entry := tx.Bucket([]byte("id2entry"))
|
||||
if innerErr := id2entry.Put(idToBytes(id), buf.Bytes()); innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) EntryModifyDN(req *ldap.ModifyDNRequest) error {
|
||||
olddn, err := ldap.ParseDN(req.DN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newrdn, err := ldap.ParseDN(req.NewRDN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var newDN ldap.DN
|
||||
|
||||
newDN.RDNs = []*ldap.RelativeDN{newrdn.RDNs[0]}
|
||||
newDN.RDNs = append(newDN.RDNs, olddn.RDNs[1:]...)
|
||||
|
||||
err = bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
flatNewDN := ldapdn.Normalize(&newDN)
|
||||
flatOldDN := ldapdn.Normalize(olddn)
|
||||
|
||||
// error out if there is an entry with the new name already
|
||||
if id := bdb.getIDByDN(tx, flatNewDN); id != 0 {
|
||||
return ErrEntryAlreadyExists
|
||||
}
|
||||
|
||||
entry, id, innerErr := bdb.getEntryByDN(tx, flatOldDN)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
|
||||
// only allow renaming leaf entries
|
||||
childIds := bdb.getChildrenIDs(tx, id)
|
||||
if len(childIds) > 0 {
|
||||
return ErrNonLeafEntry
|
||||
}
|
||||
|
||||
entry.DN = flatNewDN
|
||||
|
||||
modReq := ldap.ModifyRequest{
|
||||
DN: entry.DN,
|
||||
}
|
||||
|
||||
// create modify operation for the change attribute values
|
||||
if req.DeleteOldRDN {
|
||||
oldRDN := olddn.RDNs[0]
|
||||
for _, ava := range oldRDN.Attributes {
|
||||
modReq.Delete(ava.Type, []string{ava.Value})
|
||||
}
|
||||
}
|
||||
for _, ava := range newrdn.RDNs[0].Attributes {
|
||||
modReq.Add(ava.Type, []string{ava.Value})
|
||||
}
|
||||
innerErr = bdb.entryModifyWithTxn(tx, id, entry, &modReq)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
|
||||
// update the dn2id index
|
||||
dn2id := tx.Bucket([]byte("dn2id"))
|
||||
if err := dn2id.Put([]byte(flatNewDN), idToBytes(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dn2id.Delete([]byte(flatOldDN)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) UpdatePassword(req *ldap.PasswordModifyRequest) error {
|
||||
ndn, err := ldapdn.ParseNormalize(req.UserIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
userEntry, id, innerErr := bdb.getEntryByDN(tx, ndn)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
// Note: the password check we perform here is more or less unneeded.
|
||||
// If the request got here it's either issued by the admin (which does
|
||||
// not need the old password to reset a users password) or a user trying
|
||||
// to update its own password. In which case the password is already verified
|
||||
// as we only allow authenticated users to issue this request. Still, if
|
||||
// the request contains an old password we verify it and error out if it
|
||||
// doesn't match.
|
||||
if req.OldPassword != "" {
|
||||
userPassword := userEntry.GetEqualFoldAttributeValue("userPassword")
|
||||
match, err := ldappassword.Validate(req.OldPassword, userPassword)
|
||||
if err != nil {
|
||||
bdb.logger.Error(err)
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("Failed to validate old Password"))
|
||||
}
|
||||
if !match {
|
||||
bdb.logger.Debug("Old password does not match")
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("Failed to validate old Password"))
|
||||
}
|
||||
}
|
||||
|
||||
mod := ldap.ModifyRequest{}
|
||||
mod.DN = req.UserIdentity
|
||||
mod.Replace("userPassword", []string{req.NewPassword})
|
||||
innerErr = bdb.entryModifyWithTxn(tx, id, userEntry, &mod)
|
||||
if innerErr != nil {
|
||||
bdb.logger.Debugf("Failed to update password for '%s': '%s'", ndn, err)
|
||||
return ldap.NewError(ldap.LDAPResultOperationsError, errors.New("Failed to update Password"))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) addID2Children(tx *bolt.Tx, nParentDN string, newChildID uint64) error {
|
||||
bdb.logger.Debugf("AddID2Children '%s' id '%d'", nParentDN, newChildID)
|
||||
parentID := bdb.getIDByDN(tx, nParentDN)
|
||||
if parentID == 0 {
|
||||
return fmt.Errorf("parent not found '%s'", nParentDN)
|
||||
}
|
||||
|
||||
bdb.logger.Debugf("Parent ID: %v", parentID)
|
||||
|
||||
id2Children := tx.Bucket([]byte("id2children"))
|
||||
// FIXME add sanity check here if ID is already present
|
||||
children := id2Children.Get(idToBytes(parentID))
|
||||
children = append(children, idToBytes(newChildID)...)
|
||||
if err := id2Children.Put(idToBytes(parentID), children); err != nil {
|
||||
return fmt.Errorf("error updating id2Children index for %d: %w", parentID, err)
|
||||
}
|
||||
|
||||
bdb.logger.Debugf("AddID2Children '%d' id '%v'", parentID, children)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) getIDByDN(tx *bolt.Tx, nDN string) uint64 {
|
||||
dn2id := tx.Bucket([]byte("dn2id"))
|
||||
if dn2id == nil {
|
||||
bdb.logger.Debugf("Bucket 'dn2id' does not exist")
|
||||
return 0
|
||||
}
|
||||
id := dn2id.Get([]byte(nDN))
|
||||
if id == nil {
|
||||
bdb.logger.Debugf("DN: '%s' not found", nDN)
|
||||
return 0
|
||||
}
|
||||
return binary.LittleEndian.Uint64(id)
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) getEntryByID(tx *bolt.Tx, id uint64) (entry *ldap.Entry, err error) {
|
||||
id2entry := tx.Bucket([]byte("id2entry"))
|
||||
entrybytes := id2entry.Get(idToBytes(id))
|
||||
buf := bytes.NewBuffer(entrybytes)
|
||||
dec := gob.NewDecoder(buf)
|
||||
if err := dec.Decode(&entry); err != nil {
|
||||
return nil, fmt.Errorf("error decoding entry id: %d, %w", id, err)
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) getEntryByDN(tx *bolt.Tx, ndn string) (entry *ldap.Entry, id uint64, err error) {
|
||||
id = bdb.getIDByDN(tx, ndn)
|
||||
if id == 0 {
|
||||
return nil, id, ErrEntryNotFound
|
||||
}
|
||||
entry, err = bdb.getEntryByID(tx, id)
|
||||
return entry, id, err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) Close() {
|
||||
bdb.db.Close()
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Config bundles server configuration settings.
|
||||
type Config struct {
|
||||
Logger logrus.FieldLogger
|
||||
|
||||
LDAPHandler string
|
||||
|
||||
LDAPListenAddr string
|
||||
LDAPSListenAddr string
|
||||
|
||||
TLSCertFile string
|
||||
TLSKeyFile string
|
||||
|
||||
LDAPBaseDN string
|
||||
LDAPAdminDN string
|
||||
|
||||
LDAPAllowLocalAnonymousBind bool
|
||||
|
||||
BoltDBFile string
|
||||
|
||||
LDIFMain string
|
||||
LDIFConfig string
|
||||
|
||||
LDIFDefaultCompany string
|
||||
LDIFDefaultMailDomain string
|
||||
LDIFTemplateExtraVars map[string]interface{}
|
||||
|
||||
Metrics prometheus.Registerer
|
||||
|
||||
OnReady func(*Server)
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapdn"
|
||||
"github.com/libregraph/idm/pkg/ldapentry"
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/pkg/ldbbolt"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
)
|
||||
|
||||
type boltdbHandler struct {
|
||||
logger logrus.FieldLogger
|
||||
dbfile string
|
||||
baseDN string
|
||||
adminDN string
|
||||
allowLocalAnonymousBind bool
|
||||
ctx context.Context
|
||||
bdb *ldbbolt.LdbBolt
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
BaseDN string
|
||||
AdminDN string
|
||||
|
||||
AllowLocalAnonymousBind bool
|
||||
}
|
||||
|
||||
func NewBoltDBHandler(logger logrus.FieldLogger, fn string, options *Options) (handler.Handler, error) {
|
||||
if fn == "" {
|
||||
return nil, fmt.Errorf("file name is empty")
|
||||
}
|
||||
if options.BaseDN == "" {
|
||||
return nil, fmt.Errorf("base dn is empty")
|
||||
}
|
||||
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h := &boltdbHandler{
|
||||
logger: logger,
|
||||
dbfile: fn,
|
||||
|
||||
allowLocalAnonymousBind: options.AllowLocalAnonymousBind,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
if h.baseDN, err = ldapdn.ParseNormalize(options.BaseDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h.adminDN, err = ldapdn.ParseNormalize(options.AdminDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = h.setup()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) setup() error {
|
||||
bdb := &ldbbolt.LdbBolt{}
|
||||
|
||||
if err := bdb.Configure(h.logger, h.baseDN, h.dbfile, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bdb.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
h.bdb = bdb
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Add(boundDN string, req *ldap.AddRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "add",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
e := ldapentry.EntryFromAddRequest(req)
|
||||
|
||||
if err := h.bdb.EntryPut(e); err != nil {
|
||||
logger.WithError(err).WithField("entrydn", e.DN).Debugln("ldap add failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "bind",
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
// Handle anoymous bind
|
||||
if bindDN == "" {
|
||||
if !h.allowLocalAnonymousBind {
|
||||
logger.Debugln("ldap anonymous Bind disabled")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
} else if bindSimplePw == "" {
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
}
|
||||
|
||||
bindDN, err := ldapdn.ParseNormalize(bindDN)
|
||||
if err != nil {
|
||||
logger.WithError(err).Debugln("ldap bind request BindDN validation failed")
|
||||
return ldap.LDAPResultInvalidDNSyntax, nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(bindDN, h.baseDN) {
|
||||
logger.WithError(err).Debugln("ldap bind request BindDN outside of Database tree")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
// Disallow empty password
|
||||
if bindSimplePw == "" {
|
||||
logger.Debugf("BindDN without password")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
return h.validatePassword(logger, bindDN, bindSimplePw)
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) validatePassword(logger logrus.FieldLogger, bindDN, bindSimplePw string) (ldapserver.LDAPResultCode, error) {
|
||||
// Lookup Bind DN in database
|
||||
entries, err := h.bdb.Search(bindDN, ldap.ScopeBaseObject)
|
||||
if err != nil || len(entries) != 1 {
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
logger.Debugf("Entry '%s' does not exist", bindDN)
|
||||
}
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
userPassword := entries[0].GetEqualFoldAttributeValue("userPassword")
|
||||
match, err := ldappassword.Validate(bindSimplePw, userPassword)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
if match {
|
||||
logger.Debug("success")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Delete(boundDN string, req *ldap.DelRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "delete",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
logger.Debug("Calling boltdb delete")
|
||||
if err := h.bdb.EntryDelete(req.DN); err != nil {
|
||||
logger.WithError(err).WithField("entrydn", req.DN).Debugln("ldap delete failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
logger.Debug("delete succeeded")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Modify(boundDN string, req *ldap.ModifyRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "modify",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
"entrydn": req.DN,
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
logger.Debug("Calling boltdb modify")
|
||||
if err := h.bdb.EntryModify(req); err != nil {
|
||||
logger.WithError(err).Debug("ldap modify failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
ldapError, ok := err.(*ldap.Error)
|
||||
if !ok {
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
return ldapserver.LDAPResultCode(ldapError.ResultCode), ldapError.Err
|
||||
}
|
||||
logger.Debug("modify succeeded")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) ModifyDN(boundDN string, req *ldap.ModifyDNRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "modifyDN",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
"entrydn": req.DN,
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
logger.Debug("Calling boltdb modify DN")
|
||||
if err := h.bdb.EntryModifyDN(req); err != nil {
|
||||
logger.WithError(err).Debug("ldap modifyDN failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
ldapError, ok := err.(*ldap.Error)
|
||||
if !ok {
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
return ldapserver.LDAPResultCode(ldapError.ResultCode), ldapError.Err
|
||||
}
|
||||
logger.Debug("modify DN succeeded")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) ModifyPasswordExop(boundDN string, req *ldap.PasswordModifyRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "modpw_exop",
|
||||
"binddn": boundDN,
|
||||
"UserIdentity": req.UserIdentity,
|
||||
"OldPWPresent": req.OldPassword != "",
|
||||
"NewPwPresent": req.NewPassword != "",
|
||||
})
|
||||
|
||||
if boundDN == "" {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
if req.UserIdentity != "" {
|
||||
pwUserDN, err := ldapdn.ParseNormalize(req.UserIdentity)
|
||||
if err != nil {
|
||||
return ldap.LDAPResultOperationsError, errors.New("Error parsing DN in password modify extended operation.")
|
||||
}
|
||||
if boundDN != pwUserDN && boundDN != h.adminDN {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Calling boltdb UpdatePassword")
|
||||
err := h.bdb.UpdatePassword(req)
|
||||
if err != nil {
|
||||
logger.Debugf("boltdb UpdatePassword returned '%s'", err)
|
||||
return ldap.LDAPResultOther, err
|
||||
}
|
||||
return ldap.LDAPResultSuccess, err
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Search(boundDN string, req *ldap.SearchRequest, conn net.Conn) (ldapserver.ServerSearchResult, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "search",
|
||||
"binddn": boundDN,
|
||||
"basedn": req.BaseDN,
|
||||
"filter": req.Filter,
|
||||
"attrs": req.Attributes,
|
||||
})
|
||||
|
||||
logger.Debug("Calling boltdb search")
|
||||
entries, _ := h.bdb.Search(req.BaseDN, req.Scope)
|
||||
logger.Debugf("boltdb search returned %d entries", len(entries))
|
||||
|
||||
return ldapserver.ServerSearchResult{
|
||||
Entries: entries,
|
||||
Referrals: []string{},
|
||||
Controls: []ldap.Control{},
|
||||
ResultCode: ldap.LDAPResultSuccess,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Close(boundDN string, conn net.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) WithContext(ctx context.Context) handler.Handler {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
|
||||
h2 := new(boltdbHandler)
|
||||
*h2 = *h
|
||||
h2.ctx = ctx
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Reload(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) writeAllowed(boundDN string) bool {
|
||||
if h.adminDN != "" && h.adminDN == boundDN {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
)
|
||||
|
||||
// Interface for handlers.
|
||||
type Handler interface {
|
||||
ldapserver.Adder
|
||||
ldapserver.Binder
|
||||
ldapserver.Deleter
|
||||
ldapserver.Modifier
|
||||
ldapserver.PasswordUpdater
|
||||
ldapserver.Renamer
|
||||
ldapserver.Searcher
|
||||
ldapserver.Closer
|
||||
|
||||
WithContext(context.Context) Handler
|
||||
Reload(context.Context) error
|
||||
}
|
||||
|
||||
// Interface for middlewares.
|
||||
type Middleware interface {
|
||||
WithHandler(next Handler) Handler
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
)
|
||||
|
||||
type ldifEntry struct {
|
||||
*ldap.Entry
|
||||
|
||||
UserPassword *ldap.EntryAttribute
|
||||
}
|
||||
|
||||
func (entry *ldifEntry) validatePassword(bindSimplePw string) error {
|
||||
match, err := ldappassword.Validate(bindSimplePw, entry.UserPassword.Values[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !match {
|
||||
return fmt.Errorf("password mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-asn1-ber/asn1-ber"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
)
|
||||
|
||||
func parseFilterToIndexFilter(filter string) ([][]string, error) {
|
||||
f, err := ldapserver.CompileFilter(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result [][]string
|
||||
matches, err := parseFilterMatchLeavesForIndex(f, nil, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse filter for index failed: %w", err)
|
||||
} else {
|
||||
withObjectClass := true
|
||||
if len(matches) > 1 {
|
||||
// NOTE(longsleep): In case of more than one filter, we remove the
|
||||
// index lookup for objectClass since pretty much everything has
|
||||
// an object class anyways and our index lookup does not support
|
||||
// "and" lookups. Thus this gains a lot of efficiency.
|
||||
for _, f := range matches {
|
||||
if !strings.EqualFold("objectClass", f[1]) {
|
||||
withObjectClass = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range matches {
|
||||
if !withObjectClass && strings.EqualFold("objectClass", f[1]) {
|
||||
continue
|
||||
}
|
||||
result = append(result, f[1:])
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func parseFilterMatchLeavesForIndex(f *ber.Packet, parent [][]string, level string) ([][]string, error) {
|
||||
var err error
|
||||
|
||||
if parent == nil {
|
||||
parent = make([][]string, 0)
|
||||
}
|
||||
|
||||
switch f.Tag {
|
||||
case ldapserver.FilterEqualityMatch:
|
||||
if len(f.Children) != 2 {
|
||||
return nil, errors.New("unsupported number of children in equality match filter")
|
||||
}
|
||||
attribute := f.Children[0].Value.(string)
|
||||
value := f.Children[1].Value.(string)
|
||||
parent = append(parent, []string{level, attribute, "eq", value})
|
||||
|
||||
case ldapserver.FilterPresent:
|
||||
if len(f.Children) != 0 {
|
||||
return nil, errors.New("unsupported number of children in presence match filter")
|
||||
}
|
||||
attribute := f.Data.String()
|
||||
parent = append(parent, []string{level, attribute, "pres", ""})
|
||||
|
||||
case ldapserver.FilterSubstrings:
|
||||
if len(f.Children) != 2 {
|
||||
return nil, errors.New("unsupported number of children in substrings filter")
|
||||
}
|
||||
attribute := f.Children[0].Value.(string)
|
||||
if len(f.Children[1].Children) != 1 {
|
||||
return nil, errors.New("unsupported number of children in substrings filter")
|
||||
}
|
||||
value := f.Children[1].Children[0].Value.(string)
|
||||
switch f.Children[1].Children[0].Tag {
|
||||
case ldapserver.FilterSubstringsInitial, ldapserver.FilterSubstringsAny, ldapserver.FilterSubstringsFinal:
|
||||
parent = append(parent, []string{level, attribute, "sub", value, strconv.FormatInt(int64(f.Children[1].Children[0].Tag), 10)})
|
||||
}
|
||||
|
||||
case ldapserver.FilterAnd:
|
||||
for idx, child := range f.Children {
|
||||
parent, err = parseFilterMatchLeavesForIndex(child, parent, fmt.Sprintf("%s.&%d", level, idx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case ldapserver.FilterOr:
|
||||
for idx, child := range f.Children {
|
||||
parent, err = parseFilterMatchLeavesForIndex(child, parent, fmt.Sprintf("%s.|%d", level, idx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case ldapserver.FilterNot:
|
||||
// Ignored for now.
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
return parent, nil
|
||||
}
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/go-ldap/ldif"
|
||||
"github.com/longsleep/rndm"
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapdn"
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
)
|
||||
|
||||
type ldifHandler struct {
|
||||
logger logrus.FieldLogger
|
||||
fn string
|
||||
options *Options
|
||||
|
||||
baseDN string
|
||||
adminDN string
|
||||
allowLocalAnonymousBind bool
|
||||
|
||||
ctx context.Context
|
||||
|
||||
current atomic.Value
|
||||
|
||||
activeSearchPagings cmap.ConcurrentMap
|
||||
}
|
||||
|
||||
var _ handler.Handler = (*ldifHandler)(nil) // Verify that *ldifHandler implements handler.Handler.
|
||||
|
||||
func NewLDIFHandler(logger logrus.FieldLogger, fn string, options *Options) (handler.Handler, error) {
|
||||
if fn == "" {
|
||||
return nil, fmt.Errorf("file name is empty")
|
||||
}
|
||||
if options.BaseDN == "" {
|
||||
return nil, fmt.Errorf("base dn is empty")
|
||||
}
|
||||
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger = logger.WithField("fn", fn)
|
||||
|
||||
h := &ldifHandler{
|
||||
logger: logger,
|
||||
fn: fn,
|
||||
options: options,
|
||||
|
||||
allowLocalAnonymousBind: options.AllowLocalAnonymousBind,
|
||||
|
||||
ctx: context.Background(),
|
||||
|
||||
activeSearchPagings: cmap.New(),
|
||||
}
|
||||
if h.baseDN, err = ldapdn.ParseNormalize(options.BaseDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h.adminDN, err = ldapdn.ParseNormalize(options.AdminDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = h.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) open() error {
|
||||
if !strings.EqualFold(h.options.BaseDN, h.baseDN) {
|
||||
return fmt.Errorf("mismatched BaseDN")
|
||||
}
|
||||
|
||||
info, err := os.Stat(h.fn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open LDIF: %w", err)
|
||||
}
|
||||
|
||||
var l *ldif.LDIF
|
||||
index := newIndexMapRegister()
|
||||
|
||||
if info.IsDir() {
|
||||
h.logger.Debugln("loading LDIF files from folder")
|
||||
h.options.templateBasePath = h.fn
|
||||
var parseErrors []error
|
||||
l, parseErrors, err = parseLDIFDirectory(h.fn, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(parseErrors) > 0 {
|
||||
for _, parseErr := range parseErrors {
|
||||
h.logger.WithError(parseErr).Errorln("LDIF error")
|
||||
}
|
||||
return fmt.Errorf("error in LDIF files")
|
||||
}
|
||||
} else {
|
||||
h.logger.Debugln("loading LDIF")
|
||||
h.options.templateBasePath = filepath.Dir(h.fn)
|
||||
l, err = parseLDIFFile(h.fn, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
t, err := treeFromLDIF(l, index, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store parsed data as memory value.
|
||||
value := &ldifMemoryValue{
|
||||
l: l,
|
||||
t: t,
|
||||
|
||||
index: index,
|
||||
}
|
||||
h.current.Store(value)
|
||||
|
||||
h.logger.WithFields(logrus.Fields{
|
||||
"version": l.Version,
|
||||
"entries_count": len(l.Entries),
|
||||
"tree_length": t.Len(),
|
||||
"base_dn": h.options.BaseDN,
|
||||
"indexes": len(index),
|
||||
}).Debugln("loaded LDIF")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) load() *ldifMemoryValue {
|
||||
value := h.current.Load()
|
||||
return value.(*ldifMemoryValue)
|
||||
}
|
||||
|
||||
func (h *ldifHandler) WithContext(ctx context.Context) handler.Handler {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
|
||||
h2 := new(ldifHandler)
|
||||
*h2 = *h
|
||||
h2.ctx = ctx
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Reload(ctx context.Context) error {
|
||||
return h.open()
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Add(_ string, _ *ldap.AddRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if err := h.validateBindDN(bindDN, conn); err != nil {
|
||||
logger.WithError(err).Debugln("ldap bind request BindDN validation failed")
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
if bindSimplePw == "" {
|
||||
logger.Debugf("ldap anonymous bind request")
|
||||
if bindDN == "" {
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
} else {
|
||||
return ldap.LDAPResultUnwillingToPerform, nil
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("ldap bind request")
|
||||
}
|
||||
|
||||
current := h.load()
|
||||
|
||||
entryRecord, found := current.t.Get([]byte(bindDN))
|
||||
if !found {
|
||||
err := fmt.Errorf("user not found")
|
||||
logger.WithError(err).Debugf("ldap bind error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
entry := entryRecord.(*ldifEntry)
|
||||
|
||||
if err := entry.validatePassword(bindSimplePw); err != nil {
|
||||
logger.WithError(err).Debugf("ldap bind credentials error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Delete(_ string, _ *ldap.DelRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Modify(_ string, _ *ldap.ModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) ModifyDN(_ string, _ *ldap.ModifyDNRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) ModifyPasswordExop(_ string, _ *ldap.PasswordModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Search(bindDN string, searchReq *ldap.SearchRequest, conn net.Conn) (ldapserver.ServerSearchResult, error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
searchBaseDN := strings.ToLower(searchReq.BaseDN)
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"search_base_dn": searchBaseDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
"controls": searchReq.Controls,
|
||||
"size_limit": searchReq.SizeLimit,
|
||||
})
|
||||
|
||||
logger.Debugf("ldap search request for %s", searchReq.Filter)
|
||||
|
||||
if err := h.validateBindDN(bindDN, conn); err != nil {
|
||||
logger.WithError(err).Debugln("ldap search request BindDN validation failed")
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: ldap.LDAPResultInsufficientAccessRights,
|
||||
}, err
|
||||
}
|
||||
|
||||
indexFilter, _ := parseFilterToIndexFilter(searchReq.Filter)
|
||||
|
||||
if !strings.HasSuffix(searchBaseDN, h.baseDN) {
|
||||
err := fmt.Errorf("ldap search BaseDN is not in our BaseDN %s", h.baseDN)
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: ldap.LDAPResultInsufficientAccessRights,
|
||||
}, err
|
||||
}
|
||||
|
||||
doneControls := []ldap.Control{}
|
||||
var pagingControl *ldap.ControlPaging
|
||||
var pagingCookie []byte
|
||||
if paging := ldap.FindControl(searchReq.Controls, ldap.ControlTypePaging); paging != nil {
|
||||
pagingControl = paging.(*ldap.ControlPaging)
|
||||
if searchReq.SizeLimit > 0 && pagingControl.PagingSize >= uint32(searchReq.SizeLimit) {
|
||||
pagingControl = nil
|
||||
} else {
|
||||
pagingCookie = pagingControl.Cookie
|
||||
}
|
||||
}
|
||||
|
||||
pumpCh, resultCode := func() (<-chan *ldifEntry, ldapserver.LDAPResultCode) {
|
||||
var pumpCh chan *ldifEntry
|
||||
var start = true
|
||||
if pagingControl != nil {
|
||||
if len(pagingCookie) == 0 {
|
||||
pagingCookie = []byte(base64.RawStdEncoding.EncodeToString(rndm.GenerateRandomBytes(8)))
|
||||
pagingControl.Cookie = pagingCookie
|
||||
pumpCh = make(chan *ldifEntry)
|
||||
h.activeSearchPagings.Set(string(pagingControl.Cookie), pumpCh)
|
||||
logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump start")
|
||||
} else {
|
||||
pumpChRecord, ok := h.activeSearchPagings.Get(string(pagingControl.Cookie))
|
||||
if !ok {
|
||||
return nil, ldap.LDAPResultUnwillingToPerform
|
||||
}
|
||||
if pagingControl.PagingSize > 0 {
|
||||
logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump continue")
|
||||
pumpCh = pumpChRecord.(chan *ldifEntry)
|
||||
start = false
|
||||
} else {
|
||||
// No paging size with cookie, means abandon.
|
||||
start = false
|
||||
logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("search paging pump abandon")
|
||||
// TODO(longsleep): Cancel paging pump context.
|
||||
h.activeSearchPagings.Remove(string(pagingControl.Cookie))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pumpCh = make(chan *ldifEntry)
|
||||
}
|
||||
if start {
|
||||
current := h.load()
|
||||
go h.searchEntriesPump(h.ctx, current, pumpCh, searchReq, pagingControl, indexFilter)
|
||||
}
|
||||
|
||||
return pumpCh, ldap.LDAPResultSuccess
|
||||
}()
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
err := fmt.Errorf("search unable to perform: %d", resultCode)
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, err
|
||||
}
|
||||
|
||||
filterPacket, err := ldapserver.CompileFilter(searchReq.Filter)
|
||||
if err != nil {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: ldap.LDAPResultOperationsError,
|
||||
}, err
|
||||
}
|
||||
|
||||
var entryRecord *ldifEntry
|
||||
var entries []*ldap.Entry
|
||||
var entry *ldap.Entry
|
||||
var count uint32
|
||||
var keep bool
|
||||
results:
|
||||
for {
|
||||
select {
|
||||
case entryRecord = <-pumpCh:
|
||||
if entryRecord == nil {
|
||||
// All done, set cookie to empty.
|
||||
pagingCookie = []byte{}
|
||||
break results
|
||||
|
||||
} else {
|
||||
entry = entryRecord.Entry
|
||||
|
||||
// Apply filter.
|
||||
keep, resultCode = ldapserver.ServerApplyFilter(filterPacket, entry)
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, errors.New("search filter apply error")
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter scope.
|
||||
keep, resultCode = ldapserver.ServerFilterScope(searchReq.BaseDN, searchReq.Scope, entry)
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, errors.New("search scope apply error")
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
|
||||
// Make a copy, before filtering attributes.
|
||||
e := &ldap.Entry{
|
||||
DN: entry.DN,
|
||||
Attributes: make([]*ldap.EntryAttribute, len(entry.Attributes)),
|
||||
}
|
||||
copy(e.Attributes, entry.Attributes)
|
||||
|
||||
// Filter attributes from entry.
|
||||
resultCode, err = ldapserver.ServerFilterAttributes(searchReq.Attributes, e)
|
||||
if err != nil {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Append entry as result.
|
||||
entries = append(entries, e)
|
||||
|
||||
// Count and more.
|
||||
count++
|
||||
if pagingControl != nil {
|
||||
if count >= pagingControl.PagingSize {
|
||||
break results
|
||||
}
|
||||
}
|
||||
if searchReq.SizeLimit > 0 && count >= uint32(searchReq.SizeLimit) {
|
||||
// TODO(longsleep): handle total sizelimit for paging.
|
||||
break results
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pagingControl != nil {
|
||||
doneControls = append(doneControls, &ldap.ControlPaging{
|
||||
PagingSize: 0,
|
||||
Cookie: pagingCookie,
|
||||
})
|
||||
}
|
||||
|
||||
return ldapserver.ServerSearchResult{
|
||||
Entries: entries,
|
||||
Referrals: []string{},
|
||||
Controls: doneControls,
|
||||
ResultCode: ldap.LDAPResultSuccess,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) searchEntriesPump(ctx context.Context, current *ldifMemoryValue, pumpCh chan<- *ldifEntry, searchReq *ldap.SearchRequest, pagingControl *ldap.ControlPaging, indexFilter [][]string) {
|
||||
defer func() {
|
||||
if pagingControl != nil {
|
||||
h.activeSearchPagings.Remove(string(pagingControl.Cookie))
|
||||
close(pumpCh)
|
||||
h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump ended")
|
||||
} else {
|
||||
close(pumpCh)
|
||||
}
|
||||
}()
|
||||
|
||||
pump := func(entryRecord *ldifEntry) bool {
|
||||
select {
|
||||
case pumpCh <- entryRecord:
|
||||
case <-ctx.Done():
|
||||
if pagingControl != nil {
|
||||
h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Warnln("ldap search paging pump context done")
|
||||
} else {
|
||||
h.logger.Warnln("ldap search pump context done")
|
||||
}
|
||||
return false
|
||||
case <-time.After(1 * time.Minute):
|
||||
if pagingControl != nil {
|
||||
h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Warnln("ldap search paging pump timeout")
|
||||
} else {
|
||||
h.logger.Warnln("ldap search pump timeout")
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
searchBaseDN := strings.ToLower(searchReq.BaseDN)
|
||||
|
||||
load := true
|
||||
if len(indexFilter) > 0 {
|
||||
// Get entries with help of index.
|
||||
load = false
|
||||
var results []*[]*ldifEntry
|
||||
for _, f := range indexFilter {
|
||||
indexed, found := current.index.Load(f[0], f[1], f[2:]...)
|
||||
if !found {
|
||||
load = true
|
||||
break
|
||||
}
|
||||
results = append(results, &indexed)
|
||||
}
|
||||
if !load {
|
||||
cache := make(map[*ldifEntry]struct{})
|
||||
for _, indexed := range results {
|
||||
for _, entryRecord := range *indexed {
|
||||
if _, cached := cache[entryRecord]; cached {
|
||||
// Prevent duplicates.
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(entryRecord.DN, searchBaseDN) {
|
||||
if ok := pump(entryRecord); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
cache[entryRecord] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if load {
|
||||
// Walk through all entries (this is slow).
|
||||
h.logger.WithField("filter", searchReq.Filter).Warnln("ldap search filter does not match any index, using slow walk")
|
||||
current.t.WalkSuffix([]byte(searchBaseDN), func(key []byte, entryRecord interface{}) bool {
|
||||
if ok := pump(entryRecord.(*ldifEntry)); !ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ldifHandler) validateBindDN(bindDN string, conn net.Conn) error {
|
||||
if bindDN == "" {
|
||||
if h.allowLocalAnonymousBind {
|
||||
host, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
if net.ParseIP(host).IsLoopback() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("anonymous BindDN rejected")
|
||||
}
|
||||
return fmt.Errorf("anonymous BindDN not allowed")
|
||||
}
|
||||
|
||||
if strings.HasSuffix(bindDN, h.baseDN) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("the BindDN is not in our BaseDN: %s", h.baseDN)
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Close(bindDN string, conn net.Conn) error {
|
||||
h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
}).Debugln("ldap close")
|
||||
|
||||
return nil
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/spacewander/go-suffix-tree"
|
||||
)
|
||||
|
||||
var indexAttributes = map[string]string{
|
||||
"entryCSN": "eq",
|
||||
"entryUUID": "eq",
|
||||
"objectClass": "eq",
|
||||
"cn": "pres,eq,sub",
|
||||
"gidNumber": "eq",
|
||||
"mail": "pres,eq,sub",
|
||||
"memberUid": "eq",
|
||||
"ou": "eq",
|
||||
"uid": "eq",
|
||||
"uidNumber": "eq",
|
||||
"uniqueMember": "eq",
|
||||
|
||||
"sn": "pres,eq,sub",
|
||||
"givenName": "pres,eq,sub",
|
||||
|
||||
"mailAlternateAddress": "eq",
|
||||
|
||||
// Additional indexes for attributes usually used with AD.
|
||||
"objectGUID": "eq",
|
||||
"objectSID": "eq",
|
||||
"otherMailbox": "eq",
|
||||
"samAccountName": "eq",
|
||||
}
|
||||
|
||||
func AddIndexAttribute(name string, indices string) {
|
||||
indexAttributes[name] = indices
|
||||
}
|
||||
|
||||
func RemoveIndexAttribute(name string) {
|
||||
delete(indexAttributes, name)
|
||||
}
|
||||
|
||||
type Index interface {
|
||||
Add(name, op string, values []string, entry *ldifEntry) bool
|
||||
Load(name, op string, params ...string) ([]*ldifEntry, bool)
|
||||
}
|
||||
|
||||
type indexMap map[string][]*ldifEntry
|
||||
|
||||
func newIndexMap() indexMap {
|
||||
return make(indexMap)
|
||||
}
|
||||
|
||||
func (im indexMap) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
for _, value := range values {
|
||||
value = strings.ToLower(value)
|
||||
im[value] = append(im[value], entry)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (im indexMap) Load(name, op string, value ...string) ([]*ldifEntry, bool) {
|
||||
entries := im[strings.ToLower(value[0])]
|
||||
return entries, true
|
||||
}
|
||||
|
||||
type indexSuffixTree struct {
|
||||
t *suffix.Tree
|
||||
}
|
||||
|
||||
func newIndexSuffixTree() *indexSuffixTree {
|
||||
return &indexSuffixTree{
|
||||
t: suffix.NewTree(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ist indexSuffixTree) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
for _, value := range values {
|
||||
sfx := []byte(value)
|
||||
var entries []*ldifEntry
|
||||
if v, ok := ist.t.Get(sfx); ok {
|
||||
entries = v.([]*ldifEntry)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
ist.t.Insert(sfx, entries)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ist indexSuffixTree) Load(name, op string, value ...string) ([]*ldifEntry, bool) {
|
||||
var entries []*ldifEntry
|
||||
sfx := []byte(value[0])
|
||||
ist.t.WalkSuffix(sfx, func(key []byte, value interface{}) bool {
|
||||
entries = append(entries, value.([]*ldifEntry)...)
|
||||
return false
|
||||
})
|
||||
return entries, true
|
||||
}
|
||||
|
||||
type indexRadixTree struct {
|
||||
t *radix.Tree
|
||||
}
|
||||
|
||||
func newIndexRadixTree() *indexRadixTree {
|
||||
return &indexRadixTree{
|
||||
t: radix.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (irt *indexRadixTree) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
for _, value := range values {
|
||||
pfx := value
|
||||
var entries []*ldifEntry
|
||||
if v, ok := irt.t.Get(pfx); ok {
|
||||
entries = v.([]*ldifEntry)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
irt.t.Insert(pfx, entries)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (irt *indexRadixTree) Load(name, op string, value ...string) ([]*ldifEntry, bool) {
|
||||
var entries []*ldifEntry
|
||||
pfx := value[0]
|
||||
irt.t.WalkPrefix(pfx, func(key string, value interface{}) bool {
|
||||
entries = append(entries, value.([]*ldifEntry)...)
|
||||
return false
|
||||
})
|
||||
return entries, true
|
||||
}
|
||||
|
||||
type indexSubTree struct {
|
||||
pres indexMap
|
||||
irt *indexRadixTree
|
||||
ist *indexSuffixTree
|
||||
}
|
||||
|
||||
func newIndexSubTree() *indexSubTree {
|
||||
return &indexSubTree{
|
||||
pres: newIndexMap(),
|
||||
irt: newIndexRadixTree(),
|
||||
ist: newIndexSuffixTree(),
|
||||
}
|
||||
}
|
||||
|
||||
func (idx *indexSubTree) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
ok0 := idx.pres.Add(name, op, []string{""}, entry)
|
||||
ok1 := idx.irt.Add(name, op, values, entry)
|
||||
ok2 := idx.ist.Add(name, op, values, entry)
|
||||
return ok0 || ok1 || ok2
|
||||
}
|
||||
|
||||
func (idx *indexSubTree) Load(name, op string, params ...string) ([]*ldifEntry, bool) {
|
||||
if len(params) != 2 {
|
||||
// Require one value and sub tag.
|
||||
return nil, false
|
||||
}
|
||||
tag, err := strconv.ParseInt(params[1], 10, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case ldapserver.FilterSubstringsAny:
|
||||
// TODO(longsleep): Find a suitable way for full text search substring
|
||||
// matching, for example with Knuth-Morris-Pratt algorithm. Currently
|
||||
// we just do a presence match.
|
||||
return idx.pres.Load(name, op, "")
|
||||
case ldapserver.FilterSubstringsInitial:
|
||||
return idx.irt.Load(name, op, params[0])
|
||||
case ldapserver.FilterSubstringsFinal:
|
||||
return idx.ist.Load(name, op, params[0])
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
type indexMapRegister map[string]Index
|
||||
|
||||
func newIndexMapRegister() indexMapRegister {
|
||||
imr := make(indexMapRegister)
|
||||
for name, ops := range indexAttributes {
|
||||
for _, op := range strings.Split(ops, ",") {
|
||||
switch op {
|
||||
case "sub":
|
||||
imr[imr.getKey(name, op)] = newIndexSubTree()
|
||||
case "pres":
|
||||
imr[imr.getKey(name, op)] = newIndexMap()
|
||||
case "eq":
|
||||
imr[imr.getKey(name, op)] = newIndexMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
return imr
|
||||
}
|
||||
|
||||
func (imr indexMapRegister) getKey(name, op string) string {
|
||||
return strings.ToLower(name) + "," + op
|
||||
}
|
||||
|
||||
func (imr indexMapRegister) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
index, ok := imr[imr.getKey(name, op)]
|
||||
if !ok {
|
||||
// No matching index, refuse to add.
|
||||
return false
|
||||
}
|
||||
return index.Add(name, op, values, entry)
|
||||
}
|
||||
|
||||
func (imr indexMapRegister) Load(name, op string, params ...string) ([]*ldifEntry, bool) {
|
||||
index, ok := imr[imr.getKey(name, op)]
|
||||
if !ok {
|
||||
// No such index.
|
||||
return nil, false
|
||||
}
|
||||
return index.Load(name, op, params...)
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/go-ldap/ldif"
|
||||
"github.com/spacewander/go-suffix-tree"
|
||||
)
|
||||
|
||||
// parseLDIFFile opens the named file for reading and parses it as LDIF.
|
||||
func parseLDIFFile(fn string, options *Options) (*ldif.LDIF, error) {
|
||||
f, err := os.Open(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var r io.Reader
|
||||
|
||||
if options.TemplateEngineDisabled {
|
||||
r = f
|
||||
} else {
|
||||
r, err = parseLDIFTemplate(f, options, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return parseLDIF(r, options)
|
||||
}
|
||||
|
||||
// parseLDIFDirectory opens all ldif files in the given path in sorted order,
|
||||
// cats them all together and parses the result as LDIF.
|
||||
func parseLDIFDirectory(pn string, options *Options) (*ldif.LDIF, []error, error) {
|
||||
matches, err := filepath.Glob(filepath.Join(pn, "*.ldif"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
return matches[i] < matches[j]
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
var matchErrors []error
|
||||
for _, match := range matches {
|
||||
err = func() error {
|
||||
f, openErr := os.Open(match)
|
||||
if openErr != nil {
|
||||
matchErrors = append(matchErrors, fmt.Errorf("file read error: %w", openErr))
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
if options.TemplateEngineDisabled {
|
||||
_, copyErr := io.Copy(&buf, f)
|
||||
if copyErr != nil {
|
||||
return fmt.Errorf("file read error: %w", copyErr)
|
||||
}
|
||||
} else {
|
||||
p, parseErr := parseLDIFTemplate(f, options, nil)
|
||||
if parseErr != nil {
|
||||
matchErrors = append(matchErrors, fmt.Errorf("parse error in %s: %w", match, parseErr))
|
||||
return nil
|
||||
}
|
||||
_, copyErr := io.Copy(&buf, p)
|
||||
if copyErr != nil {
|
||||
return fmt.Errorf("template read error: %w", copyErr)
|
||||
}
|
||||
}
|
||||
buf.WriteString("\n\n")
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, matchErrors, err
|
||||
}
|
||||
}
|
||||
|
||||
l, err := parseLDIF(&buf, options)
|
||||
return l, matchErrors, err
|
||||
}
|
||||
|
||||
// parseLDIFTemplate exectues the provided text template and then parses the
|
||||
// result as LDIF.
|
||||
func parseLDIFTemplate(r io.Reader, options *Options, m map[string]interface{}) (io.Reader, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
var text []string
|
||||
for scanner.Scan() {
|
||||
t := scanner.Text()
|
||||
if t != "" && t[0] == '#' {
|
||||
// Ignore commented lines.
|
||||
continue
|
||||
}
|
||||
text = append(text, scanner.Text())
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
m = make(map[string]interface{})
|
||||
}
|
||||
tpl, err := template.New("tpl").Funcs(TemplateFuncs(m, options)).Parse(strings.Join(text, "\n"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse LDIF template: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tpl.Execute(&buf, m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to process LDIF template: %w", err)
|
||||
}
|
||||
|
||||
if options.TemplateDebug {
|
||||
fmt.Println("---\n", buf.String(), "\n----")
|
||||
}
|
||||
|
||||
return &buf, nil
|
||||
}
|
||||
|
||||
func parseLDIF(r io.Reader, options *Options) (*ldif.LDIF, error) {
|
||||
l := &ldif.LDIF{}
|
||||
err := ldif.Unmarshal(r, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// treeFromLDIF makes a tree out of the provided LDIF and if index is not nil,
|
||||
// also indexes each entry in the provided index.
|
||||
func treeFromLDIF(l *ldif.LDIF, index Index, options *Options) (*suffix.Tree, error) {
|
||||
t := suffix.NewTree()
|
||||
|
||||
// NOTE(longsleep): Create in memory tree records from LDIF data.
|
||||
var entry *ldap.Entry
|
||||
for _, entryRecord := range l.Entries {
|
||||
if entryRecord == nil || entryRecord.Entry == nil {
|
||||
// NOTE(longsleep): We don't use l.AllEntries as "nil" records can happen.
|
||||
continue
|
||||
}
|
||||
entry = entryRecord.Entry
|
||||
e := &ldifEntry{
|
||||
Entry: &ldap.Entry{
|
||||
DN: strings.ToLower(entry.DN),
|
||||
},
|
||||
}
|
||||
for _, a := range entry.Attributes {
|
||||
switch strings.ToLower(a.Name) {
|
||||
case "userpassword":
|
||||
// Don't include the password in the normal attributes.
|
||||
e.UserPassword = &ldap.EntryAttribute{
|
||||
Name: a.Name,
|
||||
Values: a.Values,
|
||||
}
|
||||
default:
|
||||
// Append it.
|
||||
e.Entry.Attributes = append(e.Entry.Attributes, &ldap.EntryAttribute{
|
||||
Name: a.Name,
|
||||
Values: a.Values,
|
||||
})
|
||||
}
|
||||
if index != nil {
|
||||
// Index equalityMatch.
|
||||
index.Add(a.Name, "eq", a.Values, e)
|
||||
// Index present.
|
||||
index.Add(a.Name, "pres", []string{""}, e)
|
||||
// Index substrings.
|
||||
index.Add(a.Name, "sub", a.Values, e)
|
||||
}
|
||||
}
|
||||
v, ok := t.Insert([]byte(e.DN), e)
|
||||
if !ok || v != nil {
|
||||
return nil, fmt.Errorf("duplicate dn value: %s", e.DN)
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"github.com/go-ldap/ldif"
|
||||
"github.com/spacewander/go-suffix-tree"
|
||||
)
|
||||
|
||||
type ldifMemoryValue struct {
|
||||
l *ldif.LDIF
|
||||
t *suffix.Tree
|
||||
|
||||
index Index
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
)
|
||||
|
||||
type ldifMiddleware struct {
|
||||
logger logrus.FieldLogger
|
||||
fn string
|
||||
options *Options
|
||||
|
||||
baseDN string
|
||||
|
||||
current atomic.Value
|
||||
|
||||
next handler.Handler
|
||||
}
|
||||
|
||||
var _ handler.Handler = (*ldifMiddleware)(nil) // Verify that *configHandler implements handler.Handler.
|
||||
|
||||
func NewLDIFMiddleware(logger logrus.FieldLogger, fn string, options *Options) (handler.Middleware, error) {
|
||||
if fn == "" {
|
||||
return nil, fmt.Errorf("file name is empty")
|
||||
}
|
||||
if options.BaseDN == "" {
|
||||
return nil, fmt.Errorf("base dn is empty")
|
||||
}
|
||||
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger = logger.WithField("fn", fn)
|
||||
|
||||
h := &ldifMiddleware{
|
||||
logger: logger,
|
||||
fn: fn,
|
||||
options: options,
|
||||
|
||||
baseDN: strings.ToLower(options.BaseDN),
|
||||
}
|
||||
|
||||
err = h.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) open() error {
|
||||
if !strings.EqualFold(h.options.BaseDN, h.baseDN) {
|
||||
return fmt.Errorf("mismatched BaseDN")
|
||||
}
|
||||
|
||||
h.logger.Debugln("loading LDIF")
|
||||
l, err := parseLDIFFile(h.fn, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, err := treeFromLDIF(l, nil, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store parsed data as memory value.
|
||||
value := &ldifMemoryValue{
|
||||
t: t,
|
||||
}
|
||||
h.current.Store(value)
|
||||
|
||||
h.logger.WithFields(logrus.Fields{
|
||||
"version": l.Version,
|
||||
"entries_count": len(l.Entries),
|
||||
"tree_length": t.Len(),
|
||||
"base_dn": h.options.BaseDN,
|
||||
}).Debugln("loaded LDIF")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) load() *ldifMemoryValue {
|
||||
value := h.current.Load()
|
||||
return value.(*ldifMemoryValue)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) WithHandler(next handler.Handler) handler.Handler {
|
||||
h.next = next
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) WithContext(ctx context.Context) handler.Handler {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
|
||||
h2 := new(ldifMiddleware)
|
||||
*h2 = *h
|
||||
h2.next = h.next.WithContext(ctx)
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Reload(ctx context.Context) error {
|
||||
err := h.open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.next.Reload(ctx)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Add(_ string, _ *ldap.AddRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldapserver.LDAPResultCode, err error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
|
||||
if bindSimplePw == "" { // Empty password means anonymous bind.
|
||||
return h.next.Bind(bindDN, bindSimplePw, conn)
|
||||
}
|
||||
|
||||
current := h.load()
|
||||
|
||||
entryRecord, found := current.t.Get([]byte(bindDN))
|
||||
if found {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if !strings.HasSuffix(bindDN, h.baseDN) {
|
||||
err := fmt.Errorf("the BindDN is not in our BaseDN %s", h.baseDN)
|
||||
logger.WithError(err).Infoln("ldap bind error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
if err := entryRecord.(*ldifEntry).validatePassword(bindSimplePw); err != nil {
|
||||
logger.WithError(err).Infoln("bind error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
return h.next.Bind(bindDN, bindSimplePw, conn)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Delete(_ string, _ *ldap.DelRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Modify(_ string, _ *ldap.ModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) ModifyDN(_ string, _ *ldap.ModifyDNRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) ModifyPasswordExop(_ string, _ *ldap.PasswordModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Search(bindDN string, searchReq *ldap.SearchRequest, conn net.Conn) (result ldapserver.ServerSearchResult, err error) {
|
||||
return h.next.Search(bindDN, searchReq, conn)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Close(bindDN string, conn net.Conn) error {
|
||||
return h.next.Close(bindDN, conn)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
type Options struct {
|
||||
BaseDN string
|
||||
AdminDN string
|
||||
AllowLocalAnonymousBind bool
|
||||
|
||||
DefaultCompany string
|
||||
DefaultMailDomain string
|
||||
|
||||
TemplateExtraVars map[string]interface{}
|
||||
TemplateEngineDisabled bool
|
||||
TemplateDebug bool
|
||||
|
||||
templateBasePath string
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/libregraph/idm"
|
||||
)
|
||||
|
||||
const formatAsFileSizeLimit int64 = 1024 * 1024
|
||||
|
||||
func TemplateFuncs(m map[string]interface{}, options *Options) template.FuncMap {
|
||||
defaults := map[string]interface{}{
|
||||
"Company": "Default",
|
||||
"BaseDN": idm.DefaultLDAPBaseDN,
|
||||
"MailDomain": idm.DefaultMailDomain,
|
||||
}
|
||||
for k, v := range defaults {
|
||||
if _, ok := m[k]; !ok {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
if options != nil {
|
||||
if options.BaseDN != "" {
|
||||
m["BaseDN"] = options.BaseDN
|
||||
}
|
||||
if options.DefaultCompany != "" {
|
||||
m["Company"] = options.DefaultCompany
|
||||
}
|
||||
if options.DefaultMailDomain != "" {
|
||||
m["MailDomain"] = options.DefaultMailDomain
|
||||
}
|
||||
for k, v := range options.TemplateExtraVars {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
autoIncrement := uint64(1000)
|
||||
if v, ok := m["AutoIncrementMin"]; ok {
|
||||
autoIncrement = v.(uint64)
|
||||
}
|
||||
|
||||
basePath := options.templateBasePath
|
||||
|
||||
return template.FuncMap{
|
||||
"WithCompany": func(value string) string {
|
||||
m["Company"] = value
|
||||
return ""
|
||||
},
|
||||
"WithBaseDN": func(value string) string {
|
||||
m["BaseDN"] = value
|
||||
return ""
|
||||
},
|
||||
"WithMailDomain": func(value string) string {
|
||||
m["MailDomain"] = value
|
||||
return ""
|
||||
},
|
||||
"AutoIncrement": func(values ...uint64) uint64 {
|
||||
if len(values) > 0 {
|
||||
autoIncrement = values[0]
|
||||
} else {
|
||||
autoIncrement++
|
||||
}
|
||||
return autoIncrement
|
||||
},
|
||||
"formatAsBase64": func(s string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(s))
|
||||
},
|
||||
"formatAsFileBase64": func(fn string) (string, error) {
|
||||
if basePath == "" {
|
||||
return "", fmt.Errorf("LDIF template fromFile failed, no base path")
|
||||
}
|
||||
fn = filepath.Clean(fn)
|
||||
if !filepath.IsAbs(fn) {
|
||||
fn = filepath.Join(basePath, fn)
|
||||
}
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// NOTE(longsleep): Poor man base path check, should work well enough on Linux.
|
||||
// See https://github.com/golang/go/issues/18358 for details.
|
||||
if !strings.HasPrefix(fn, strings.TrimRight(basePath, "/")+"/") {
|
||||
return "", fmt.Errorf("LDIF template formatAsFile %s outside of %s is not allowed", fn, basePath)
|
||||
}
|
||||
|
||||
f, err := os.Open(fn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("LDIF template formatAsFile open failed with error: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
reader := io.LimitReader(f, formatAsFileSizeLimit+1)
|
||||
|
||||
var buf bytes.Buffer
|
||||
encoder := base64.NewEncoder(base64.StdEncoding, &buf)
|
||||
n, err := io.Copy(encoder, reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("LDIF template formatAsFile error: %w", err)
|
||||
}
|
||||
if n > formatAsFileSizeLimit {
|
||||
return "", fmt.Errorf("LDIF template formatAsFile size limit exceeded: %s", fn)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
metricsSubsystemLDAPServer = "ldapserver"
|
||||
)
|
||||
|
||||
// MustRegister registers all rtm metrics with the provided registerer and
|
||||
// panics upon the first registration that causes an error.
|
||||
func MustRegister(reg prometheus.Registerer, cs ...prometheus.Collector) {
|
||||
reg.MustRegister(cs...)
|
||||
}
|
||||
|
||||
type ldapServerCollector struct {
|
||||
stats *ldapserver.Stats
|
||||
|
||||
connsTotalDesc *prometheus.Desc
|
||||
connsCurrentDesc *prometheus.Desc
|
||||
connsMaxDesc *prometheus.Desc
|
||||
|
||||
bindsDesc *prometheus.Desc
|
||||
unbindsDesc *prometheus.Desc
|
||||
searchesDsc *prometheus.Desc
|
||||
}
|
||||
|
||||
func NewLDAPServerCollector(s *ldapserver.Server) prometheus.Collector {
|
||||
return &ldapServerCollector{
|
||||
stats: s.Stats,
|
||||
|
||||
connsTotalDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "connections_total"),
|
||||
"Total number of incoming LDAP connections",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
connsCurrentDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "connections_current"),
|
||||
"Current number of concurrent established incoming LDAP connections",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
connsMaxDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "connections_max"),
|
||||
"Maximum number of concurrent established incoming LDAP connections",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
bindsDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "binds_total"),
|
||||
"Total number of incoming LDAP bind requests",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
unbindsDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "unbinds_total"),
|
||||
"Total number of incoming LDAP unbind requests",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
searchesDsc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "searches_total"),
|
||||
"Total number of incoming LDAP search requests",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Describe is implemented with DescribeByCollect. That's possible because the
|
||||
// Collect method will always return the same two metrics with the same two
|
||||
// descriptors.
|
||||
func (lc *ldapServerCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
prometheus.DescribeByCollect(lc, ch)
|
||||
}
|
||||
|
||||
// Collect first gathers the associated managers collectors managers data. Then
|
||||
// it creates constant metrics based on the returned data.
|
||||
func (lc *ldapServerCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
stats := lc.stats.Clone()
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.connsTotalDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Conns),
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.connsCurrentDesc,
|
||||
prometheus.GaugeValue,
|
||||
float64(stats.ConnsCurrent),
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.connsMaxDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.ConnsMax),
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.bindsDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Binds),
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.unbindsDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Unbinds),
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.searchesDsc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Searches),
|
||||
)
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/bombsimon/logrusr/v3"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
"github.com/libregraph/idm/server/handler/boltdb"
|
||||
"github.com/libregraph/idm/server/handler/ldif"
|
||||
)
|
||||
|
||||
const DefaultGeneratedPasswordLength = 16
|
||||
|
||||
// Server is our server implementation.
|
||||
type Server struct {
|
||||
config *Config
|
||||
|
||||
logger logrus.FieldLogger
|
||||
|
||||
LDAPServer *ldapserver.Server
|
||||
LDAPHandler handler.Handler
|
||||
}
|
||||
|
||||
// NewServer constructs a server from the provided parameters.
|
||||
func NewServer(c *Config) (*Server, error) {
|
||||
s := &Server{
|
||||
config: c,
|
||||
|
||||
logger: c.Logger,
|
||||
}
|
||||
|
||||
s.LDAPServer = ldapserver.NewServer()
|
||||
s.LDAPServer.EnforceLDAP = false
|
||||
s.LDAPServer.GeneratedPasswordLength = DefaultGeneratedPasswordLength
|
||||
ldapserver.Logger(logrusr.New(c.Logger))
|
||||
|
||||
var err error
|
||||
switch c.LDAPHandler {
|
||||
case "ldif":
|
||||
ldifHandlerOptions := &ldif.Options{
|
||||
BaseDN: s.config.LDAPBaseDN,
|
||||
AdminDN: s.config.LDAPAdminDN,
|
||||
AllowLocalAnonymousBind: s.config.LDAPAllowLocalAnonymousBind,
|
||||
|
||||
DefaultCompany: s.config.LDIFDefaultCompany,
|
||||
DefaultMailDomain: s.config.LDIFDefaultMailDomain,
|
||||
TemplateExtraVars: s.config.LDIFTemplateExtraVars,
|
||||
|
||||
TemplateDebug: os.Getenv("KIDM_TEMPLATE_DEBUG") != "",
|
||||
}
|
||||
|
||||
s.LDAPHandler, err = ldif.NewLDIFHandler(s.logger, s.config.LDIFMain, ldifHandlerOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create LDIF source handler: %w", err)
|
||||
}
|
||||
if s.config.LDIFConfig != "" {
|
||||
middleware, middlewareErr := ldif.NewLDIFMiddleware(s.logger, s.config.LDIFConfig, ldifHandlerOptions)
|
||||
if middlewareErr != nil {
|
||||
return nil, fmt.Errorf("failed to create LDIF config handler: %w", middlewareErr)
|
||||
}
|
||||
s.LDAPHandler = middleware.WithHandler(s.LDAPHandler)
|
||||
}
|
||||
case "boltdb":
|
||||
boltOptions := &boltdb.Options{
|
||||
BaseDN: s.config.LDAPBaseDN,
|
||||
AdminDN: s.config.LDAPAdminDN,
|
||||
|
||||
AllowLocalAnonymousBind: s.config.LDAPAllowLocalAnonymousBind,
|
||||
}
|
||||
s.LDAPHandler, err = boltdb.NewBoltDBHandler(s.logger, s.config.BoltDBFile, boltOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create BoltDB handler: %w", err)
|
||||
}
|
||||
|
||||
// FIXME Let the frontend (LDAPServer) handle filtering and attribute list until we added backend support
|
||||
s.LDAPServer.EnforceLDAP = true
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown LDAPHandler: '%s'", c.LDAPHandler)
|
||||
}
|
||||
|
||||
if c.Metrics != nil {
|
||||
s.LDAPServer.SetStats(true)
|
||||
MustRegister(c.Metrics, NewLDAPServerCollector(s.LDAPServer))
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Serve starts all the accociated servers resources and listeners and blocks
|
||||
// forever until signals or error occurs.
|
||||
func (s *Server) Serve(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
serveCtx, serveCtxCancel := context.WithCancel(ctx)
|
||||
defer serveCtxCancel()
|
||||
|
||||
logger := s.logger
|
||||
|
||||
errCh := make(chan error, 2)
|
||||
exitCh := make(chan struct{}, 1)
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
readyCh := make(chan struct{}, 1)
|
||||
triggerCh := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-serveCtx.Done():
|
||||
return
|
||||
case <-readyCh:
|
||||
}
|
||||
logger.WithFields(logrus.Fields{}).Infoln("ready")
|
||||
}()
|
||||
|
||||
var serversWg sync.WaitGroup
|
||||
|
||||
// NOTE(rhafer): since v3.4.3 the ldap package allows to set a custom logger.
|
||||
// Set that to use to our logger.
|
||||
loggerWriter := logger.WithField("scope", "ldap").WriterLevel(logrus.DebugLevel)
|
||||
defer loggerWriter.Close()
|
||||
ldap.Logger(log.New(loggerWriter, "", 0))
|
||||
|
||||
ldapHandler := s.LDAPHandler.WithContext(serveCtx)
|
||||
s.LDAPServer.AddFunc("", ldapHandler)
|
||||
s.LDAPServer.BindFunc("", ldapHandler)
|
||||
s.LDAPServer.DeleteFunc("", ldapHandler)
|
||||
s.LDAPServer.ModifyFunc("", ldapHandler)
|
||||
s.LDAPServer.ModifyDNFunc("", ldapHandler)
|
||||
s.LDAPServer.PasswordExOpFunc("", ldapHandler)
|
||||
s.LDAPServer.SearchFunc("", ldapHandler)
|
||||
s.LDAPServer.CloseFunc("", ldapHandler)
|
||||
|
||||
serversWg.Add(1)
|
||||
go func() {
|
||||
defer serversWg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-triggerCh:
|
||||
reloadErr := ldapHandler.Reload(serveCtx)
|
||||
if reloadErr != nil {
|
||||
logger.Debugln("reload error: %w", reloadErr)
|
||||
} else {
|
||||
logger.Debugln("reload complete")
|
||||
}
|
||||
case <-serveCtx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if s.config.LDAPListenAddr != "" {
|
||||
serversWg.Add(1)
|
||||
go func() {
|
||||
defer serversWg.Done()
|
||||
logger.WithField("listen_addr", s.config.LDAPListenAddr).Infoln("starting LDAP listener")
|
||||
serveErr := s.LDAPServer.ListenAndServe(s.config.LDAPListenAddr)
|
||||
if serveErr != nil {
|
||||
errCh <- serveErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
if s.config.LDAPSListenAddr != "" {
|
||||
serversWg.Add(1)
|
||||
go func() {
|
||||
defer serversWg.Done()
|
||||
logger.WithField("listen_addr_tls", s.config.LDAPSListenAddr).Infoln("starting LDAPS listener")
|
||||
serveErr := s.LDAPServer.ListenAndServeTLS(s.config.LDAPSListenAddr, s.config.TLSCertFile, s.config.TLSKeyFile)
|
||||
if serveErr != nil {
|
||||
errCh <- serveErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
serversWg.Wait()
|
||||
logger.Debugln("server listeners stopped")
|
||||
close(exitCh)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
close(readyCh) // TODO(longsleep): Implement real ready.
|
||||
if s.config.OnReady != nil {
|
||||
go s.config.OnReady(s)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for exit or error, with support for HUP to reload
|
||||
err = func() error {
|
||||
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
for {
|
||||
select {
|
||||
case errFromChannel := <-errCh:
|
||||
return errFromChannel
|
||||
case reason := <-signalCh:
|
||||
if reason == syscall.SIGHUP {
|
||||
logger.Infoln("reload signal received")
|
||||
select {
|
||||
case triggerCh <- true:
|
||||
default:
|
||||
}
|
||||
continue
|
||||
}
|
||||
logger.WithField("signal", reason).Warnln("received signal")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Shutdown, server will stop to accept new connections, requires Go 1.8+.
|
||||
logger.Infoln("clean server shutdown start")
|
||||
_, shutdownCtxCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
go func() {
|
||||
close(s.LDAPServer.Quit)
|
||||
}()
|
||||
|
||||
// Cancel our own context,
|
||||
serveCtxCancel()
|
||||
func() {
|
||||
for {
|
||||
select {
|
||||
case <-exitCh:
|
||||
logger.Infoln("clean server shutdown complete, exiting")
|
||||
return
|
||||
default:
|
||||
// Services have not quit yet.
|
||||
logger.Info("waiting for services to exit")
|
||||
}
|
||||
select {
|
||||
case reason := <-signalCh:
|
||||
logger.WithField("signal", reason).Warn("received signal")
|
||||
return
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}()
|
||||
shutdownCtxCancel() // Prevents leak.
|
||||
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user