switch to go vendoring

This commit is contained in:
Michael Barz
2023-04-19 20:10:09 +02:00
parent 632fa05ef9
commit afc6ed1e41
8527 changed files with 3004916 additions and 2 deletions
+12
View File
@@ -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
View File
@@ -0,0 +1,8 @@
/vendor
/bin
/golint.txt
/govet.txt
/dist
/test/tests.*
/3rdparty-LICENSES.md
/.vscode
+77
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,6 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2021 The LibreGraph Authors.
*/
package idm // import "github.com/libregraph/idm"
+94
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}