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
vendor/github.com/libregraph/idm/.editorconfig generated vendored Normal file
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
vendor/github.com/libregraph/idm/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
/vendor
/bin
/golint.txt
/govet.txt
/dist
/test/tests.*
/3rdparty-LICENSES.md
/.vscode

77
vendor/github.com/libregraph/idm/.golangci.yaml generated vendored Normal file
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

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
vendor/github.com/libregraph/idm/AUTHORS generated vendored Normal file
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
vendor/github.com/libregraph/idm/CHANGELOG.md generated vendored Normal file
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
vendor/github.com/libregraph/idm/CONTRIBUTORS generated vendored Normal file
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
vendor/github.com/libregraph/idm/Dockerfile.build generated vendored Normal file
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
vendor/github.com/libregraph/idm/LICENSE.txt generated vendored Normal file
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
vendor/github.com/libregraph/idm/Makefile generated vendored Normal file
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
vendor/github.com/libregraph/idm/README.md generated vendored Normal file
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
vendor/github.com/libregraph/idm/defaults.go generated vendored Normal file
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
vendor/github.com/libregraph/idm/doc.go generated vendored Normal file
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
vendor/github.com/libregraph/idm/pkg/ldapdn/ldapdn.go generated vendored Normal file
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
}

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)
}

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)
}

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")
}

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
}

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.

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
vendor/github.com/libregraph/idm/pkg/ldapserver/add.go generated vendored Normal file
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
}

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
}

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
}

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
}

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
}

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
)

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
}

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
}

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
}

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
}

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
}

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
vendor/github.com/libregraph/idm/pkg/ldbbolt/ldbbolt.go generated vendored Normal file
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
vendor/github.com/libregraph/idm/server/config.go generated vendored Normal file
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)
}

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
}

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
}

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
}

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
}

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
}

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...)
}

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
}

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
}

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)
}

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
}

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
vendor/github.com/libregraph/idm/server/metrics.go generated vendored Normal file
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
vendor/github.com/libregraph/idm/server/server.go generated vendored Normal file
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
}

7
vendor/github.com/libregraph/lico/.dependabot.yml generated vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 2

32
vendor/github.com/libregraph/lico/.editorconfig generated vendored Normal file
View File

@@ -0,0 +1,32 @@
# 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
; Python: PEP8 defines 4 spaces for indentation
[*.py]
indent_style = space
indent_size = 4
; YAML format, 2 spaces
[*.{yaml,yml,yaml.in,json}]
indent_style = space
indent_size = 2
; HTML, CSS and JavaScript, 4 spaces
[*.{html,css,js,ts,tsx,jsx}]
charset = utf-8
indent_size = 2
indent_style = space
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

26
vendor/github.com/libregraph/lico/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,26 @@
.*
# We don't want to ignore the following even if they are dot-files
!.editorconfig
!.gitattributes
!.gitignore
!.gitlab-ci.yml
!.env
!.chglog
!.dependabot.yml
!.github
/vendor
/bin
/cmd/licod/debug
/test/tests.*
/test/coverage.*
/golint.txt
/govet.txt
/dist
/examples
/identifier/node_modules
/Caddyfile
/3rdparty-LICENSES.md
/identifier-registration.yaml
/scopes.yaml

996
vendor/github.com/libregraph/lico/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,996 @@
# CHANGELOG
## Unreleased
## v0.59.4 (2022-12-02)
- Pull survey client dependency from Github
## v0.59.3 (2022-12-01)
- Bump loader-utils from 2.0.0 to 2.0.4 in /identifier
- Bump github.com/golang-jwt/jwt/v4 from 4.3.0 to 4.4.3
- Bump github.com/sirupsen/logrus from 1.8.1 to 1.9.0
- Bump github.com/crewjam/saml from 0.4.6 to 0.4.10
- Update oidc and rndm external dependencies
- Bump github.com/gabriel-vasile/mimetype from 1.4.0 to 1.4.1
- Bump [@xmldom](https://github.com/xmldom/)/xmldom from 0.8.2 to 0.8.5 in /identifier
## v0.59.2 (2022-10-19)
- Fix a bunch of eslint warnings
- Bump identifier third party dependencies
- Bump caniuse-lite to latest version
## v0.59.1 (2022-10-13)
- Update rndm to 1.1.2
## v0.59.0 (2022-09-27)
- Switch CI pipeline to Go 1.18
- Increase state cookie duration to 10 minutes
- Properly handle prompt select_account and consent for external oidc
- Update transient go dependencies
- Use error wrapping in oauth2 callback propertly
- Add short instructions for libregraph backend
- Remove obsolete dummy backend
- Remove obsolete cookie backend
- Remove kc backend
- Bump github.com/prometheus/client_golang from 1.12.1 to 1.13.0
- Bump github.com/spf13/cobra from 1.4.0 to 1.5.0
## v0.58.0 (2022-09-26)
- Implement code flow for external OIDC authorities
- Don't enforce prompt=None for external OIDC auth
- Fix development server listner and proxy address
- Ensure to commit Yarn 2 config
- Add missing build dependencies
- Allow build to succeed in CI even with eslint warnings
- Fetch identifier vendor dependencies in vendor CI step
- Make Go linter errors non-fatal
- Add build CI
- Add dependabot config
- Upgrade to Yarn 2
- Use Yarn 2
## v0.57.0 (2022-08-23)
- Allow backends to set top level ID token claims
- Support loading validators from PEM encoded certificates
- Fix parsing of JWKS in authorities registration YAML
## v0.56.1 (2022-07-19)
- Fix HTTP2 support for libregraph backend connections
## v0.56.0 (2022-07-07)
- Update oidc-go to v0.3.4
- Retain issuer subpath when computing well-known configuration URI
- Bump all internal Python scripts to run with Python 3
- Add support for implicit scopes for server registered clients
## v0.55.0 (2022-04-13)
- Update to current browserlist database
- Bump to require Go 1.18
## v0.54.1 (2022-03-31)
- Update dependencies and move to different uuid package
- Interpolate identifier error message translations correctly
## v0.54.0 (2022-03-15)
- Bump follow-redirects from 1.14.4 to 1.14.8 in /identifier
- Bump github.com/crewjam/saml to v0.4.6
- Server Servername on TLS config
- Allow to set a CA certificate for LDAPS connections
- Use LibreGraph branded names when generating 3rd-party license overview
- Update JavaScript license ranger to latest version
- Add identifier i18n via ietf code to support Chinese better
- Add cookie support for identifier locale selection
- Allow i18n Makefile to operate on individual po files
- Update German translation
- Add support to limit the available identifier web app locales
- Improve i18n of identifier web app
- Bring back translations for German, French and Dutch
- Update README to reflect LibreGraph
- Update third party dependencies
- Bring back i18n for identifier web app
- Use fixed translation ids for error messages
- Avoid adding state twice to endsession callback URL query
- Enable dependabot for Go modules
## v0.53.1 (2021-12-20)
- Injecty identifier identity into context in token requests
- Fix panic when client request has no client_id
- Do not show sign-in screen when prompt=none when no user
## v0.53.0 (2021-12-01)
- Add support for sessions when using the libregraph identifier backend
- Blacklist other selective scopes for multiple libregraph backend support
- Add scope based backend selection for libregraph identity backend
- Remove auth pass through from request headers
## v0.52.0 (2021-11-12)
- Support accountEnabled property in libregraph identifier backend
- Add support for identifier backends to expand the requested scopes
- Add support to extend authorized scopes from backend
- Update 3rd-party direct and transitive dependencies
- Ensure user data is refreshed on token creation
- Use lico specific unique salt for sub values
- Simplify and unify built-in scopes and access/refresh token claims
- Add support for top level at claims via in libregraph identifier backend
- Retain received branding even on hello updates, until hello reset
## v0.51.1 (2021-10-15)
- Ensure that app-icon.svg gets built with Makefile
## v0.51.0 (2021-10-15)
- Add support for open extensions in libregraph identifier backend
- Migrate dgrijalva/jwt-go to golang-jwt/jwt-go
## v0.50.0 (2021-10-14)
- Switch HTTP client default User-Agent to LibreGraph Connect
- Inject additional HTTP request headers into libregraph backend requests
- Implement generic libregraph backend
- Also make the identifier backends plugable
- Make bootstrap of backend plugabble
- Add support for visual branding of identifier
- Replace Kopano logo with general app icon
- Refactor translations, English only for now
- Improve style of back buttons after style changes
- Remove more Kopano CI, replace with generic UI and styles
- Migrate more stuff away from konnect naming to lico naming
- Modernize 3rd-party dependencies and remove kpop
- Update 3rd-party identifier webapp dependencies
- Use actually working caddy configuration in example
- Update 3rd-party Go dependencies to their latest
- Build with Go 1.17
- Remove obsolete Jenkinsfile
- Apply LibreGraph naming treewide
## v0.34.0 (2021-05-06)
- Correct Docker based build example
- Fix broken client registration unit test initialization
- Allow 127.0.0.1 and [::1] redirect_uris for native clients
- Allow redirect_uris without path for native clients
- Allow configuration of expiration of dynamic client_secret values
- Update dependencies in Dockerfile.release
## v0.33.11 (2020-12-14)
- Validate XML before SAML processing
## v0.33.10 (2020-11-02)
- Fix processing for prompt select_account with consent
- Improve checks for Basic auth data in token requests
## v0.33.9 (2020-10-27)
- Build with Go 1.14.10
- enhance description
- Add uri_base_path to binscript and config file
- Catch potential errors when parsing own styles
## v0.33.8 (2020-10-02)
- Generate random endsession state for external authority
- Update dependencies in Dockerfile
## v0.33.7 (2020-09-29)
- Set prompt=None to avoid loops with external authority
## v0.33.6 (2020-09-10)
- v0.33.6
- Update Jenkins reporting plugin from checkstyle to recordIssues
- Remove extra kty key from JWKS top level document
## v0.33.5 (2020-06-25)
- Fix regression which encodes URL fragments twice
- Update Docker dependencies
## v0.33.4 (2020-06-23)
- Avoid generating fragmet/query URLs with wrong order
- Return state for oidc endsession response redirects
- Build with Go 1.14.4
## v0.33.3 (2020-06-02)
- Use server provided username to avoid case mismatch
## v0.33.2 (2020-06-02)
- Use signed-out-uri if set as fallback for goodbye redirect on saml slo
- Add checks to ensure post_logout_redirect_uri is not empty
## v0.33.1 (2020-05-26)
- Fix SAML2 logout request parsing
- Cure panic when no state is found in saml esr
- Use SAML IdP Issuer value from meta data entityID
## v0.33.0 (2020-04-16)
- Allow configuration of expiration of oidc access, id and refresh tokens
- Implement trampolin for external OIDC authority end session
- Update to latest Alpine release
- Update ca-certificates version
## v0.32.0 (2020-04-15)
- Implement delegation of end session to external authority
- Improve names of temporary state and consent cookies
- Use correct path when removing state cookies
- Store identified user external authority ID in session data
- Implement redirect binding slo response
## v0.31.0 (2020-04-09)
- Relax linter to let more warning pass
- Implement validation for IdP initiated SLO requests
- Add support for expiration and session id for external authorities
- Fix wrong error message when there was no error
- Add additional TODO markers for SAML external authority
- Improve logging when using external SAML authority
- Retry SAML initialize on error
- Improve OIDC endsession endpoint handler when without token hint
- Implement support for SAML IdP slo
- Fail early when SAML2 authority fails to resolve user from backend
- Apply user mapping when resolving users from LDAP backend
- Update 3rd party dependencies
- Update license ranger and generate 3rd party licenses from vendor folder
## v0.30.0 (2020-03-09)
- Add SAML2 external authority example config
- Update linter in CI to latest version so it works with Go 1.14
- Implement SAML2 external authority support
- Prepare external authority support for different authority types
- Update and deduplicate external dependencies
- Ensure identifier client index.html is actually loaded
- Build with Go 1.14
- Merge branch 'IljaN-make-identifier-webapp-optional'
- Add disable-identifier-webapp option
- Migrate konnect identifier to newly introduced theme.spacing api
## v0.29.0 (2020-02-13)
- Detect browser state change issues
- Add fulllint helper to lint from the start
- Update 3rd party Go dependencies
- Update javascript 3rd party dependencies
- Reorganize component folder structure
- Remove webkit autofill hack
- Update license parser to support esm sub modules
- Reorganize identifier webapp
- Update c-r-a, kpop and dependencies
- Clean up linter warnings
- Merge branch 'embedding' of https://github.com/IljaN/konnect
- Merge branch 'bugfix/dynamic-port-redirect-native-clients' of https://github.com/DeepDiver1975/konnect
- Make konnect usable as library
- Only lint changes, to increase visibility of newly introduced issues
- Allow dynamic ports in redirect uri for native clients
- Add build arg for explict version selection for Docker build
- Update third party dependencies
- Fix unhandled error
- Log initialiation error when external auth fails to initialize
- Fix spelling mistakes
## v0.28.1 (2019-12-16)
- Update oidc-go to fix pkce Base64URL padding
## v0.28.0 (2019-12-02)
- Update third party modules
- Update kcc-go to v5
## v0.27.0 (2019-11-25)
- Relax linting requirement
- Update dependencies to their latest minor releases
- Update 3rd party dependencies
- Use Go modules instead of Go dep
- Set SameSite=None for all cookies
- Build with Go 1.13.4
## v0.26.0 (2019-11-11)
- Strip issuer subpath for OIDC url endpoints
- Force prompt=none for sencodary authorize after external authority auth
- Avoid error when identifier backend resolve cannot find a user
- Update curl to fix building of container image
- Build with Go 1.13.3
## v0.25.3 (2019-10-23)
- Fix cookie backend claims context
- Ensure BASE in fmt and check targets
- Add a list of technologies used
## v0.25.2 (2019-09-30)
- Build with Go 1.13.1
## v0.25.1 (2019-09-11)
- Update Docker entrypoint for metrics listener
- Expose metrics port for Docker containers
## v0.25.0 (2019-09-11)
- Build with Go 1.13 and update minimal Go version to 1.13
- Add usage survey block to README
- Add automatic survey reporting
- Add basic metrics
## v0.24.2 (2019-09-05)
- Merge pull request [#112](https://github.com/libregraph/lico/issues/112/) in KC/konnect from ~GITCOMMIT/konnect:master to master
## v0.24.1 (2019-09-04)
- Enable Icelandic translation, and avoid loading untranslated catalogs
- Update kpop to 0.24.5
- Translated using Weblate (Icelandic)
- Add args to changelog target
- Update kpop to 0.20.4
- Update list of enabled languages
- Add Hindi
- rename language
- Translated using Weblate (Dutch)
- Translated using Weblate (Russian)
- Translated using Weblate (Norwegian Bokmål)
- Translated using Weblate (French)
- Translated using Weblate (Portuguese (Portugal))
- Translated using Weblate (Portuguese (Portugal))
- Translated using Weblate (Norwegian Bokmål)
- Translated using Weblate (Russian)
- Cleanup Dockerfile
- Fixup headlines
## v0.24.0 (2019-07-10)
- Update dep to v0.5.4
- Update kcc-go and dependencies
## v0.23.6 (2019-07-09)
- Add healthcheck success output
- Update Dockerfiles for best practices
- Avoid trying to load a key with empty filename
- Add healthcheck sub command
- Bump diff from 3.4.0 to 3.5.0 in /identifier
- Handle redirect_uri parse error in client registration
## v0.23.5 (2019-06-12)
- Update kcc-go to 4.0.0 (and dependencies)
- Use Apache-2.0 license
- Deduplicate yarn.lock
- Bump handlebars from 4.0.11 to 4.1.2 in /identifier
- Bump clean-css from 4.1.9 to 4.1.11 in /identifier
- Bump axios from 0.16.2 to 0.18.1 in /identifier
- Bump sshpk from 1.13.1 to 1.16.1 in /identifier
## v0.23.4 (2019-05-21)
- Avoid breaking on startup when starting with empty scopes definitions
## v0.23.3 (2019-05-10)
- Fix a problem where welcome page would not display
## v0.23.2 (2019-05-10)
- Avoid remove of empty keyframes for autoFill detection
- Properly detect Chrome auto fill in login form fields
## v0.23.1 (2019-05-09)
- Use correct dep download URL
- Ensure JSON translations are not empty on fresh build
- Build with Go 1.12 and use latest dep tool
## v0.23.0 (2019-05-09)
- Update js license ranger to include notices
- Optimize use of visual white space
- Update kpop and migrage typography to new variants
- Enable nl and ru languages in production build
- Translated using Weblate (Dutch)
- Rebuild translation catalogs
- Add stats target for i18n
- Rebuild translations and translate to German
- Make it possible to translate built in scope descriptions
- Always allow merge to run
- Add language selector
- Only leave actually translated languages enabled in production builds
- Merge translation files and fix German typos
- Update kpop
- Correctly register pt-PT
- Update kpop and react-scripts
- Slightly imporve Material-UI styles
- Update react-router to 5.0.0
- Update Material-UI dependency to latest
- Update React to 18.8.6
- Do not start browser when in dev mode
- Replace __PATH_PREFIX__ with sane value in dev mode
- Change license to Apache License 2.0
## v0.22.0 (2019-04-26)
- Add origins key to web client examples
- Add hint that Konnect has learned to load JSON Web Keys
- Update external Kopano dependencies
- Include NOTICE files in 3rdparty-LICENSES.md
- Log default OIDC provider signing details
- Implement support for EdDSA keys
- Fix typos
- Add TLS client auth support for kc backend
- Setup kcc default HTTP client
- Unify HTTP client settings and setup
- Add support to set URI base path
- Translated using Weblate (Portuguese (Portugal))
- Translated using Weblate (Norwegian Bokmål)
- Translated using Weblate (Russian)
- Update Go dependencies
- Add threadsafe authority discovery support
- Only log unhandled inner identity manager errors
- Only compare hostname (not the port) for native clients
- Only enable default external authority
- Fixup yaml config
- Set RSA-PSS salt length for all RSA-PSS JWT algs always
- Add OAuth2 RP support to identifier
- Add examples for remove debugging and IDE
- Ignore debug build results
- Ignore .vscode for people using it
- Integrate Delve debugger support via `make dlv`
- Use Go report card batch
- Add Go report card
- Add godoc entry point with import annotation
- Improve docs, mark cookie backend as testing only
- Add reference for OpenID Connect dynamic client registration spec
## v0.21.0 (2019-03-24)
- Add dynamic client registration configuration support
- Validate client secrets of dynamically registered clients
- Add commandline parameter to allow dynamic client registration
- Use prefix to identitfy dynamic clients ids
- Properly pass on claims scopes on auth redirect
- Implement OpenID Connect Dynamic Client Registration 1.0
- Add cross references to implemented standards
## v0.20.0 (2019-03-15)
- Add support for preferred_username claim
- Implement PKCE code challenges as defined in RFC 7636
- Add support for konnect/id scope with LDAP backends
- Make LDAP subject source configurable
- Improve DN to sub conversion to clarify code
- Fix up --use parameter in jwk-from-pem util
- update Alpine base
## v0.19.1 (2019-02-06)
- Show details and print OK for make check
- Add client guest flag to configuration and bin script
## v0.19.0 (2019-02-06)
- Include registration and scopes yaml examples in dist tarball
- Make OIDC authorize session available early
- Add utils sub command for pem2jwk conversion
- Correct some spelling errors in configuration comments
- Support trust for trusted clients using guest identity
- Support trusted client scopes in secure oidc request
## v0.18.0 (2019-01-22)
- Bring back mandatory identity claims for ldap identifier backend
- Allow startup without guest manager
- Allow empty user claims in identifier
- Cleanup identifier logon claims and comments
- Bump base copyright years to 2019
- Build with Node 10
- Migrate from Glide to Dep
- Use blake2b implementation from golang.org/x/crypto
## v0.17.0 (2019-01-22)
- Konnect now requires Go 1.10
- Add sanity checks for user entry IDs
- Support internal claims for identifier backends
- Add multi server support for kc backend
- Add support to return request provided claims in ID token and userinfo
- Add possibility to pass thru claims from request to tokens
- Add request claims as authorized claims for all managers
- Add jti claim to access and refresh tokens
- Add OIDC endsession support for guest users via session
- Support guest users via signed claims authorize request
- Add OIDC invalid_request_object error and use accordingly
- Add support for the auth_time OIDC claim request
- Add validation for the sub requested claim
- OIDC authorize claims parameter support (1/2)
- OIDC authorize claims parameter support (1/2)
- Add support for client jwks in client registartion
- Implement support for request objects with OIDC authorize
- Always offer all supported ID token signing alg values
## v0.16.1 (2018-11-30)
- Fix startup problem without scopes conf
## v0.16.0 (2018-11-30)
- Extend identifier API docs by added fields of hello response
- Report and allow scopes which are configured in scopes conf
- Add new scopes configuration file to config and bin script
- Add scopes.yaml configuration file
- Move scope meta data to backend
- Consolidate publicate scope definition
- Log correct error after SSOLogon response
## v0.15.0 (2018-10-31)
- docs: Add OpenAPI 3 specification for the Konnect Identifier REST API
- Translated using Weblate (German)
- build: Fetch and include identifier 3rd party licenses in dist
- Use Go 1.11 in Jenkins
- identifier: Full German translation
- Add a bunch of languages for translation
- Fixup gofmt
- identifier: Add i18n support for dynamic error messages
- identifier: Add i18n for identifier web app
- identifier: Add gear for i18n
- identifier: Make identifier screens responsive
- Remove docs not relevant for konnect
## v0.14.4 (2018-10-16)
- Use archiveArtifacts instead of deprecated archive step
- Use golint from new location
- identifier: Allow unset of logon cookie without user
- ldap: Compare LDAP attributes case insensitive
## v0.14.3 (2018-09-28)
- Update build checks
- Update yarn.lock
## v0.14.2 (2018-09-28)
- scripts: Reverse signing_kid check
- scripts: Ensure correct owner when creating paths
## v0.14.1 (2018-09-26)
- Remove obsolete use of external environment files
- Fix possible race in session cleanup
## v0.14.0 (2018-09-21)
- Refuse to start with low exponent RSA keys in RS signing mode
- Use RSA-PSS (PS256) as JWT alg by default
## v0.13.1 (2018-09-19)
- oidc: Use correct Salt length with RSA-PSS signatures
## v0.13.0 (2018-09-17)
- oidc, identifier: Use kcoidc auth to kc for kc sessions
## v0.12.0 (2018-09-12)
- oidc: Allow change of signing method
- oidc: Allow additional validations keys
- Integrate kc session support to docs and scripts
- identifier: Add configuration for kc session timeout
- identifier, oidc: Add support for backend identity provider sessions
- Update svg syntax
- identifier: Set random NONCE in CSP and HTML
- Add missing session API endpoint to Caddyfile examples
## v0.11.2 (2018-09-07)
- smaller typo corrections
## v0.11.1 (2018-09-07)
- Fix end session endpoint subject verify
- Remove forgotten debug
## v0.11.0 (2018-09-06)
- oidc: Make subject URL safe by default
- identifier: Update react-scripts to 1.1.5
- oidc: Implement `sid` ID Token claim
- oidc: Implement browser state and session state
- Increase no-file limit to infinite
## v0.10.2 (2018-08-29)
- identifier: Use new favicon built from svg
- identifier: Update to kpop 0.9.2 and dependencies
- provider: Ensure to verify authentication request
## v0.10.1 (2018-08-21)
- Add setup subcommand to binscript
## v0.10.0 (2018-08-17)
- Include scripts in dist tarball
- Run Jenkins with Go 1.10
- Add log-level to config and avoid double timestamp for systemd
- Add commandline args for log output control
- Add systemd unit with runner script and config
- Move rkt exaples to README
## v0.9.0 (2018-08-01)
- identifier: Add some TODO comments
- oidc: Add support for additional claims in ID Token
- oidc: Return scope value with authorize response
- oidc: Add support for additional userinfo claims
## v0.8.0 (2018-07-27)
- oidc: Add support for url-safe sub via scope
## v0.7.0 (2018-07-17)
- Remove redux debug logging from production builds
- Use PureComponent in base app
- Update to kpop 0.5 and Material-UI 1
- identifier: Add text labels for new scopes
- Implement scope limitation
- Remove debug
- Cleanup scope structs
- oidc: Add all claims to context
## v0.6.0 (2018-05-28)
- Add checks and consent to end session support
- Allow configuration of client secrets
- Implement endsession endpoint
- identifier: Fix undefined link in consent screen
- identifier: Update style to kpop and kopanoBlue
- identifier: Remove tap plugin
- identifier: Use kpop components
- identifier: Add autoComplete attribute to login
- identifier: Add build version information and favicon
- identifier: Bump React and Material-UI versions
## v0.5.5 (2018-04-11)
- Add identifier-registration parameter to services
## v0.5.4 (2018-04-09)
- provider: Support redirect_uri values with query
## v0.5.3 (2018-04-05)
- identifier: Use correct no_uid_auth flag for logon to kc
## v0.5.2 (2018-04-04)
- docker: Allow Docker to switch user at runtime
- docker: Make it possible to load secrets from custom location
- identifier: Use no_uid_auth flag for logon to kc
- Remove forgotten debug logging
## v0.5.1 (2018-03-23)
- Docker: Support additional ARGS via environment
- Add hints for unix user required for kc backend
- Fix Docker examples so they actually work
## v0.5.0 (2018-03-16)
- server: Disable HTTP request log by default
- Add instructions for client registry conf
- identifier: Add Client registry and validation
- fix link to openid spec
- Use port 3001 for development
- Update build parameters for Go 1.10 compatibility
- Update README to include Docker and dependencies
- Update to Go 1.9 and Glide 0.13.1
- Add 3rd party license information
- Never fail on junit in post state
- Do not run lint on normal build
- Fixed a typo (Konano > Kopano)
## v0.4.1 (2018-02-09)
- provider: Allow the OAuth2 token flow
- identifier: Fix select_account mode
- Update release download link
- Fill default parameters for cookie backend
## v0.4.0 (2018-01-30)
- Add Dockerfile.release
- Add Dockerfile
- identifier: Use properties to retrieve userdata
- fix typo on readme
- identifier: Implement family_name and given_name
- identifier: Add UUID decode support to ldap uuid
- identifier: LDAP descriptors are case insensitive
- identifier: Implement uuid attribute support
- identifier: Clean data from store on logoff
- identifier: add overlay support with message
- identifier: use augmenting teamwork background only
- identifier: Update background to augmenting teamwork
- identifier: Properlu handle LDAP search not found
- identifier: Properly handle LDAP bootstrap errors
## v0.3.0 (2018-01-12)
- Refactor bootstrap/launch code
- Add support for auth_time claim in ID Token
- Update example scripts to use the new parameters
- Remove --insecure parameter from examples
- Remove double claim validation
- identifier: Remove re-logon without password
- Add support to load PKCS[#8](https://github.com/libregraph/lico/issues/8/) keys
- Load all keys from file
- Add support for trusted proxies
- identifier: Store logon time and validate max age
- identifier: Add LDAP rate limiter
- identifier: Implement LDAP backend
- Add comments about authorized scopes
- Make older golint happy
- Update README
- Fix whitespace in Caddyfiles
- Identifier: use SYSTEM as KC username default
- Update Caddyfile to be a real example
- Use unpadded Base64URL encoding for left-most hash
- Update docs to reflect plugin
- Add API overview graph
- Disable service worker
- Integrate redux into service worker
## v0.2.2 (2017-11-29)
- Fix URLs extrated from CSS
## v0.2.1 (2017-11-29)
- Remove v prefix from version number
## v0.2.0 (2017-11-29)
- Bump up Loading a litte so it fits on low height screens better
- Use inline blurred svg thumbnail background
- Use webpack with code splitting
- Fix support for service worker fetching index.html
- Report additional supported scopes
- Allow CORS for discovery docs
- Build identifier webapp by default
- Include idenfier webapp in dist
- Fixup systemd service
- Add Makefile for identifier client
- Update rkt builder and services for kc backend
- Add implicit trust for clients on the iss URI
- Fixup identifier HTML page server routes
- Add secure default CSP to HTML handler
- Fixup: loading is now a string, no longer bool
- Handle offline_access scope filtering
- Add support to show multiple scopes
- Use redirect as component
- Allow identifier users to be included in tokens
- Split up stuff into multiple files
- Use unique component class names
- Allow identifier users to be included in tokens
- Add some hardcoded clients for testing
- Reset errors and loading from choose to login
- Set prompt=none when identifier is done
- Fix prompt=login login
- Implement proper loading state for consent ui
- Implement consent cancel
- Properly retrieve and pass through displayName
- Only show account selector when prompt requests it
- WIP: implement consent via direct identifier flows
## v0.1.0 (2017-11-27)
- Only allow continue= values which begin with location.origin
- Update README for backends
- Ignore no-cookie error
- Add support for Firefox
- Implement welcome screen and logoff ui
- Set Referer-Policy header
- Split up the monster
- Move hardcoded defaults to config
- Add logoff API endpoint
- Add cookie checks for logon and hello
- Fix linter errors and unit tests
- Move general code to utils
- Implement identifier and kc backend
- Move config to seperate package
- Ignore /examples folder
- Merge pull request [#6](https://github.com/libregraph/lico/issues/6/) in KC/konnect from ~SEISENMANN/konnect:longsleep-jenkinsfile to master
- Add Jenkinsfile
- Add aci builder and systemd service
## v0.0.1 (2017-10-02)
- Add docs abourt key and secret parameter
- Fix README to use correct bin location
- Merge pull request [#5](https://github.com/libregraph/lico/issues/5/) in KC/konnect from ~SEISENMANN/konnect:longsleep-kw-sign-in to master
- Add support for KW sign-in form
- Merge pull request [#4](https://github.com/libregraph/lico/issues/4/) in KC/konnect from ~SEISENMANN/konnect:longsleep-use-lowercase-cmdline-params to master
- Use only lower case commandline arguments
- Merge pull request [#3](https://github.com/libregraph/lico/issues/3/) in KC/konnect from ~SEISENMANN/konnect:longsleep-use-external-rndm to master
- Use rndm from external module
- Build static without cgo by default
- Add Makefile
- Use seperate listener, add log message when listening started
- Put local imports last
- Use build date in version command
- Add X-Forwarded-Prefix to Caddyfile
- Merge pull request [#2](https://github.com/libregraph/lico/issues/2/) in KC/konnect from ~SEISENMANN/konnect:longsleep-caddyfile to master
- Add example Caddyfile
- Move random helpers to own subpackage
- Merge pull request [#3](https://github.com/libregraph/lico/issues/3/) in ~SEISENMANN/konnect from longsleep-konnect-id-scope to master
- Implement konnect/id scope
- Update dependencies
- Enable code flows in discovery document
- Support --secret parameter value as hex
- Update README with newly added parameters
- Support identity claims in refresh tokens
- Merge pull request [#1](https://github.com/libregraph/lico/issues/1/) in ~SEISENMANN/konnect from longsleep-encrypt-cookies-in-at to master
- Add encryption manager
- Use nacl.secretbox for cookies encryption
- Prepare encryption of cookies value in at
- Move refresh token implementation to konnect
- Move kc claims to konnect package
- Remove obsolete OPTION handler
- Add support for insecure TLS client connections
- Fix typo in example users - sorry Ford, i thought you were perfect
- Add option to limit cookie pass through to know names
- Store cookie value in access token
- Add jwks.json endpoint
- Use subject as user id identifier everywhere
- Add userinfo endpoint with cors
- Add token endpoint with cors
- Implement code flow support
- Use cookies and users compatible with minioidc
- Add support for sub path reverse proxy mode
- Add Python and YAML to .editorconfig
- Add cookie backend support
- Add cookie identity manager
- Add more commandline flags
- Add key loading
- Add unit tests for provider
- Remove forgotten debug
- Refactor server launch code
- Prepare serve code refactorization
- Simplify
- Add dummy user backend for testing
- Add .well-known discovery endpoint
- Add OIDC basic implementation including authorize endpoint
- Add references to other implementations
- Use glide helper for unit tests
- Add health-check handler with unit tests
- Add minimal README, tl;dr only for now
- Add vendoring and dependency locks with Glide
- Add initial server stub with commandline flags, logger and version
- Initial commit

53
vendor/github.com/libregraph/lico/Caddyfile.dev generated vendored Normal file
View File

@@ -0,0 +1,53 @@
# Example Caddyfile to use with https://caddyserver.com
#
# This assumes Konnect is running with identifier on 127.0.0.1:8777. In addition
# for development, the identifier is used directly from webpack-dev-server
# running on 127.0.0.1:3001. Additional examples are included for third party
# login provides which use cookie passthrough backend.
*:8443 {
errors stderr
log stdout
tls self_signed
# konnect oidc
proxy /.well-known/openid-configuration 127.0.0.1:8777
proxy /konnect/v1/jwks.json 127.0.0.1:8777
proxy /konnect/v1/token 127.0.0.1:8777
proxy /konnect/v1/userinfo 127.0.0.1:8777
proxy /konnect/v1/static 127.0.0.1:8777
proxy /konnect/v1/session 127.0.0.1:8777
proxy /konnect/v1/register 127.0.0.1:8777
# konnect identifier development via webpack-dev-server
proxy /signin/v1/ 127.0.0.1:3001 {
header_downstream Cache-Control "no-cache, max-age=0, public"
header_downstream Referrer-Policy origin
header_downstream Content-Security-Policy "object-src 'none'; script-src 'self'; base-uri 'none'; frame-ancestors 'none';"
}
proxy /ws 127.0.0.1:3001 {
websocket
}
proxy /static 127.0.0.1:3001
proxy /signin/v1/identifier/_/ 127.0.0.1:8777 {
transparent
}
# konnect identifier login area
proxy /signin/ 127.0.0.1:8777 {
transparent
}
# third party login area provider example
# proxy /provider/simple 127.0.0.1:8999
# konnect authorize endpoint below third party login area provider
#proxy /provider/simple/konnect/v1/authorize 127.0.0.1:8777 {
# without /provider/simple
# header_upstream X-Forwarded-Prefix /provider/simple
#}
# konnect cookieserver, start with python3 ./examples/cookieserver.py 8088
#proxy /cookieserver/simple-userinfo 127.0.0.1:8088
}

24
vendor/github.com/libregraph/lico/Caddyfile.example generated vendored Normal file
View File

@@ -0,0 +1,24 @@
# Example Caddyfile to use with https://caddyserver.com
#
# This assumes Konnect is running with identifier on 127.0.0.1:8777.
*:8443 {
errors stderr
log stdout
tls self_signed
# konnect oidc
proxy /.well-known/openid-configuration 127.0.0.1:8777
proxy /konnect/v1/jwks.json 127.0.0.1:8777
proxy /konnect/v1/token 127.0.0.1:8777
proxy /konnect/v1/userinfo 127.0.0.1:8777
proxy /konnect/v1/static 127.0.0.1:8777
proxy /konnect/v1/session 127.0.0.1:8777
proxy /konnect/v1/register 127.0.0.1:8777
# konnect identifier login area
proxy /signin/ 127.0.0.1:8777 {
transparent
}
}

59
vendor/github.com/libregraph/lico/Dockerfile.build generated vendored Normal file
View File

@@ -0,0 +1,59 @@
#
# Copyright 2019 Kopano and its licensors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3 or
# later, as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
FROM golang:1.17.2-buster
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ARG GOLANGCI_LINT_TAG=v1.23.8
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 \
github.com/axw/gocov/... \
github.com/AlekSi/gocov-xml \
github.com/wadey/gocovmerge \
&& go clean -cache && rm -rf /root/go
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
gettext-base \
imagemagick \
scour \
nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
yarn \
&& 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
vendor/github.com/libregraph/lico/LICENSE.txt generated vendored Normal file
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.

214
vendor/github.com/libregraph/lico/Makefile generated vendored Normal file
View File

@@ -0,0 +1,214 @@
PACKAGE = github.com/libregraph/lico
PACKAGE_NAME = libregraph-$(shell basename $(PACKAGE))
# Tools
GO ?= go
GOFMT ?= gofmt
GOLINT ?= golangci-lint
DLV ?= dlv
GO2XUNIT ?= go2xunit
GOCOV ?= gocov
GOCOVXML ?= gocov-xml
GOCOVMERGE ?= gocovmerge
CHGLOG ?= git-chglog
# Cgo
CGO_ENABLED ?= 0
# 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/" -type d))))
TIMEOUT = 30
GOLINT_ARGS ?= --new
# Debug variables
DLV_APIVERSION ?= 2
DLV_ARGS ?=
DLV_EXECUTABLE ?= bin/licod
DLV_ATTACH_PID ?= $(shell pgrep -f $(DLV_EXECUTABLE))
# Build
LDFLAGS ?= -s -w
ASMFLAGS ?=
GCFLAGS ?=
.PHONY: all
all: vendor | $(CMDS) identifier-webapp
.PHONY: commands
commands: $(CMDS)
.PHONY: $(CMDS)
$(CMDS): vendor ; $(info building $@ ...) @
$(GO) build \
-mod vendor \
-trimpath \
-tags release \
-buildmode=exe \
-asmflags '$(ASMFLAGS)' \
-gcflags '$(GCFLAGS)' \
-ldflags '$(LDFLAGS) -buildid=reproducible/$(VERSION) -X $(PACKAGE)/version.Version=$(VERSION) -X $(PACKAGE)/version.BuildDate=$(DATE) -extldflags -static' \
-o bin/$(notdir $@) ./$@
.PHONY: identifier-webapp
identifier-webapp:
$(MAKE) -C identifier build
# Helpers
.PHONY: lint
lint: vendor ; $(info running $(GOLINT) ...) @
$(GOLINT) run $(GOLINT_ARGS)
$(MAKE) -C identifier lint
.PHONY: lint-checkstyle
lint-checkstyle: vendor ; $(info running $(GOLINT) checkstyle ...) @
@mkdir -p test
$(GOLINT) run $(GOLINT_ARGS) --out-format checkstyle --issues-exit-code 0 > test/tests.lint.xml
$(MAKE) -C identifier lint-checkstyle
.PHONY: fulllint
fulllint: GOLINT_ARGS=
fulllint: lint
.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=1 $(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=1 $(GO) test -timeout $(TIMEOUT)s $(ARGS) -v $(TESTPKGS) | tee test/tests.output
test -s test/tests.output && $(GO2XUNIT) -fail -input test/tests.output -output test/tests.xml
COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out
COVERAGE_XML = $(COVERAGE_DIR)/coverage.xml
COVERAGE_HTML = $(COVERAGE_DIR)/coverage.html
.PHONY: test-coverage
test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
test-coverage: ; $(info running coverage tests ...)
@mkdir -p $(COVERAGE_DIR)/coverage
@rm -f test/tests.output
@for pkg in $(TESTPKGS); do \
CGO_ENABLED=1 $(GO) test -timeout $(TIMEOUT)s -v \
-coverpkg=$$($(GO) list -mod=readonly -f '{{ join .Deps "\n" }}' $$pkg | \
grep '^$(PACKAGE)/' | grep -v '^$(PACKAGE)/vendor/' | \
tr '\n' ',')$$pkg \
-covermode=atomic \
-coverprofile="$(COVERAGE_DIR)/coverage/`echo $$pkg | tr "/" "-"`.cover" $$pkg | tee -a test/tests.output ;\
done
@$(GO2XUNIT) -fail -input test/tests.output -output test/tests.xml
@$(GOCOVMERGE) $(COVERAGE_DIR)/coverage/*.cover > $(COVERAGE_PROFILE)
@$(GO) tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML)
@$(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML)
# Debug
.PHONY: dlv
dlv: ; $(info attaching Delve debugger ...)
$(DLV) attach --api-version=$(DLV_APIVERSION) $(DLV_ARGS) $(DLV_ATTACH_PID) $(DLV_EXECUTABLE)
# Mod
.PHONY: go.sum
go.sum: go.mod ; $(info updating dependencies ...)
@$(GO) mod tidy -v
@touch $@
.PHONY: vendor
vendor: go.mod ; $(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
make -s -C identifier licenses >> $(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"
@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 ../*.yaml.in "${PACKAGE_NAME}-${VERSION}" && \
cp -avf ../bin/* "${PACKAGE_NAME}-${VERSION}" && \
cp -avr ../identifier/build "${PACKAGE_NAME}-${VERSION}/identifier-webapp" && \
cp -avf ../scripts/licod.binscript "${PACKAGE_NAME}-${VERSION}/scripts" && \
cp -avf ../scripts/licod.service "${PACKAGE_NAME}-${VERSION}/scripts" && \
cp -avf ../scripts/licod.cfg "${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)
# Rest
.PHONY: clean
clean: ; $(info cleaning ...) @
@rm -rf bin
@rm -rf test/test.*
@$(MAKE) -C identifier clean
.PHONY: version
version:
@echo $(VERSION)

5
vendor/github.com/libregraph/lico/NOTICE.txt generated vendored Normal file
View File

@@ -0,0 +1,5 @@
LibreGraph Connect
Copyright 2017-2021 Kopano and its licensors
This product includes software developed at
Kopano (https://kopano.com/).

256
vendor/github.com/libregraph/lico/README.md generated vendored Normal file
View File

@@ -0,0 +1,256 @@
# LibreGraph Connect
LibreGraph Connect implements an [OpenID provider](http://openid.net/specs/openid-connect-core-1_0.html)
(OP) with integrated web login and consent forms.
[![Go Report Card](https://goreportcard.com/badge/github.com/libregraph/lico)](https://goreportcard.com/report/github.com/libregraph/lico)
LibreGraph Connect has it origin in Kopano Konnect and is meant as its vendor
agnostic successor.
## Technologies
- Go
- React
## Standards supported by Lico
Lico provides services based on open standards. To get you an idea what
Lico can do and how you could use it, this section lists the
[OpenID Connect](https://openid.net/connect/) standards which are implemented.
- https://openid.net/specs/openid-connect-core-1_0.html
- https://openid.net/specs/openid-connect-discovery-1_0.html
- https://openid.net/specs/openid-connect-frontchannel-1_0.html
- https://openid.net/specs/openid-connect-session-1_0.html
- https://openid.net/specs/openid-connect-registration-1_0.html
Furthermore the following extensions/base specifications extend, define and
combine the implementation details.
- https://tools.ietf.org/html/rfc6749
- https://tools.ietf.org/html/rfc7517
- https://tools.ietf.org/html/rfc7519
- https://tools.ietf.org/html/rfc7636
- https://tools.ietf.org/html/rfc7693
- https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html
- https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html
- https://www.iana.org/assignments/jose/jose.xhtml
- https://nacl.cr.yp.to/secretbox.html
## Build dependencies
Make sure you have Go 1.18 or later installed. This project uses Go Modules.
Lico also includes a modern web app which requires a couple of additional
build dependencies which are furthermore also assumed to be in your $PATH.
- yarn - [Yarn](https://yarnpkg.com)
- convert, identify - [Imagemagick](https://www.imagemagick.org)
- scour - [Scour](https://github.com/scour-project/scour)
To build Lico, a `Makefile` is provided, which requires [make](https://www.gnu.org/software/make/manual/make.html).
When building, third party dependencies will tried to be fetched from the Internet
if not there already.
## Building from source
```
git clone <THIS-PROJECT> lico
cd lico
make
```
### Optional build dependencies
Some optional build dependencies are required for linting and continuous
integration. Those tools are mostly used by make to perform various tasks and
are expected to be found in your $PATH.
- golangci-lint - [golangci-lint](https://github.com/golangci/golangci-lint)
- go2xunit - [go2xunit](https://github.com/tebeka/go2xunit)
- gocov - [gocov](https://github.com/axw/gocov)
- gocov-xml - [gocov-xml](https://github.com/AlekSi/gocov-xml)
- gocovmerge - [gocovmerge](https://github.com/wadey/gocovmerge)
### Build with Docker
```
docker build -t licod-builder -f Dockerfile.build .
docker run -it --rm -u $(id -u):$(id -g) -v $(pwd):/build licod-builder
```
## Running Lico
Lico can provide user login based on available backends.
All backends require certain general parameters to be present. Create a RSA
key-pair file with `openssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:4096`
and provide the key file with the `--signing-private-key` parameter. Lico can
load PEM encoded PKCS#1 and PKCS#8 key files and JSON Web Keys from `.json` files
If you skip this, Lico will create a random non-persistent RSA key on startup.
To encrypt certain values, Lico needs a secure encryption key. Create a
suitable key of 32 bytes with `openssl rand -out encryption.key 32` and provide
the full path to that file via the `--encryption-secret` parameter. If you skip
this, Lico will generate a random key on startup.
To run a functional OpenID Connect provider, an issuer identifier is required.
The `iss` is a full qualified https:// URI pointing to the web server which
serves the requests to Lico (example: https://example.com). Provide the
Issuer Identifier with the `--iss` parametter when starting Lico.
Furthermore to allow clients to utilize the Lico services, clients need to
be known/registered. For now Lico uses a static configuration file which
allows clients and their allowed urls to be registered. See the the example at
`identifier-registration.yaml.in`. Copy and modify that file to include all
the clients which should be able to use OpenID Connect and/or OAuth2 and start
Lico with the `--identifier-registration-conf` parameter pointing to that
file. Without any explicitly registered clients, Lico will only accept clients
which redirect to an URI which starts with the value provided with the `--iss`
parameter.
### Lico cryptography and validation
A tool can be used to create keys for Lico and also to validate tokens to
ensure correct operation is [Step CLI](https://github.com/smallstep/cli). This
helps since OpenSSL is not able to create or validate all of the different key
formats, ciphers and curves which are supported by Lico.
Here are some examples relevant for Lico.
```
step crypto keypair 1-rsa.pub 1-rsa.pem \
--kty RSA --size 4096 --no-password --insecure
```
```
step crypto keypair 1-ecdsa-p-256.pub 1-ecdsa-p-256.pem \
--kty EC --curve P-256 --no-password --insecure
```
```
step crypto jwk create 1-eddsa-ed25519.pub.json 1-eddsa-ed25519.key.json \
-kty OKP --crv Ed25519 --no-password --insecure
```
```
echo $TOKEN_VALUE | step crypto jwt verify --iss $ISS \
--aud playground-trusted.js --jwks $ISS/konnect/v1/jwks.json
```
### URL endpoints
Take a look at `Caddyfile.example` on the URL endpoints provided by Lico and
how to expose them through a TLS proxy.
The base URL of the frontend proxy is what will become the value of the `--iss`
parameter when starting up Lico. OIDC requires the Issuer Identifier to be
secure (https:// required).
### LibreGraph backend
Generic backend support is available through the LibreGraph API. Any service can
provide the required endpoints and Lico connects to them.
```
export LIBREGRAPH_URI=http://your-backend.local:5050
bin/licod serve --listen=127.0.0.1:8777 \
--iss=https://mylico.local \
libregraph
```
### LDAP backend
This assumes that Lico can directly connect to an LDAP server via TCP.
```
export LDAP_URI=ldap://myldap.local:389
export LDAP_BINDDN="cn=admin,dc=example,dc=local"
export LDAP_BINDPW="its-a-secret"
export LDAP_BASEDN="dc=example,dc=local"
export LDAP_SCOPE=sub
export LDAP_LOGIN_ATTRIBUTE=uid
export LDAP_EMAIL_ATTRIBUTE=mail
export LDAP_NAME_ATTRIBUTE=cn
export LDAP_UUID_ATTRIBUTE=uidNumber
export LDAP_UUID_ATTRIBUTE_TYPE=text
export LDAP_FILTER="(objectClass=organizationalPerson)"
bin/licod serve --listen=127.0.0.1:8777 \
--iss=https://mylico.local \
ldap
```
### Build Lico Docker image
This project includes a `Dockerfile` which can be used to build a Docker
container from the locally build version. Similarly the `Dockerfile.release`
builds the Docker image locally from the latest release download.
```
docker build -t licod .
```
```
docker build -f Dockerfile.release -t licod .
```
## Run unit tests
```
make test
```
## Development
As Lico includes a web application (identifier), a `Caddyfile.dev` file is
provided which exposes the identifier's web application directly via a
webpack dev server.
### Debugging
Lico is built stripped and without debug symbols by default. To build for
debugging, compile with additional environment variables which override/reset
build optimization like this
```
LDFLAGS="" GCFLAGS="all=-N -l" ASMFLAGS="" make cmd/licod
```
The resulting binary is not stripped and sutiable to be debugged with [Delve](https://github.com/go-delve/delve).
To connect Delve to a running Lico binary you can use the `make dlv` command.
Control its behavior via `DLV_*` environment variables. See the `Makefile` source
for details.
```
DLV_ARGS= make dlv
```
#### Remote debugging
To use remote debugging, pass additional args like this.
```
DLV_ARGS=--listen=:2345 make dlv
```
## Usage survey
By default, any running licod regularly transmits survey data to a Kopano
user survey service at https://stats.kopano.io . To disable participation, set
the environment variable `KOPANO_SURVEYCLIENT_AUTOSURVEY` to `no`.
The survey data includes system and platform information and the following
specific settings:
- Identify manager name (as selected when starting licod)
See [here](https://stash.kopano.io/projects/KGOL/repos/ksurveyclient-go) for further
documentation and customization possibilities.
## License
See `LICENSE.txt` for licensing information of this project.

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package bsguest
import (
"github.com/libregraph/lico/bootstrap"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/managers"
)
// Identity managers.
const (
identityManagerName = "guest"
)
func Register() error {
return bootstrap.RegisterIdentityManager(identityManagerName, NewIdentityManager)
}
func MustRegister() {
if err := Register(); err != nil {
panic(err)
}
}
func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
config := bs.Config()
logger := config.Config.Logger
identityManagerConfig := &identity.Config{
Logger: logger,
}
guestIdentityManager := managers.NewGuestIdentityManager(identityManagerConfig)
return guestIdentityManager, nil
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package bsldap
import (
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/libregraph/lico/bootstrap"
"github.com/libregraph/lico/identifier"
"github.com/libregraph/lico/identifier/backends/ldap"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/managers"
)
// Identity managers.
const (
identityManagerName = "ldap"
)
func Register() error {
return bootstrap.RegisterIdentityManager(identityManagerName, NewIdentityManager)
}
func MustRegister() {
if err := Register(); err != nil {
panic(err)
}
}
func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
config := bs.Config()
logger := config.Config.Logger
if config.AuthorizationEndpointURI.String() != "" {
return nil, fmt.Errorf("ldap backend is incompatible with authorization-endpoint-uri parameter")
}
config.AuthorizationEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/authorize")
if config.EndSessionEndpointURI.String() != "" {
return nil, fmt.Errorf("ldap backend is incompatible with endsession-endpoint-uri parameter")
}
config.EndSessionEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/endsession")
if config.SignInFormURI.EscapedPath() == "" {
config.SignInFormURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier")
}
if config.SignedOutURI.EscapedPath() == "" {
config.SignedOutURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/goodbye")
}
// Default LDAP attribute mappings.
attributeMapping := map[string]string{
ldap.AttributeLogin: os.Getenv("LDAP_LOGIN_ATTRIBUTE"),
ldap.AttributeEmail: os.Getenv("LDAP_EMAIL_ATTRIBUTE"),
ldap.AttributeName: os.Getenv("LDAP_NAME_ATTRIBUTE"),
ldap.AttributeFamilyName: os.Getenv("LDAP_FAMILY_NAME_ATTRIBUTE"),
ldap.AttributeGivenName: os.Getenv("LDAP_GIVEN_NAME_ATTRIBUTE"),
ldap.AttributeUUID: os.Getenv("LDAP_UUID_ATTRIBUTE"),
fmt.Sprintf("%s_type", ldap.AttributeUUID): os.Getenv("LDAP_UUID_ATTRIBUTE_TYPE"),
}
// Add optional LDAP attribute mappings.
if numericUIDAttribute := os.Getenv("LDAP_UIDNUMBER_ATTRIBUTE"); numericUIDAttribute != "" {
attributeMapping[ldap.AttributeNumericUID] = numericUIDAttribute
}
// Sub from LDAP attribute mappings.
var subMapping []string
if subMappingString := os.Getenv("LDAP_SUB_ATTRIBUTES"); subMappingString != "" {
subMapping = strings.Split(subMappingString, " ")
}
// Use a clone here to avoid changing the config of other possible users of the config.
tlsConfig := config.TLSClientConfig.Clone()
if caCertFile := os.Getenv("LDAP_TLS_CACERT"); caCertFile != "" {
if pemBytes, err := ioutil.ReadFile(caCertFile); err == nil {
rpool, _ := x509.SystemCertPool()
if rpool.AppendCertsFromPEM(pemBytes) {
tlsConfig.RootCAs = rpool
} else {
return nil, fmt.Errorf("failed to append CA certificate(s) from '%s' to pool", caCertFile)
}
} else {
return nil, fmt.Errorf("failed to read CA certificate(s) from '%s': %w", caCertFile, err)
}
}
identifierBackend, identifierErr := ldap.NewLDAPIdentifierBackend(
config.Config,
tlsConfig,
os.Getenv("LDAP_URI"),
os.Getenv("LDAP_BINDDN"),
os.Getenv("LDAP_BINDPW"),
os.Getenv("LDAP_BASEDN"),
os.Getenv("LDAP_SCOPE"),
os.Getenv("LDAP_FILTER"),
subMapping,
attributeMapping,
)
if identifierErr != nil {
return nil, fmt.Errorf("failed to create identifier backend: %v", identifierErr)
}
fullAuthorizationEndpointURL := bootstrap.WithSchemeAndHost(config.AuthorizationEndpointURI, config.IssuerIdentifierURI)
fullSignInFormURL := bootstrap.WithSchemeAndHost(config.SignInFormURI, config.IssuerIdentifierURI)
fullSignedOutEndpointURL := bootstrap.WithSchemeAndHost(config.SignedOutURI, config.IssuerIdentifierURI)
activeIdentifier, err := identifier.NewIdentifier(&identifier.Config{
Config: config.Config,
BaseURI: config.IssuerIdentifierURI,
PathPrefix: bs.MakeURIPath(bootstrap.APITypeSignin, ""),
StaticFolder: config.IdentifierClientPath,
LogonCookieName: "__Secure-KKT", // Kopano-Konnect-Token
ScopesConf: config.IdentifierScopesConf,
WebAppDisabled: config.IdentifierClientDisabled,
AuthorizationEndpointURI: fullAuthorizationEndpointURL,
SignedOutEndpointURI: fullSignedOutEndpointURL,
DefaultBannerLogo: config.IdentifierDefaultBannerLogo,
DefaultSignInPageText: config.IdentifierDefaultSignInPageText,
DefaultUsernameHintText: config.IdentifierDefaultUsernameHintText,
UILocales: config.IdentifierUILocales,
Backend: identifierBackend,
})
if err != nil {
return nil, fmt.Errorf("failed to create identifier: %v", err)
}
err = activeIdentifier.SetKey(config.EncryptionSecret)
if err != nil {
return nil, fmt.Errorf("invalid --encryption-secret parameter value for identifier: %v", err)
}
identityManagerConfig := &identity.Config{
SignInFormURI: fullSignInFormURL,
SignedOutURI: fullSignedOutEndpointURL,
Logger: logger,
ScopesSupported: config.Config.AllowedScopes,
}
identifierIdentityManager := managers.NewIdentifierIdentityManager(identityManagerConfig, activeIdentifier)
logger.Infoln("using identifier backed identity manager")
return identifierIdentityManager, nil
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2021 Kopano and its licensors
*
* 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.
*
*/
package bslibregraph
import (
"fmt"
"os"
"strings"
"github.com/cevaris/ordered_map"
"github.com/libregraph/lico/bootstrap"
"github.com/libregraph/lico/identifier"
"github.com/libregraph/lico/identifier/backends/libregraph"
"github.com/libregraph/lico/identity"
identityClients "github.com/libregraph/lico/identity/clients"
"github.com/libregraph/lico/identity/managers"
)
// Identity managers.
const (
identityManagerName = "libregraph"
)
func Register() error {
return bootstrap.RegisterIdentityManager(identityManagerName, NewIdentityManager)
}
func MustRegister() {
if err := Register(); err != nil {
panic(err)
}
}
func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
config := bs.Config()
logger := config.Config.Logger
if config.AuthorizationEndpointURI.String() != "" {
return nil, fmt.Errorf("libregraph backend is incompatible with authorization-endpoint-uri parameter")
}
config.AuthorizationEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/authorize")
if config.EndSessionEndpointURI.String() != "" {
return nil, fmt.Errorf("libregraph backend is incompatible with endsession-endpoint-uri parameter")
}
config.EndSessionEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/endsession")
if config.SignInFormURI.EscapedPath() == "" {
config.SignInFormURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier")
}
if config.SignedOutURI.EscapedPath() == "" {
config.SignedOutURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/goodbye")
}
defaultURI := os.Getenv("LIBREGRAPH_URI")
var scopedURIs *ordered_map.OrderedMap
if scopedURIsString := os.Getenv("LIBREGRAPH_SCOPED_URIS"); scopedURIsString != "" {
scopedURIs = ordered_map.NewOrderedMap()
// Format is <scope>:<url>,<scope>:<url>,...
for _, v := range strings.Split(scopedURIsString, ",") {
parts := strings.SplitN(v, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("failed to parse scoped URIs, format invalid")
}
scopedURIs.Set(parts[0], parts[1])
}
}
var clients *identityClients.Registry
if clientsRecord, ok := bs.Managers().Get("clients"); ok {
clients = clientsRecord.(*identityClients.Registry)
} else {
return nil, fmt.Errorf("clients manager not found but is required")
}
identifierBackend, identifierErr := libregraph.NewLibreGraphIdentifierBackend(
config.Config,
config.TLSClientConfig,
defaultURI,
scopedURIs,
clients,
)
if identifierErr != nil {
return nil, fmt.Errorf("failed to create identifier backend: %v", identifierErr)
}
fullAuthorizationEndpointURL := bootstrap.WithSchemeAndHost(config.AuthorizationEndpointURI, config.IssuerIdentifierURI)
fullSignInFormURL := bootstrap.WithSchemeAndHost(config.SignInFormURI, config.IssuerIdentifierURI)
fullSignedOutEndpointURL := bootstrap.WithSchemeAndHost(config.SignedOutURI, config.IssuerIdentifierURI)
activeIdentifier, err := identifier.NewIdentifier(&identifier.Config{
Config: config.Config,
BaseURI: config.IssuerIdentifierURI,
PathPrefix: bs.MakeURIPath(bootstrap.APITypeSignin, ""),
StaticFolder: config.IdentifierClientPath,
LogonCookieName: "__Secure-KKT", // Kopano-Konnect-Token
ScopesConf: config.IdentifierScopesConf,
WebAppDisabled: config.IdentifierClientDisabled,
AuthorizationEndpointURI: fullAuthorizationEndpointURL,
SignedOutEndpointURI: fullSignedOutEndpointURL,
DefaultBannerLogo: config.IdentifierDefaultBannerLogo,
DefaultSignInPageText: config.IdentifierDefaultSignInPageText,
DefaultUsernameHintText: config.IdentifierDefaultUsernameHintText,
UILocales: config.IdentifierUILocales,
Backend: identifierBackend,
})
if err != nil {
return nil, fmt.Errorf("failed to create identifier: %v", err)
}
err = activeIdentifier.SetKey(config.EncryptionSecret)
if err != nil {
return nil, fmt.Errorf("invalid --encryption-secret parameter value for identifier: %v", err)
}
identityManagerConfig := &identity.Config{
SignInFormURI: fullSignInFormURL,
SignedOutURI: fullSignedOutEndpointURL,
Logger: logger,
ScopesSupported: config.Config.AllowedScopes,
}
identifierIdentityManager := managers.NewIdentifierIdentityManager(identityManagerConfig, activeIdentifier)
logger.Infoln("using identifier backed identity manager")
return identifierIdentityManager, nil
}

View File

@@ -0,0 +1,541 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package bootstrap
import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/longsleep/rndm"
"github.com/sirupsen/logrus"
"github.com/libregraph/lico/config"
"github.com/libregraph/lico/encryption"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/managers"
oidcProvider "github.com/libregraph/lico/oidc/provider"
"github.com/libregraph/lico/utils"
)
// API types.
type APIType string
const (
APITypeKonnect APIType = "konnect"
APITypeSignin APIType = "signin"
)
// Defaults.
const (
DefaultSigningKeyID = "default"
DefaultSigningKeyBits = 2048
DefaultGuestIdentityManagerName = "guest"
)
// Bootstrap is a data structure to hold configuration required to start
// konnectd.
type Bootstrap interface {
Config() *Config
Managers() *managers.Managers
MakeURIPath(api APIType, subpath string) string
}
// Implementation of the bootstrap interface.
type bootstrap struct {
config *Config
uriBasePath string
managers *managers.Managers
}
// Config returns the bootstap configuration.
func (bs *bootstrap) Config() *Config {
return bs.config
}
// Managers returns bootstrapped identity-managers.
func (bs *bootstrap) Managers() *managers.Managers {
return bs.managers
}
// Boot is the main entry point to bootstrap the service after validating the
// given configuration. The resulting Bootstrap struct can be used to retrieve
// configured identity-managers and their respective http-handlers and config.
//
// This function should be used by consumers which want to embed this project
// as a library.
func Boot(ctx context.Context, settings *Settings, cfg *config.Config) (Bootstrap, error) {
// NOTE(longsleep): Ensure to use same salt length as the hash size.
// See https://www.ietf.org/mail-archive/web/jose/current/msg02901.html for
// reference and https://github.com/golang-jwt/jwt/v4/issues/285 for
// the issue in upstream jwt-go.
for _, alg := range []string{jwt.SigningMethodPS256.Name, jwt.SigningMethodPS384.Name, jwt.SigningMethodPS512.Name} {
sm := jwt.GetSigningMethod(alg)
if signingMethodRSAPSS, ok := sm.(*jwt.SigningMethodRSAPSS); ok {
signingMethodRSAPSS.Options.SaltLength = rsa.PSSSaltLengthEqualsHash
}
}
bs := &bootstrap{
config: &Config{
Config: cfg,
Settings: settings,
},
}
err := bs.initialize(settings)
if err != nil {
return nil, err
}
err = bs.setup(ctx, settings)
if err != nil {
return nil, err
}
return bs, nil
}
// initialize, parsed parameters from commandline with validation and adds them
// to the associated Bootstrap data.
func (bs *bootstrap) initialize(settings *Settings) error {
logger := bs.config.Config.Logger
var err error
if settings.IdentityManager == "" {
return fmt.Errorf("identity-manager argument missing, use one of kc, ldap, cookie, dummy")
}
bs.config.IssuerIdentifierURI, err = url.Parse(settings.Iss)
if err != nil {
return fmt.Errorf("invalid iss value, iss is not a valid URL), %v", err)
} else if settings.Iss == "" {
return fmt.Errorf("missing iss value, did you provide the --iss parameter?")
} else if bs.config.IssuerIdentifierURI.Scheme != "https" {
return fmt.Errorf("invalid iss value, URL must start with https://")
} else if bs.config.IssuerIdentifierURI.Host == "" {
return fmt.Errorf("invalid iss value, URL must have a host")
}
bs.uriBasePath = settings.URIBasePath
bs.config.SignInFormURI, err = url.Parse(settings.SignInURI)
if err != nil {
return fmt.Errorf("invalid sign-in URI, %v", err)
}
bs.config.SignedOutURI, err = url.Parse(settings.SignedOutURI)
if err != nil {
return fmt.Errorf("invalid signed-out URI, %v", err)
}
bs.config.AuthorizationEndpointURI, err = url.Parse(settings.AuthorizationEndpointURI)
if err != nil {
return fmt.Errorf("invalid authorization-endpoint-uri, %v", err)
}
bs.config.EndSessionEndpointURI, err = url.Parse(settings.EndsessionEndpointURI)
if err != nil {
return fmt.Errorf("invalid endsession-endpoint-uri, %v", err)
}
if settings.Insecure {
// NOTE(longsleep): This disable http2 client support. See https://github.com/golang/go/issues/14275 for reasons.
bs.config.TLSClientConfig = utils.InsecureSkipVerifyTLSConfig()
logger.Warnln("insecure mode, TLS client connections are susceptible to man-in-the-middle attacks")
} else {
bs.config.TLSClientConfig = utils.DefaultTLSConfig()
}
for _, trustedProxy := range settings.TrustedProxy {
if ip := net.ParseIP(trustedProxy); ip != nil {
bs.config.Config.TrustedProxyIPs = append(bs.config.Config.TrustedProxyIPs, &ip)
continue
}
if _, ipNet, errParseCIDR := net.ParseCIDR(trustedProxy); errParseCIDR == nil {
bs.config.Config.TrustedProxyNets = append(bs.config.Config.TrustedProxyNets, ipNet)
continue
}
}
if len(bs.config.Config.TrustedProxyIPs) > 0 {
logger.Infoln("trusted proxy IPs", bs.config.Config.TrustedProxyIPs)
}
if len(bs.config.Config.TrustedProxyNets) > 0 {
logger.Infoln("trusted proxy networks", bs.config.Config.TrustedProxyNets)
}
if len(settings.AllowScope) > 0 {
bs.config.Config.AllowedScopes = settings.AllowScope
logger.Infoln("using custom allowed OAuth 2 scopes", bs.config.Config.AllowedScopes)
}
bs.config.Config.AllowClientGuests = settings.AllowClientGuests
if bs.config.Config.AllowClientGuests {
logger.Infoln("client controlled guests are enabled")
}
bs.config.Config.AllowDynamicClientRegistration = settings.AllowDynamicClientRegistration
if bs.config.Config.AllowDynamicClientRegistration {
logger.Infoln("dynamic client registration is enabled")
}
encryptionSecretFn := settings.EncryptionSecretFile
if encryptionSecretFn != "" {
logger.WithField("file", encryptionSecretFn).Infoln("loading encryption secret from file")
bs.config.EncryptionSecret, err = ioutil.ReadFile(encryptionSecretFn)
if err != nil {
return fmt.Errorf("failed to load encryption secret from file: %v", err)
}
if len(bs.config.EncryptionSecret) != encryption.KeySize {
return fmt.Errorf("invalid encryption secret size - must be %d bytes", encryption.KeySize)
}
} else {
logger.Warnf("missing --encryption-secret parameter, using random encyption secret with %d bytes", encryption.KeySize)
bs.config.EncryptionSecret = rndm.GenerateRandomBytes(encryption.KeySize)
}
bs.config.Config.ListenAddr = settings.Listen
bs.config.IdentifierClientDisabled = settings.IdentifierClientDisabled
bs.config.IdentifierClientPath = settings.IdentifierClientPath
bs.config.IdentifierRegistrationConf = settings.IdentifierRegistrationConf
if bs.config.IdentifierRegistrationConf != "" {
bs.config.IdentifierRegistrationConf, _ = filepath.Abs(bs.config.IdentifierRegistrationConf)
if _, errStat := os.Stat(bs.config.IdentifierRegistrationConf); errStat != nil {
return fmt.Errorf("identifier-registration-conf file not found or unable to access: %v", errStat)
}
bs.config.IdentifierAuthoritiesConf = bs.config.IdentifierRegistrationConf
}
bs.config.IdentifierScopesConf = settings.IdentifierScopesConf
if bs.config.IdentifierScopesConf != "" {
bs.config.IdentifierScopesConf, _ = filepath.Abs(bs.config.IdentifierScopesConf)
if _, errStat := os.Stat(bs.config.IdentifierScopesConf); errStat != nil {
return fmt.Errorf("identifier-scopes-conf file not found or unable to access: %v", errStat)
}
}
if settings.IdentifierDefaultBannerLogo != "" {
// Load from file.
b, errRead := ioutil.ReadFile(settings.IdentifierDefaultBannerLogo)
if errRead != nil {
return fmt.Errorf("identifier-default-banner-logo failed to open: %w", errRead)
}
bs.config.IdentifierDefaultBannerLogo = b
}
if settings.IdentifierDefaultSignInPageText != "" {
bs.config.IdentifierDefaultSignInPageText = &settings.IdentifierDefaultSignInPageText
}
if settings.IdentifierDefaultUsernameHintText != "" {
bs.config.IdentifierDefaultUsernameHintText = &settings.IdentifierDefaultUsernameHintText
}
bs.config.IdentifierUILocales = settings.IdentifierUILocales
bs.config.SigningKeyID = settings.SigningKid
bs.config.Signers = make(map[string]crypto.Signer)
bs.config.Validators = make(map[string]crypto.PublicKey)
bs.config.Certificates = make(map[string][]*x509.Certificate)
signingMethodString := settings.SigningMethod
bs.config.SigningMethod = jwt.GetSigningMethod(signingMethodString)
if bs.config.SigningMethod == nil {
return fmt.Errorf("unknown signing method: %s", signingMethodString)
}
signingKeyFns := settings.SigningPrivateKeyFiles
if len(signingKeyFns) > 0 {
first := true
for _, signingKeyFn := range signingKeyFns {
logger.WithField("path", signingKeyFn).Infoln("loading signing key")
err = addSignerWithIDFromFile(signingKeyFn, "", bs)
if err != nil {
return err
}
if first {
// Also add key under the provided id.
first = false
err = addSignerWithIDFromFile(signingKeyFn, bs.config.SigningKeyID, bs)
if err != nil {
return err
}
}
}
} else {
//NOTE(longsleep): remove me - create keypair a random key pair.
sm := jwt.SigningMethodPS256
bs.config.SigningMethod = sm
logger.WithField("alg", sm.Name).Warnf("missing --signing-private-key parameter, using random %d bit signing key", DefaultSigningKeyBits)
signer, _ := rsa.GenerateKey(rand.Reader, DefaultSigningKeyBits)
bs.config.Signers[bs.config.SigningKeyID] = signer
}
// Ensure we have a signer for the things we need.
err = validateSigners(bs)
if err != nil {
return err
}
validationKeysPath := settings.ValidationKeysPath
if validationKeysPath != "" {
logger.WithField("path", validationKeysPath).Infoln("loading validation keys")
err = addValidatorsFromPath(validationKeysPath, bs)
if err != nil {
return err
}
}
bs.config.Config.HTTPTransport = utils.HTTPTransportWithTLSClientConfig(bs.config.TLSClientConfig)
bs.config.AccessTokenDurationSeconds = settings.AccessTokenDurationSeconds
if bs.config.AccessTokenDurationSeconds == 0 {
bs.config.AccessTokenDurationSeconds = 60 * 10 // 10 Minutes
}
bs.config.IDTokenDurationSeconds = settings.IDTokenDurationSeconds
if bs.config.IDTokenDurationSeconds == 0 {
bs.config.IDTokenDurationSeconds = 60 * 60 // 1 Hour
}
bs.config.RefreshTokenDurationSeconds = settings.RefreshTokenDurationSeconds
if bs.config.RefreshTokenDurationSeconds == 0 {
bs.config.RefreshTokenDurationSeconds = 60 * 60 * 24 * 365 * 3 // 3 Years
}
bs.config.DyamicClientSecretDurationSeconds = settings.DyamicClientSecretDurationSeconds
return nil
}
// setup takes care of setting up the managers based on the associated
// Bootstrap's data.
func (bs *bootstrap) setup(ctx context.Context, settings *Settings) error {
managers, err := newManagers(ctx, bs)
if err != nil {
return err
}
bs.managers = managers
identityManager, err := bs.setupIdentity(ctx, settings)
if err != nil {
return err
}
managers.Set("identity", identityManager)
guestManager, err := bs.setupGuest(ctx, identityManager)
if err != nil {
return err
}
managers.Set("guest", guestManager)
oidcProvider, err := bs.setupOIDCProvider(ctx)
if err != nil {
return err
}
managers.Set("oidc", oidcProvider)
managers.Set("handler", oidcProvider) // Use OIDC provider as default HTTP handler.
err = managers.Apply()
if err != nil {
return fmt.Errorf("failed to apply managers: %v", err)
}
// Final steps
err = oidcProvider.InitializeMetadata()
if err != nil {
return fmt.Errorf("failed to initialize provider metadata: %v", err)
}
return nil
}
func (bs *bootstrap) MakeURIPath(api APIType, subpath string) string {
subpath = strings.TrimPrefix(subpath, "/")
uriPath := ""
switch api {
case APITypeKonnect:
uriPath = fmt.Sprintf("%s/konnect/v1/%s", strings.TrimSuffix(bs.uriBasePath, "/"), subpath)
case APITypeSignin:
uriPath = fmt.Sprintf("%s/signin/v1/%s", strings.TrimSuffix(bs.uriBasePath, "/"), subpath)
default:
panic("unknown api type")
}
if subpath == "" {
uriPath = strings.TrimSuffix(uriPath, "/")
}
return uriPath
}
func (bs *bootstrap) MakeURI(api APIType, subpath string) *url.URL {
uriPath := bs.MakeURIPath(api, subpath)
uri, _ := url.Parse(bs.config.IssuerIdentifierURI.String())
uri.Path = uriPath
return uri
}
func (bs *bootstrap) setupIdentity(ctx context.Context, settings *Settings) (identity.Manager, error) {
logger := bs.config.Config.Logger
if settings.IdentityManager == "" {
return nil, fmt.Errorf("identity-manager argument missing")
}
// Identity manager.
identityManagerName := settings.IdentityManager
identityManager, err := getIdentityManagerByName(identityManagerName, bs)
if err != nil {
return nil, err
}
logger.WithFields(logrus.Fields{
"name": identityManagerName,
"scopes": identityManager.ScopesSupported(nil),
"claims": identityManager.ClaimsSupported(nil),
}).Infoln("identity manager set up")
return identityManager, nil
}
func (bs *bootstrap) setupGuest(ctx context.Context, identityManager identity.Manager) (identity.Manager, error) {
if !bs.config.Config.AllowClientGuests {
return nil, nil
}
var err error
logger := bs.config.Config.Logger
guestManager, err := getIdentityManagerByName(DefaultGuestIdentityManagerName, bs)
if err != nil {
return nil, err
}
if guestManager != nil {
logger.Infoln("identity guest manager set up")
}
return guestManager, nil
}
func (bs *bootstrap) setupOIDCProvider(ctx context.Context) (*oidcProvider.Provider, error) {
var err error
logger := bs.config.Config.Logger
sessionCookiePath, err := getCommonURLPathPrefix(bs.config.AuthorizationEndpointURI.EscapedPath(), bs.config.EndSessionEndpointURI.EscapedPath())
if err != nil {
return nil, fmt.Errorf("failed to find common URL prefix for authorize and endsession: %v", err)
}
var registrationPath = ""
if bs.config.Config.AllowDynamicClientRegistration {
registrationPath = bs.MakeURIPath(APITypeKonnect, "/register")
}
provider, err := oidcProvider.NewProvider(&oidcProvider.Config{
Config: bs.config.Config,
IssuerIdentifier: bs.config.IssuerIdentifierURI.String(),
WellKnownPath: "/.well-known/openid-configuration",
JwksPath: bs.MakeURIPath(APITypeKonnect, "/jwks.json"),
AuthorizationPath: bs.config.AuthorizationEndpointURI.EscapedPath(),
TokenPath: bs.MakeURIPath(APITypeKonnect, "/token"),
UserInfoPath: bs.MakeURIPath(APITypeKonnect, "/userinfo"),
EndSessionPath: bs.config.EndSessionEndpointURI.EscapedPath(),
CheckSessionIframePath: bs.MakeURIPath(APITypeKonnect, "/session/check-session.html"),
RegistrationPath: registrationPath,
BrowserStateCookiePath: bs.MakeURIPath(APITypeKonnect, "/session/"),
BrowserStateCookieName: "__Secure-KKBS", // Kopano-Konnect-Browser-State
SessionCookiePath: sessionCookiePath,
SessionCookieName: "__Secure-KKCS", // Kopano-Konnect-Client-Session
AccessTokenDuration: time.Duration(bs.config.AccessTokenDurationSeconds) * time.Second,
IDTokenDuration: time.Duration(bs.config.IDTokenDurationSeconds) * time.Second,
RefreshTokenDuration: time.Duration(bs.config.RefreshTokenDurationSeconds) * time.Second,
})
if err != nil {
return nil, fmt.Errorf("failed to create provider: %v", err)
}
if bs.config.SigningMethod != nil {
err = provider.SetSigningMethod(bs.config.SigningMethod)
if err != nil {
return nil, fmt.Errorf("failed to set provider signing method: %v", err)
}
}
// All add signers.
for id, signer := range bs.config.Signers {
if id == bs.config.SigningKeyID {
err = provider.SetSigningKey(id, signer)
// Always set default key.
if id != DefaultSigningKeyID {
provider.SetValidationKey(DefaultSigningKeyID, signer.Public())
}
} else {
// Set non default signers as well.
err = provider.SetSigningKey(id, signer)
}
if err != nil {
return nil, err
}
}
// Add all validators.
for id, publicKey := range bs.config.Validators {
err = provider.SetValidationKey(id, publicKey)
if err != nil {
return nil, err
}
}
// Add all certificates.
for id, certificate := range bs.config.Certificates {
err = provider.SetCertificate(id, certificate)
if err != nil {
return nil, err
}
}
sk, ok := provider.GetSigningKey(bs.config.SigningMethod)
if !ok {
return nil, fmt.Errorf("no signing key for selected signing method")
}
if bs.config.SigningKeyID == "" {
// Ensure that there is a default signing Key ID even if none was set.
provider.SetValidationKey(DefaultSigningKeyID, sk.PrivateKey.Public())
}
logger.WithFields(logrus.Fields{
"id": sk.ID,
"method": fmt.Sprintf("%T", sk.SigningMethod),
"alg": sk.SigningMethod.Alg(),
}).Infoln("oidc token signing default set up")
return provider, nil
}

67
vendor/github.com/libregraph/lico/bootstrap/config.go generated vendored Normal file
View File

@@ -0,0 +1,67 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package bootstrap
import (
"crypto"
"crypto/tls"
"crypto/x509"
"net/url"
"github.com/golang-jwt/jwt/v4"
"github.com/libregraph/lico/config"
)
// Config is a typed application config which represents the active
// bootstrap configuration.
type Config struct {
Config *config.Config
Settings *Settings
SignInFormURI *url.URL
SignedOutURI *url.URL
AuthorizationEndpointURI *url.URL
EndSessionEndpointURI *url.URL
TLSClientConfig *tls.Config
IssuerIdentifierURI *url.URL
IdentifierClientDisabled bool
IdentifierClientPath string
IdentifierRegistrationConf string
IdentifierAuthoritiesConf string
IdentifierScopesConf string
IdentifierDefaultBannerLogo []byte
IdentifierDefaultSignInPageText *string
IdentifierDefaultUsernameHintText *string
IdentifierUILocales []string
EncryptionSecret []byte
SigningMethod jwt.SigningMethod
SigningKeyID string
Signers map[string]crypto.Signer
Validators map[string]crypto.PublicKey
Certificates map[string][]*x509.Certificate
AccessTokenDurationSeconds uint64
IDTokenDurationSeconds uint64
RefreshTokenDurationSeconds uint64
DyamicClientSecretDurationSeconds uint64
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package bootstrap
import (
"context"
"fmt"
"time"
"github.com/libregraph/lico/identity"
identityAuthorities "github.com/libregraph/lico/identity/authorities"
identityClients "github.com/libregraph/lico/identity/clients"
identityManagers "github.com/libregraph/lico/identity/managers"
"github.com/libregraph/lico/managers"
codeManagers "github.com/libregraph/lico/oidc/code/managers"
)
type IdentityManagerFactory func(Bootstrap) (identity.Manager, error)
var identityManagerRegistry = make(map[string]IdentityManagerFactory)
func RegisterIdentityManager(name string, f IdentityManagerFactory) error {
identityManagerRegistry[name] = f
return nil
}
func getIdentityManagerByName(name string, bs Bootstrap) (identity.Manager, error) {
if f, found := identityManagerRegistry[name]; !found {
return nil, fmt.Errorf("no identity manager with name %s registered", name)
} else {
return f(bs)
}
}
func newManagers(ctx context.Context, bs *bootstrap) (*managers.Managers, error) {
logger := bs.config.Config.Logger
var err error
mgrs := managers.New()
// Encryption manager.
encryption, err := identityManagers.NewEncryptionManager(nil)
if err != nil {
return nil, fmt.Errorf("failed to create encryption manager: %v", err)
}
err = encryption.SetKey(bs.config.EncryptionSecret)
if err != nil {
return nil, fmt.Errorf("invalid --encryption-secret parameter value for encryption: %v", err)
}
mgrs.Set("encryption", encryption)
logger.Infof("encryption set up with %d key size", encryption.GetKeySize())
// OIDC code manage.
code := codeManagers.NewMemoryMapManager(ctx)
mgrs.Set("code", code)
// Identifier client registry manager.
clients, err := identityClients.NewRegistry(ctx, bs.config.IssuerIdentifierURI, bs.config.IdentifierRegistrationConf, bs.config.Config.AllowDynamicClientRegistration, time.Duration(bs.config.DyamicClientSecretDurationSeconds)*time.Second, logger)
if err != nil {
return nil, fmt.Errorf("failed to create client registry: %v", err)
}
mgrs.Set("clients", clients)
// Identifier authorities registry manager.
authorities, err := identityAuthorities.NewRegistry(ctx, bs.MakeURI(APITypeSignin, ""), bs.config.IdentifierAuthoritiesConf, logger)
if err != nil {
return nil, fmt.Errorf("failed to create authorities registry: %v", err)
}
mgrs.Set("authorities", authorities)
return mgrs, nil
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package bootstrap
// Settings is a typed application config which represents the user accessible
// boostrap settings params.
type Settings struct {
Iss string
IdentityManager string
URIBasePath string
SignInURI string
SignedOutURI string
AuthorizationEndpointURI string
EndsessionEndpointURI string
Insecure bool
TrustedProxy []string
AllowScope []string
AllowClientGuests bool
AllowDynamicClientRegistration bool
EncryptionSecretFile string
Listen string
IdentifierClientDisabled bool
IdentifierClientPath string
IdentifierRegistrationConf string
IdentifierScopesConf string
IdentifierDefaultBannerLogo string
IdentifierDefaultSignInPageText string
IdentifierDefaultUsernameHintText string
IdentifierUILocales []string
SigningKid string
SigningMethod string
SigningPrivateKeyFiles []string
ValidationKeysPath string
CookieBackendURI string
CookieNames []string
AccessTokenDurationSeconds uint64
IDTokenDurationSeconds uint64
RefreshTokenDurationSeconds uint64
DyamicClientSecretDurationSeconds uint64
}

410
vendor/github.com/libregraph/lico/bootstrap/utils.go generated vendored Normal file
View File

@@ -0,0 +1,410 @@
package bootstrap
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/golang-jwt/jwt/v4"
"github.com/sirupsen/logrus"
"gopkg.in/square/go-jose.v2"
"github.com/libregraph/lico/signing"
)
func parseJSONWebKey(jsonBytes []byte) (*jose.JSONWebKey, error) {
k := &jose.JSONWebKey{}
if err := k.UnmarshalJSON(jsonBytes); err != nil {
return nil, err
}
return k, nil
}
// LoadSignerFromFile loads a private-key for signing
//
// Supports JSON (JWK/JWS) and PEM
func LoadSignerFromFile(fn string) (string, crypto.Signer, error) {
readBytes, errRead := ioutil.ReadFile(fn)
if errRead != nil {
return "", nil, fmt.Errorf("failed to parse key file: %v", errRead)
}
ext := filepath.Ext(fn)
switch ext {
case ".json":
k, err := parseJSONWebKey(readBytes)
if err != nil {
return "", nil, fmt.Errorf("failed to parse key file as JWK: %v", err)
}
if !k.Valid() {
return "", nil, fmt.Errorf("json file is not a valid JWK")
}
if k.IsPublic() {
return "", nil, fmt.Errorf("JWK is a public key, private key required to use as signer")
}
signer, ok := k.Key.(crypto.Signer)
if !ok {
return "", nil, fmt.Errorf("JWS key type %T is not a signer", k.Key)
}
return k.KeyID, signer, nil
case ".pem":
fallthrough
default:
// Try PEM if not otherwise detected.
signer, err := parsePEMSigner(readBytes)
return "", signer, err
}
}
func parsePEMSigner(pemBytes []byte) (crypto.Signer, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, fmt.Errorf("no PEM block found")
}
var signer crypto.Signer
for {
pkcs1Key, errParse1 := x509.ParsePKCS1PrivateKey(block.Bytes)
if errParse1 == nil {
signer = pkcs1Key
break
}
pkcs8Key, errParse2 := x509.ParsePKCS8PrivateKey(block.Bytes)
if errParse2 == nil {
signerSigner, ok := pkcs8Key.(crypto.Signer)
if !ok {
return nil, fmt.Errorf("failed to use key as crypto signer")
}
signer = signerSigner
break
}
ecKey, errParse3 := x509.ParseECPrivateKey(block.Bytes)
if errParse3 == nil {
signer = ecKey
break
}
return nil, fmt.Errorf("failed to parse signer key - valid PKCS#1, PKCS#8 ...? %v, %v, %v", errParse1, errParse2, errParse3)
}
return signer, nil
}
// LoadValidatorFromFile loads a public-key used for validation.
//
// Supported formats are JSON-JWK and PEM
func LoadValidatorFromFile(fn string) (string, crypto.PublicKey, error) {
kid, _, key, err := loadValidatorFromFile(fn)
return kid, key, err
}
// LoadCertificatesAndValidatorFromFile loads chain of certificates and a
// public-key used for validation.
//
// Supported formats are JSON-JWK and PEM
func LoadCertificatesAndValidatorFromFile(fn string) (string, []*x509.Certificate, crypto.PublicKey, error) {
return loadValidatorFromFile(fn)
}
func loadValidatorFromFile(fn string) (string, []*x509.Certificate, crypto.PublicKey, error) {
readBytes, errRead := ioutil.ReadFile(fn)
if errRead != nil {
return "", nil, nil, fmt.Errorf("failed to parse key file: %v", errRead)
}
ext := filepath.Ext(fn)
switch ext {
case ".json":
k, err := parseJSONWebKey(readBytes)
if err != nil {
return "", nil, nil, fmt.Errorf("failed to parse key file as JWK: %v", err)
}
if !k.Valid() {
return "", nil, nil, fmt.Errorf("json file is not a valid JWK")
}
if !k.IsPublic() {
public := k.Public()
k = &public
}
return k.KeyID, k.Certificates, k.Key, nil
case ".pem":
fallthrough
default:
// Try PEM if not otherwise detected.
certificates, validator, err := parsePEMValidator(readBytes)
return "", certificates, validator, err
}
}
func parsePEMValidator(pemBytes []byte) ([]*x509.Certificate, crypto.PublicKey, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, nil, fmt.Errorf("no PEM block found")
}
var certificates []*x509.Certificate
var validator crypto.PublicKey
for {
pkixPubKey, errParse0 := x509.ParsePKIXPublicKey(block.Bytes)
if errParse0 == nil {
validator = pkixPubKey
break
}
pkcs1PubKey, errParse1 := x509.ParsePKCS1PublicKey(block.Bytes)
if errParse1 == nil {
validator = pkcs1PubKey
break
}
pkcs1PrivKey, errParse2 := x509.ParsePKCS1PrivateKey(block.Bytes)
if errParse2 == nil {
validator = pkcs1PrivKey.Public()
break
}
pkcs8Key, errParse3 := x509.ParsePKCS8PrivateKey(block.Bytes)
if errParse3 == nil {
signerSigner, ok := pkcs8Key.(crypto.Signer)
if !ok {
return nil, nil, fmt.Errorf("failed to use key as crypto signer")
}
validator = signerSigner.Public()
break
}
ecKey, errParse4 := x509.ParseECPrivateKey(block.Bytes)
if errParse4 == nil {
validator = ecKey.Public()
break
}
certs, errParse5 := x509.ParseCertificates(block.Bytes)
if errParse5 == nil {
validator = certs[0].PublicKey
certificates = append(certificates, certs...)
break
}
return nil, nil, fmt.Errorf("failed to parse validator key - valid PKCS#1, PKCS#8 ...? %v, %v, %v, %v, %v, %v", errParse0, errParse1, errParse2, errParse3, errParse4, errParse5)
}
return certificates, validator, nil
}
func addSignerWithIDFromFile(fn string, kid string, bs *bootstrap) error {
fi, err := os.Lstat(fn)
if err != nil {
return fmt.Errorf("failed load load signer key: %v", err)
}
mode := fi.Mode()
switch {
case mode.IsDir():
return fmt.Errorf("signer key must be a file")
}
// Load file.
signerKid, signer, err := LoadSignerFromFile(fn)
if err != nil {
return err
}
if kid == "" {
kid = signerKid
}
if kid == "" {
// Get ID from file, following symbolic link.
var real string
if mode&os.ModeSymlink != 0 {
real, err = os.Readlink(fn)
if err != nil {
return err
}
_, real = filepath.Split(real)
} else {
real = fi.Name()
}
kid = getKeyIDFromFilename(real)
}
if _, ok := bs.config.Signers[kid]; ok {
bs.config.Config.Logger.WithFields(logrus.Fields{
"path": fn,
"kid": kid,
}).Warnln("skipped as signer with same kid already loaded")
return nil
} else {
bs.config.Config.Logger.WithFields(logrus.Fields{
"path": fn,
"kid": kid,
}).Debugln("loaded signer key")
}
bs.config.Signers[kid] = signer
return nil
}
func validateSigners(bs *bootstrap) error {
haveRSA := false
haveECDSA := false
haveEd25519 := false
for _, signer := range bs.config.Signers {
switch s := signer.(type) {
case *rsa.PrivateKey:
// Ensure the private key is not vulnerable with PKCS-1.5 signatures. See
// https://paragonie.com/blog/2018/04/protecting-rsa-based-protocols-against-adaptive-chosen-ciphertext-attacks#rsa-anti-bb98
// for details.
if s.PublicKey.E < 65537 {
return fmt.Errorf("RSA signing key with public exponent < 65537")
}
haveRSA = true
case *ecdsa.PrivateKey:
haveECDSA = true
case ed25519.PrivateKey:
haveEd25519 = true
default:
return fmt.Errorf("unsupported signer type: %v", s)
}
}
// Validate signing method
switch bs.config.SigningMethod.(type) {
case *jwt.SigningMethodRSA:
if !haveRSA {
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
}
case *jwt.SigningMethodRSAPSS:
if !haveRSA {
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
}
case *jwt.SigningMethodECDSA:
if !haveECDSA {
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
}
case *signing.SigningMethodEdwardsCurve:
if !haveEd25519 {
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
}
default:
return fmt.Errorf("unsupported signing method: %s", bs.config.SigningMethod.Alg())
}
if !haveRSA {
bs.config.Config.Logger.Warnln("no RSA signing private key, some clients might not be compatible")
}
return nil
}
func addValidatorsFromPath(pn string, bs *bootstrap) error {
fi, err := os.Lstat(pn)
if err != nil {
return fmt.Errorf("failed load load validator keys: %v", err)
}
switch mode := fi.Mode(); {
case mode.IsDir():
// OK.
default:
return fmt.Errorf("validator path must be a directory")
}
// Load all files.
files := []string{}
if pemFiles, err := filepath.Glob(filepath.Join(pn, "*.pem")); err != nil {
return fmt.Errorf("validator path err: %v", err)
} else {
files = append(files, pemFiles...)
}
if jsonFiles, err := filepath.Glob(filepath.Join(pn, "*.json")); err != nil {
return fmt.Errorf("validator path err: %v", err)
} else {
files = append(files, jsonFiles...)
}
for _, file := range files {
kid, certificates, validator, err := loadValidatorFromFile(file)
if err != nil {
bs.config.Config.Logger.WithError(err).WithField("path", file).Warnln("failed to load validator key")
continue
}
// Get ID from file, without following symbolic links.
if kid == "" {
_, fn := filepath.Split(file)
kid = getKeyIDFromFilename(fn)
}
if _, ok := bs.config.Validators[kid]; ok {
bs.config.Config.Logger.WithFields(logrus.Fields{
"path": file,
"kid": kid,
}).Warnln("skipped as validator with same kid already loaded")
continue
} else {
bs.config.Config.Logger.WithFields(logrus.Fields{
"path": file,
"kid": kid,
}).Debugln("loaded validator key")
}
bs.config.Validators[kid] = validator
if certificates != nil {
bs.config.Certificates[kid] = certificates
}
}
return nil
}
func WithSchemeAndHost(u, base *url.URL) *url.URL {
if u.Host != "" && u.Scheme != "" {
return u
}
r, _ := url.Parse(u.String())
r.Scheme = base.Scheme
r.Host = base.Host
return r
}
func getKeyIDFromFilename(fn string) string {
ext := filepath.Ext(fn)
return strings.TrimSuffix(fn, ext)
}
func getCommonURLPathPrefix(p1, p2 string) (string, error) {
parts1 := strings.Split(p1, "/")
parts2 := strings.Split(p2, "/")
common := make([]string, 0)
for idx, p := range parts1 {
if idx >= len(parts2) {
break
}
if p != parts2[idx] {
break
}
common = append(common, p)
}
if len(common) == 0 {
return "", errors.New("no common path prefix")
}
return strings.Join(common, "/"), nil
}

169
vendor/github.com/libregraph/lico/claims.go generated vendored Normal file
View File

@@ -0,0 +1,169 @@
/*
* Copyright 2017-2021 Kopano and its licensors
*
* 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.
*
*/
package lico
import (
"errors"
"github.com/golang-jwt/jwt/v4"
"github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/payload"
)
// Access token claims used.
const (
RefClaim = "lg.r"
IdentityClaim = "lg.i"
IdentityProviderClaim = "lg.p"
ScopesClaim = "scp"
)
// Identifier identity sub claims used.
const (
IdentifiedUserClaim = "us"
IdentifiedUserIDClaim = "id"
IdentifiedUsernameClaim = "un"
IdentifiedDisplayNameClaim = "dn"
IdentifiedData = "da"
IdentifiedUserIsGuest = "gu"
)
// Internal claim names used for special things.
const (
InternalExtraIDTokenClaimsClaim = "$lico.id.extra"
InternalExtraAccessTokenClaimsClaim = "$lico.at.extra"
)
// TokenType defines the token type value.
type TokenTypeValue string
// Is compares the associated TokenTypeValue to the provided one.
func (ttv TokenTypeValue) Is(value TokenTypeValue) bool {
return ttv == value
}
// The known token type values.
const (
TokenTypeIDToken TokenTypeValue = "" // Just a placeholder, not actually set in ID Tokens.
TokenTypeAccessToken TokenTypeValue = "1"
TokenTypeRefreshToken TokenTypeValue = "2"
)
// AccessTokenClaims define the claims found in access tokens issued.
type AccessTokenClaims struct {
jwt.StandardClaims
TokenType TokenTypeValue `json:"lg.t"`
AuthorizedClaimsRequest *payload.ClaimsRequest `json:"lg.acr,omitempty"`
AuthorizedScopesList payload.ScopesValue `json:"scp"`
IdentityClaims jwt.MapClaims `json:"lg.i"`
IdentityProvider string `json:"lg.p,omitempty"`
*oidc.SessionClaims
}
// Valid implements the jwt.Claims interface.
func (c AccessTokenClaims) Valid() error {
if err := c.StandardClaims.Valid(); err != nil {
return err
}
if c.IdentityClaims != nil {
if err := c.IdentityClaims.Valid(); err != nil {
return err
}
}
if c.TokenType.Is(TokenTypeAccessToken) {
return nil
}
return errors.New("not an access token")
}
// AuthorizedScopes returns a map with scope keys and true value of all scopes
// set in the accociated access token.
func (c AccessTokenClaims) AuthorizedScopes() map[string]bool {
authorizedScopes := make(map[string]bool)
for _, scope := range c.AuthorizedScopesList {
authorizedScopes[scope] = true
}
return authorizedScopes
}
// RefreshTokenClaims define the claims used by refresh tokens.
type RefreshTokenClaims struct {
jwt.StandardClaims
TokenType TokenTypeValue `json:"lg.t"`
ApprovedScopesList payload.ScopesValue `json:"scp"`
ApprovedClaimsRequest *payload.ClaimsRequest `json:"lg.acr,omitempty"`
Ref string `json:"lg.r"`
IdentityClaims jwt.MapClaims `json:"lg.i"`
IdentityProvider string `json:"lg.p,omitempty"`
}
// Valid implements the jwt.Claims interface.
func (c RefreshTokenClaims) Valid() error {
if err := c.StandardClaims.Valid(); err != nil {
return err
}
if c.IdentityClaims != nil {
if err := c.IdentityClaims.Valid(); err != nil {
return err
}
}
if c.TokenType.Is(TokenTypeRefreshToken) {
return nil
}
return errors.New("not a refresh token")
}
// NumericIDClaims define the claims used with the konnect/id scope.
type NumericIDClaims struct {
// NOTE(longsleep): Always keep these claims compatible with the GitLab API
// https://docs.gitlab.com/ce/api/users.html#for-user.
NumericID int64 `json:"id,omitempty"`
NumericIDUsername string `json:"username,omitempty"`
}
// Valid implements the jwt.Claims interface.
func (c NumericIDClaims) Valid() error {
if c.NumericIDUsername == "" {
return errors.New("username claim not valid")
}
return nil
}
// UniqueUserIDClaims define the claims used with the konnect/uuid scope.
type UniqueUserIDClaims struct {
UniqueUserID string `json:"lg.uuid,omitempty"`
}
// Valid implements the jwt.Claims interface.
func (c UniqueUserIDClaims) Valid() error {
if c.UniqueUserID == "" {
return errors.New("lg.uuid claim not valid")
}
return nil
}

42
vendor/github.com/libregraph/lico/config/config.go generated vendored Normal file
View File

@@ -0,0 +1,42 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package config
import (
"net"
"net/http"
"github.com/sirupsen/logrus"
)
// Config defines a Server's configuration settings.
type Config struct {
ListenAddr string
WithMetrics bool
Logger logrus.FieldLogger
HTTPTransport http.RoundTripper
TrustedProxyIPs []*net.IP
TrustedProxyNets []*net.IPNet
AllowedScopes []string
AllowClientGuests bool
AllowDynamicClientRegistration bool
}

44
vendor/github.com/libregraph/lico/context.go generated vendored Normal file
View File

@@ -0,0 +1,44 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package lico
import (
"context"
"github.com/golang-jwt/jwt/v4"
)
// key is an unexported type for keys defined in this package.
// This prevents collisions with keys defined in other packages.
type key int
// claimsKey is the key for claims in contexts. It is
// unexported; clients use konnect.NewClaimsContext and
// connect.FromClaimsContext instead of using this key directly.
var claimsKey key
// NewClaimsContext returns a new Context that carries value auth.
func NewClaimsContext(ctx context.Context, claims jwt.Claims) context.Context {
return context.WithValue(ctx, claimsKey, claims)
}
// FromClaimsContext returns the AuthRecord value stored in ctx, if any.
func FromClaimsContext(ctx context.Context) (jwt.Claims, bool) {
claims, ok := ctx.Value(claimsKey).(jwt.Claims)
return claims, ok
}

22
vendor/github.com/libregraph/lico/doc.go generated vendored Normal file
View File

@@ -0,0 +1,22 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
// Package lico is a Go implementation of an OpenID Connect server with
// flexibale authorization and authentication backends and consent screen.
//
// See README.md for more info.
package lico // import "github.com/libregraph/lico"

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package encryption
import (
"fmt"
"golang.org/x/crypto/nacl/secretbox"
)
const (
// KeySize is the size of the keys created by GenerateKey()
KeySize = 32
// NonceSize is the size of the nonces created by GenerateNonce()
NonceSize = 24
)
// Encrypt generates a random nonce and encrypts the input using nacl.secretbox
// package. We store the nonce in the first 24 bytes of the encrypted text.
func Encrypt(msg []byte, key *[KeySize]byte) ([]byte, error) {
nonce, err := GenerateNonce()
if err != nil {
return nil, err
}
return encryptWithNonce(msg, nonce, key)
}
func encryptWithNonce(msg []byte, nonce *[NonceSize]byte, key *[KeySize]byte) ([]byte, error) {
encrypted := secretbox.Seal(nonce[:], msg, nonce, key)
return encrypted, nil
}
// Decrypt extracts the nonce from the encrypted text, and attempts to decrypt
// with nacl.box.
func Decrypt(msg []byte, key *[KeySize]byte) ([]byte, error) {
if len(msg) < (NonceSize + secretbox.Overhead) {
return nil, fmt.Errorf("wrong length of ciphertext")
}
var nonce [NonceSize]byte
copy(nonce[:], msg[:NonceSize])
decrypted, ok := secretbox.Open(nil, msg[NonceSize:], &nonce, key)
if !ok {
return nil, fmt.Errorf("decryption failed")
}
return decrypted, nil
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package encryption
import (
"crypto/rand"
"io"
)
// GenerateKey generates a new random secret key.
func GenerateKey() (*[KeySize]byte, error) {
key := new([KeySize]byte)
_, err := io.ReadFull(rand.Reader, key[:])
if err != nil {
return nil, err
}
return key, nil
}
// GenerateNonce creates a new random nonce.
func GenerateNonce() (*[NonceSize]byte, error) {
nonce := new([NonceSize]byte)
_, err := io.ReadFull(rand.Reader, nonce[:])
if err != nil {
return nil, err
}
return nonce, nil
}

View File

@@ -0,0 +1,96 @@
---
# OpenID Connect client registry.
clients:
# - id: playground.js
# name: OIDC Playground
# application_type: web
# redirect_uris:
# - https://my-host:8509/
# origins:
# - https://my-host:8509
# - id: playground-trusted.js
# name: Trusted OIDC Playground
# trusted: yes
# implicit_scopes:
# - Implicitly.Added
# application_type: web
# redirect_uris:
# - https://my-host:8509/
# origins:
# - https://my-host:8509
# - id: playground-trusted.js
# name: Trusted Insecure OIDC Playground
# trusted: yes
# application_type: web
# insecure: yes
# - id: client-with-keys
# secret: super
# application_type: native
# redirect_uris:
# - http://localhost
# trusted_scopes:
# - LibreGraph.GuestOK
# - LibgreGraph.NumericID
# jwks:
# keys:
# - kty: EC
# use: sig
# kid: client-with-keys-key-1
# crv: P-256
# x: RTZpWoRbjwX1YavmSHVBj6Cy3Yzdkkp6QLvTGB22D0c
# y: jeavjwcX0xlDSchFcBMzXSU7wGs2VPpNxWCwmxFvmF0
# request_object_signing_alg: ES256
# - id: first
# secret: lala
# application_type: native
# redirect_uris:
# - my://app
# - id: second
# secret: lulu
# application_type: native
# redirect_uris:
# - http://localhost
# External authority registry.
authorities:
# - id: my-univention-oidc
# name: Univention
# client_id: libregraph-lico
# authority_type: oidc
# jwks:
# keys:
# - kty: EC
# use: sig
# kid: example-key-1
# crv: P-256
# x: RTZpWoRbjwX1YavmSHVBj6Cy3Yzdkkp6QLvTGB22D0c
# y: jeavjwcX0xlDSchFcBMzXSU7wGs2VPpNxWCwmxFvmF0
# default: yes
# authorization_endpoint: https://my-univention/signin/v1/identifier/_/authorize
# response_type: id_token
# scopes:
# - openid
# - profile
# identity_claim_name: preferred_username
# identity_aliases:
# external-user-a: local-user-a
# external-user-b: local-user-b
# identity_alias_required: true
# - id: my-univention-saml2
# name: Univention
# entity_id: libregraph-lico
# authority_type: saml2
# default: yes
# trusted: yes
# discover: yes
# metadata_endpoint: https://my-univention/simplesamlphp/saml2/idp/metadata.php
# identity_claim_name: uid
# identity_alias_required: false
# end_session_enabled: true

9
vendor/github.com/libregraph/lico/identifier/.env generated vendored Normal file
View File

@@ -0,0 +1,9 @@
PORT=3001
HOST=127.0.0.1
BROWSER=none
ESLINT_NO_DEV_ERRORS=true
REACT_APP_KOPANO_BUILD=0.0.0-dev-env
INLINE_RUNTIME_CHUNK=false
FAST_REFRESH=false
WDS_SOCKET_HOST=0.0.0.0
WDS_SOCKET_PORT=0

View File

@@ -0,0 +1,38 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
/.yarninstall
# testing
/coverage
.eslintcache
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# i18n
src/locales/*.json
src/locales/**/*.json
# yarn
.pnp.*
!.yarnrc.yml
!.yarn/
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View File

@@ -0,0 +1,7 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.2.2.cjs

65
vendor/github.com/libregraph/lico/identifier/Makefile generated vendored Normal file
View File

@@ -0,0 +1,65 @@
# Tools
YARN ?= yarn
# Variables
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)
# Build
.PHONY: all
all: build
.PHONY: build
build: vendor | src i18n ; $(info building identifier Webapp ...) @
@rm -rf build
REACT_APP_KOPANO_BUILD="${VERSION}" CI=false $(YARN) run build
.PHONY: src
src:
@$(MAKE) -C src
.PHONY: i18n
i18n: vendor
@$(MAKE) -C i18n
.PHONY: lint
lint: vendor ; $(info running eslint ...) @
@$(YARN) lint . --cache && echo "eslint: no lint errors"
.PHONY: lint-checkstyle
lint-checkstyle: vendor ; $(info running eslint checkstyle ...) @
@mkdir -p ../test
$(YARN) lint -f checkstyle -o ../test/tests.eslint.xml . || true
# Yarn
.PHONY: vendor
vendor: .yarninstall
.yarninstall: package.json ; $(info getting depdencies with yarn ...) @
@$(YARN) install --immutable
@touch $@
# Stuff
.PHONY: licenses
licenses:
echo "## LibreGraph Connect identifier web app\n"
@$(YARN) run licenses
.PHONY: clean ; $(info cleaning identifier Webapp ...) @
clean:
$(YARN) cache clean
@rm -rf build
@rm -rf node_modules
@rm -f .yarninstall
@$(MAKE) -C src clean
.PHONY: version
version:
@echo $(VERSION)

View File

@@ -0,0 +1,3 @@
# LibreGraph Connect Identifier
Web app for browser sign-in, sign-out and account management.

File diff suppressed because it is too large Load Diff

145
vendor/github.com/libregraph/lico/identifier/api.go generated vendored Normal file
View File

@@ -0,0 +1,145 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package identifier
import (
"bytes"
"fmt"
"net/http"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
"github.com/libregraph/lico/identifier/meta"
"github.com/libregraph/lico/identifier/meta/scopes"
)
func (i *Identifier) writeWebappIndexHTML(rw http.ResponseWriter, req *http.Request) {
nonce := rndm.GenerateRandomString(32)
// FIXME(longsleep): Set a secure CSP. Right now we need `data:` for images
// since it is used. Since `data:` URLs possibly could allow xss, a better
// way should be found for our early loading inline SVG stuff.
rw.Header().Set("Content-Security-Policy", fmt.Sprintf("default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'nonce-%s'; base-uri 'none'; frame-ancestors 'none';", nonce))
// Write index with random nonce to response.
index := bytes.Replace(i.webappIndexHTML, []byte("__CSP_NONCE__"), []byte(nonce), 1)
rw.Write(index)
}
func (i Identifier) writeHelloResponse(rw http.ResponseWriter, req *http.Request, r *HelloRequest, identifiedUser *IdentifiedUser) (*HelloResponse, error) {
var err error
response := &HelloResponse{
State: r.State,
Branding: &meta.Branding{
BannerLogo: i.defaultBannerLogo,
UsernameHintText: i.Config.DefaultUsernameHintText,
SignInPageText: i.Config.DefaultSignInPageText,
Locales: i.Config.UILocales,
},
}
handleHelloLoop:
for {
// Check prompt value.
switch {
case r.Prompts[oidc.PromptNone] == true:
// Never show sign-in, directly return error.
return nil, fmt.Errorf("prompt none requested")
case r.Prompts[oidc.PromptLogin] == true:
// Ignore all potential sources, when prompt login was requested.
if identifiedUser != nil {
response.Username = identifiedUser.Username()
response.DisplayName = identifiedUser.Name()
if response.Username != "" {
response.Success = true
}
}
break handleHelloLoop
default:
// Let all other prompt values pass.
}
if identifiedUser == nil {
// Check if logged in via cookie.
identifiedUser, err = i.GetUserFromLogonCookie(req.Context(), req, r.MaxAge, true)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode logon cookie in hello")
}
}
if identifiedUser != nil {
response.Username = identifiedUser.Username()
response.DisplayName = identifiedUser.Name()
if response.Username != "" {
response.Success = true
break
}
}
break
}
if !response.Success {
return response, nil
}
switch r.Flow {
case FlowOAuth:
fallthrough
case FlowConsent:
fallthrough
case FlowOIDC:
// TODO(longsleep): Add something to validate the parameters.
clientDetails, err := i.clients.Lookup(req.Context(), r.ClientID, "", r.RedirectURI, "", true)
if err != nil {
return nil, err
}
promptConsent := false
// Check prompt value.
switch {
case r.Prompts[oidc.PromptConsent] == true:
promptConsent = true
default:
// Let all other prompt values pass.
}
// If not trusted, always force consent.
if !clientDetails.Trusted {
promptConsent = true
}
if promptConsent {
// TODO(longsleep): Filter scopes to scopes we know about and all.
response.Next = FlowConsent
response.Scopes = r.Scopes
response.ClientDetails = clientDetails
response.Meta = &meta.Meta{
Scopes: scopes.NewScopesFromIDs(r.Scopes, i.meta.Scopes),
}
}
// Add authorize endpoint URI as continue URI.
response.ContinueURI = i.authorizationEndpointURI.String()
response.Flow = r.Flow
}
return response, nil
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package backends
import (
"context"
"github.com/libregraph/lico/identifier/meta/scopes"
"github.com/libregraph/lico/identity"
)
// A Backend is an identifier Backend providing functionality to logon and to
// fetch user meta data.
type Backend interface {
RunWithContext(context.Context) error
Logon(ctx context.Context, audience string, username string, password string) (success bool, userID *string, sessionRef *string, user UserFromBackend, err error)
GetUser(ctx context.Context, userID string, sessionRef *string, requestedScopes map[string]bool) (user UserFromBackend, err error)
ResolveUserByUsername(ctx context.Context, username string) (user UserFromBackend, err error)
RefreshSession(ctx context.Context, userID string, sessionRef *string, claims map[string]interface{}) error
DestroySession(ctx context.Context, sessionRef *string) error
UserClaims(userID string, authorizedScopes map[string]bool) map[string]interface{}
ScopesSupported() []string
ScopesMeta() *scopes.Scopes
Name() string
}
// UserFromBackend are users as provided by backends which can have additional
// claims together with a user name.
type UserFromBackend interface {
identity.UserWithUsername
BackendClaims() map[string]interface{}
BackendScopes() []string
RequiredScopes() []string
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package ldap
// Define some known LDAP attribute descriptors.
const (
AttributeDN = "dn"
AttributeLogin = "uid"
AttributeEmail = "mail"
AttributeName = "cn"
AttributeFamilyName = "sn"
AttributeGivenName = "givenName"
AttributeUUID = "uuid"
)
// Additional mappable virtual attributes.
const (
AttributeNumericUID = "konnectNumericID"
)
// Define our known LDAP attribute value types.
const (
AttributeValueTypeText = "text"
AttributeValueTypeBinary = "binary"
AttributeValueTypeUUID = "uuid"
)

View File

@@ -0,0 +1,670 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package ldap
import (
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"time"
"github.com/go-ldap/ldap/v3"
uuid "github.com/gofrs/uuid"
"github.com/libregraph/oidc-go"
"github.com/sirupsen/logrus"
"golang.org/x/time/rate"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/config"
"github.com/libregraph/lico/identifier/backends"
"github.com/libregraph/lico/identifier/meta/scopes"
)
const ldapIdentifierBackendName = "identifier-ldap"
var ldapSupportedScopes = []string{
oidc.ScopeProfile,
oidc.ScopeEmail,
konnect.ScopeUniqueUserID,
konnect.ScopeRawSubject,
}
// LDAPIdentifierBackend is a backend for the Identifier which connects LDAP.
type LDAPIdentifierBackend struct {
addr string
isTLS bool
bindDN string
bindPassword string
baseDN string
scope int
searchFilter string
getFilter string
entryIDMapping []string
attributeMapping ldapAttributeMapping
supportedScopes []string
logger logrus.FieldLogger
dialer *net.Dialer
tlsConfig *tls.Config
timeout int
limiter *rate.Limiter
}
type ldapAttributeMapping map[string]string
var ldapDefaultAttributeMapping = ldapAttributeMapping{
AttributeLogin: AttributeLogin,
AttributeEmail: AttributeEmail,
AttributeName: AttributeName,
AttributeFamilyName: AttributeFamilyName,
AttributeGivenName: AttributeGivenName,
AttributeUUID: AttributeUUID,
fmt.Sprintf("%s_type", AttributeUUID): AttributeValueTypeText,
}
func (m ldapAttributeMapping) attributes() []string {
attributes := make([]string, len(m)+1)
attributes[0] = AttributeDN
idx := 1
for _, attribute := range m {
attributes[idx] = attribute
idx++
}
return attributes
}
type ldapUser struct {
entryID string
id int64
data ldapAttributeMapping
}
func newLdapUser(entryID string, mapping ldapAttributeMapping, entry *ldap.Entry) (*ldapUser, error) {
// Go through all returned attributes, add them to the local data set if
// we know them in the mapping.
var id int64
data := make(ldapAttributeMapping)
for _, attribute := range entry.Attributes {
if len(attribute.Values) == 0 {
continue
}
for n, mapped := range mapping {
// LDAP attribute descriptors / short names are case insensitive. See
// https://tools.ietf.org/html/rfc4512#page-4.
if strings.ToLower(attribute.Name) == strings.ToLower(mapped) {
// Check if we need conversion.
switch mapping[fmt.Sprintf("%s_type", n)] {
case AttributeValueTypeBinary:
// Binary gets encoded witih Base64.
data[n] = base64.StdEncoding.EncodeToString(attribute.ByteValues[0])
case AttributeValueTypeUUID:
// Try to decode as UUID https://tools.ietf.org/html/rfc4122 and
// serialize to string.
if value, err := uuid.FromBytes(attribute.ByteValues[0]); err == nil {
data[n] = value.String()
}
default:
data[n] = attribute.Values[0]
}
if n == AttributeNumericUID {
numericID, err := strconv.ParseInt(data[n], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid numeric ID %v in record", err)
}
id = numericID
}
}
}
}
return &ldapUser{
entryID: entryID,
id: id,
data: data,
}, nil
}
func (u *ldapUser) getAttributeValue(n string) string {
if n == "" {
return ""
}
return u.data[n]
}
func (u *ldapUser) Subject() string {
return u.entryID
}
func (u *ldapUser) Email() string {
return u.getAttributeValue(AttributeEmail)
}
func (u *ldapUser) EmailVerified() bool {
return false
}
func (u *ldapUser) Name() string {
return u.getAttributeValue(AttributeName)
}
func (u *ldapUser) FamilyName() string {
return u.getAttributeValue(AttributeFamilyName)
}
func (u *ldapUser) GivenName() string {
return u.getAttributeValue(AttributeGivenName)
}
func (u *ldapUser) Username() string {
return u.getAttributeValue(AttributeLogin)
}
func (u *ldapUser) ID() int64 {
return u.id
}
func (u *ldapUser) UniqueID() string {
return u.getAttributeValue(AttributeUUID)
}
func (u *ldapUser) BackendClaims() map[string]interface{} {
claims := make(map[string]interface{})
claims[konnect.IdentifiedUserIDClaim] = u.entryID
return claims
}
func (u *ldapUser) BackendScopes() []string {
return nil
}
func (u *ldapUser) RequiredScopes() []string {
return nil
}
// NewLDAPIdentifierBackend creates a new LDAPIdentifierBackend with the provided
// parameters.
func NewLDAPIdentifierBackend(
c *config.Config,
tlsConfig *tls.Config,
uriString,
bindDN,
bindPassword,
baseDN,
scopeString,
filter string,
subAttributes []string,
mappedAttributes map[string]string,
) (*LDAPIdentifierBackend, error) {
var err error
var scope int
var uri *url.URL
for {
if uriString == "" {
err = fmt.Errorf("server must not be empty")
break
}
uri, err = url.Parse(uriString)
if err != nil {
break
}
if bindDN == "" && bindPassword != "" {
err = fmt.Errorf("bind DN must not be empty when bind password is given")
break
}
if baseDN == "" {
err = fmt.Errorf("base DN must not be empty")
break
}
switch scopeString {
case "sub":
scope = ldap.ScopeWholeSubtree
case "one":
scope = ldap.ScopeSingleLevel
case "base":
scope = ldap.ScopeBaseObject
case "":
scope = ldap.ScopeWholeSubtree
default:
err = fmt.Errorf("unknown scope value: %v, must be one of sub, one or base", scopeString)
}
if err != nil {
break
}
break
}
if err != nil {
return nil, fmt.Errorf("ldap identifier backend %v", err)
}
attributeMapping := ldapAttributeMapping{}
for k, v := range ldapDefaultAttributeMapping {
if mapped, ok := mappedAttributes[k]; ok && mapped != "" {
v = mapped
}
attributeMapping[k] = v
c.Logger.WithField("attribute", fmt.Sprintf("%v:%v", k, v)).Debugln("ldap identifier backend set attribute")
}
// Build supported scopes based on default scopes and scope mapping.
supportedScopes := make([]string, len(ldapSupportedScopes))
copy(supportedScopes, ldapSupportedScopes)
if numericUIDAttribute := mappedAttributes[AttributeNumericUID]; numericUIDAttribute != "" {
supportedScopes = append(supportedScopes, konnect.ScopeNumericID)
attributeMapping[AttributeNumericUID] = numericUIDAttribute
c.Logger.WithField("attribute", fmt.Sprintf("%v:%v", AttributeNumericUID, numericUIDAttribute)).Debugln("ldap identifier backend use attribute")
}
if filter == "" {
filter = "(objectClass=inetOrgPerson)"
}
c.Logger.WithField("filter", filter).Debugln("ldap identifier backend set filter")
loginAttribute := attributeMapping[AttributeLogin]
addr := uri.Host
isTLS := false
switch uri.Scheme {
case "":
uri.Scheme = "ldap"
fallthrough
case "ldap":
if uri.Port() == "" {
addr += ":389"
}
case "ldaps":
if uri.Port() == "" {
addr += ":636"
}
// To be able to verify the servers TLS certificate we need to set the
// server's hostname. (Normally tls.DialWithDialer() would take care of
// that, but we're not using that in LDAPIdentifierBackend.connect())
if !tlsConfig.InsecureSkipVerify && tlsConfig.ServerName == "" {
tlsConfig.ServerName = uri.Hostname()
}
isTLS = true
default:
err = fmt.Errorf("invalid URI scheme: %v", uri.Scheme)
}
if err != nil {
return nil, fmt.Errorf("ldap identifier backend %v", err)
}
var entryIDMapping []string
if len(subAttributes) > 0 {
entryIDMapping = subAttributes
c.Logger.WithField("mapping", entryIDMapping).Debugln("ldap identifier sub is mapped")
}
b := &LDAPIdentifierBackend{
addr: addr,
isTLS: isTLS,
bindDN: bindDN,
bindPassword: bindPassword,
baseDN: baseDN,
scope: scope,
searchFilter: fmt.Sprintf("(&(%s)(%s=%%s))", filter, loginAttribute),
getFilter: filter,
entryIDMapping: entryIDMapping,
attributeMapping: attributeMapping,
supportedScopes: supportedScopes,
logger: c.Logger,
dialer: &net.Dialer{
Timeout: ldap.DefaultTimeout,
DualStack: true,
},
tlsConfig: tlsConfig,
timeout: 60, //XXX(longsleep): make timeout configuration.
limiter: rate.NewLimiter(100, 200), //XXX(longsleep): make rate limits configuration.
}
b.logger.WithField("ldap", fmt.Sprintf("%s://%s ", uri.Scheme, addr)).Infoln("ldap server identifier backend set up")
return b, nil
}
// RunWithContext implements the Backend interface.
func (b *LDAPIdentifierBackend) RunWithContext(ctx context.Context) error {
return nil
}
// Logon implements the Backend interface, enabling Logon with user name and
// password as provided. Requests are bound to the provided context.
func (b *LDAPIdentifierBackend) Logon(ctx context.Context, audience, username, password string) (bool, *string, *string, backends.UserFromBackend, error) {
loginAttributeName := b.attributeMapping[AttributeLogin]
if loginAttributeName == "" {
return false, nil, nil, nil, fmt.Errorf("ldap identifier backend logon impossible as no login attribute is set")
}
l, err := b.connect(ctx)
if err != nil {
return false, nil, nil, nil, fmt.Errorf("ldap identifier backend logon connect error: %v", err)
}
defer l.Close()
// Search for the given username.
entry, err := b.searchUsername(l, username, b.attributeMapping.attributes())
switch {
case ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject):
return false, nil, nil, nil, nil
}
if err != nil {
return false, nil, nil, nil, fmt.Errorf("ldap identifier backend logon search error: %v", err)
}
if !strings.EqualFold(entry.GetAttributeValue(loginAttributeName), username) {
return false, nil, nil, nil, fmt.Errorf("ldap identifier backend logon search returned wrong user")
}
// Bind as the user to verify the password.
err = l.Bind(entry.DN, password)
switch {
case ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials):
return false, nil, nil, nil, nil
}
if err != nil {
return false, nil, nil, nil, fmt.Errorf("ldap identifier backend logon error: %v", err)
}
entryID := b.entryIDFromEntry(b.attributeMapping, entry)
if entryID == "" {
return false, nil, nil, nil, fmt.Errorf("ldap identifier backend logon entry without entry ID: %v", entry.DN)
}
user, err := newLdapUser(entryID, b.attributeMapping, entry)
if err != nil {
return false, nil, nil, nil, fmt.Errorf("ldap identifier backend logon entry data error: %v", err)
}
// Use the users subject as user id.
userID := user.Subject()
b.logger.WithFields(logrus.Fields{
"username": user.Username(),
"id": userID,
}).Debugln("ldap identifier backend logon")
return true, &userID, nil, user, nil
}
// ResolveUserByUsername implements the Beckend interface, providing lookup for
// user by providing the username. Requests are bound to the provided context.
func (b *LDAPIdentifierBackend) ResolveUserByUsername(ctx context.Context, username string) (backends.UserFromBackend, error) {
loginAttributeName := b.attributeMapping[AttributeLogin]
if loginAttributeName == "" {
return nil, fmt.Errorf("ldap identifier backend resolve impossible as no login attribute is set")
}
l, err := b.connect(ctx)
if err != nil {
return nil, fmt.Errorf("ldap identifier backend resolve connect error: %v", err)
}
defer l.Close()
// Search for the given username.
entry, err := b.searchUsername(l, username, b.attributeMapping.attributes())
switch {
case ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject):
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("ldap identifier backend resolve search error: %v", err)
}
if !strings.EqualFold(entry.GetAttributeValue(loginAttributeName), username) {
return nil, fmt.Errorf("ldap identifier backend resolve search returned wrong user")
}
newEntryID := b.entryIDFromEntry(b.attributeMapping, entry)
user, err := newLdapUser(newEntryID, b.attributeMapping, entry)
if err != nil {
return nil, fmt.Errorf("ldap identifier backend resolve entry data error: %v", err)
}
return user, nil
}
// GetUser implements the Backend interface, providing user meta data retrieval
// for the user specified by the userID. Requests are bound to the provided
// context.
func (b *LDAPIdentifierBackend) GetUser(ctx context.Context, entryID string, sessionRef *string, requestedScopes map[string]bool) (backends.UserFromBackend, error) {
l, err := b.connect(ctx)
if err != nil {
return nil, fmt.Errorf("ldap identifier backend get user connect error: %v", err)
}
defer l.Close()
entry, err := b.getUser(l, entryID, b.attributeMapping.attributes())
if err != nil {
return nil, fmt.Errorf("ldap identifier backend get user error: %v", err)
}
newEntryID := b.entryIDFromEntry(b.attributeMapping, entry)
if !strings.EqualFold(newEntryID, entryID) {
return nil, fmt.Errorf("ldap identifier backend get user returned wrong user")
}
user, err := newLdapUser(newEntryID, b.attributeMapping, entry)
if err != nil {
return nil, fmt.Errorf("ldap identifier backend get user entry data error: %v", err)
}
return user, err
}
// RefreshSession implements the Backend interface.
func (b *LDAPIdentifierBackend) RefreshSession(ctx context.Context, userID string, sessionRef *string, claims map[string]interface{}) error {
return nil
}
// DestroySession implements the Backend interface providing destroy to KC session.
func (b *LDAPIdentifierBackend) DestroySession(ctx context.Context, sessionRef *string) error {
return nil
}
// UserClaims implements the Backend interface, providing user specific claims
// for the user specified by the userID.
func (b *LDAPIdentifierBackend) UserClaims(userID string, authorizedScopes map[string]bool) map[string]interface{} {
return nil
}
// ScopesSupported implements the Backend interface, providing supported scopes
// when running this backend.
func (b *LDAPIdentifierBackend) ScopesSupported() []string {
return b.supportedScopes
}
// ScopesMeta implements the Backend interface, providing meta data for
// supported scopes.
func (b *LDAPIdentifierBackend) ScopesMeta() *scopes.Scopes {
return nil
}
// Name implements the Backend interface.
func (b *LDAPIdentifierBackend) Name() string {
return ldapIdentifierBackendName
}
func (b *LDAPIdentifierBackend) connect(parentCtx context.Context) (*ldap.Conn, error) {
// A timeout for waiting for a limiter slot. The timeout also includes the
// time to connect to the LDAP server which as a consequence means that both
// getting a free slot and establishing the connection are one timeout.
ctx, cancel := context.WithTimeout(parentCtx, time.Duration(b.timeout)*time.Second)
defer cancel()
err := b.limiter.Wait(ctx)
if err != nil {
return nil, err
}
c, err := b.dialer.DialContext(ctx, "tcp", b.addr)
if err != nil {
return nil, ldap.NewError(ldap.ErrorNetwork, err)
}
var l *ldap.Conn
if b.isTLS {
sc := tls.Client(c, b.tlsConfig)
err = sc.Handshake()
if err != nil {
c.Close()
return nil, ldap.NewError(ldap.ErrorNetwork, err)
}
l = ldap.NewConn(sc, true)
} else {
l = ldap.NewConn(c, false)
}
l.Start()
// Bind with general user (which is preferably read only).
if b.bindDN != "" {
err = l.Bind(b.bindDN, b.bindPassword)
if err != nil {
return nil, err
}
}
return l, nil
}
func (b *LDAPIdentifierBackend) searchUsername(l *ldap.Conn, username string, attributes []string) (*ldap.Entry, error) {
base, filter := b.baseAndSearchFilterFromUsername(username)
// Search for the given username.
searchRequest := ldap.NewSearchRequest(
base,
b.scope, ldap.NeverDerefAliases, 1, b.timeout, false,
filter,
attributes,
nil,
)
sr, err := l.Search(searchRequest)
if err != nil {
return nil, err
}
switch len(sr.Entries) {
case 0:
// Nothing found.
return nil, ldap.NewError(ldap.LDAPResultNoSuchObject, err)
case 1:
// Exactly one found, success.
return sr.Entries[0], nil
default:
// Invalid when multiple matched.
return nil, fmt.Errorf("user too many entries returned")
}
}
func (b *LDAPIdentifierBackend) getUser(l *ldap.Conn, entryID string, attributes []string) (*ldap.Entry, error) {
base, filter := b.baseAndGetFilterFromEntryID(entryID)
if base == "" || filter == "" || entryID == "" {
return nil, fmt.Errorf("ldap identifier backend get user invalid user ID: %v", entryID)
}
scope := b.scope
if base == entryID {
// Ensure that scope is limited, when directly requesting an entry.
scope = ldap.ScopeBaseObject
}
// search for the given DN.
searchRequest := ldap.NewSearchRequest(
base,
scope, ldap.NeverDerefAliases, 1, b.timeout, false,
filter,
attributes,
nil,
)
sr, err := l.Search(searchRequest)
if err != nil {
return nil, err
}
if len(sr.Entries) != 1 {
return nil, fmt.Errorf("user does not exist or too many entries returned")
}
return sr.Entries[0], nil
}
func (b *LDAPIdentifierBackend) entryIDFromEntry(mapping ldapAttributeMapping, entry *ldap.Entry) string {
if b.entryIDMapping != nil {
// Encode as URL query.
values := url.Values{}
for _, k := range b.entryIDMapping {
v := entry.GetAttributeValues(k)
if len(v) > 0 {
values[k] = v
}
}
// URL encode values to string.
return values.Encode()
}
// Use DN by default is no mapping is set.
return entry.DN
}
func (b *LDAPIdentifierBackend) baseAndGetFilterFromEntryID(entryID string) (string, string) {
if b.entryIDMapping != nil {
// Parse entryID as URL encoded query values, and build & filter to search for them all.
if values, err := url.ParseQuery(entryID); err == nil {
filter := ""
for k, values := range values {
for _, value := range values {
filter = fmt.Sprintf("%s(%s=%s)", filter, k, value)
}
}
if filter != "" {
return b.baseDN, fmt.Sprintf("(&%s%s)", b.getFilter, filter)
}
}
// Failed to parse entry ID.
return "", ""
}
// Map DN to entryID.
_, err := ldap.ParseDN(entryID)
if err != nil {
return "", ""
}
return entryID, b.getFilter
}
func (b *LDAPIdentifierBackend) baseAndSearchFilterFromUsername(username string) (string, string) {
// Build search filter with username.
return b.baseDN, fmt.Sprintf(b.searchFilter, username)
}

View File

@@ -0,0 +1,560 @@
/*
* Copyright 2021 Kopano and its licensors
*
* 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.
*
*/
package libregraph
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/cevaris/ordered_map"
"github.com/libregraph/oidc-go"
"github.com/sirupsen/logrus"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/config"
"github.com/libregraph/lico/identifier"
"github.com/libregraph/lico/identifier/backends"
"github.com/libregraph/lico/identifier/meta/scopes"
identityClients "github.com/libregraph/lico/identity/clients"
"github.com/libregraph/lico/utils"
)
const libreGraphIdentifierBackendName = "identifier-libregraph"
const (
OpenTypeExtensionType = "#microsoft.graph.openTypeExtension"
IdentityClaimsExtensionName = "libregraph.identityClaims"
IDTokenClaimsExtensionName = "libregraph.idTokenClaims"
AccessTokenClaimsExtensionName = "libregraph.accessTokenClaims"
RequestedScopesExtensionName = "libregraph.requestedScopes"
SessionExtensionName = "libregraph.session"
)
const (
apiPathMe = "/api/v1/me"
apiPathUsers = "/api/v1/users"
)
var libreGraphSpportedScopes = []string{
oidc.ScopeProfile,
oidc.ScopeEmail,
konnect.ScopeUniqueUserID,
konnect.ScopeRawSubject,
}
type LibreGraphIdentifierBackend struct {
supportedScopes []string
logger logrus.FieldLogger
tlsConfig *tls.Config
client *http.Client
baseURLMap *ordered_map.OrderedMap
useMultipleBackends bool
clients *identityClients.Registry
}
type libreGraphUser struct {
AccountEnabled bool `json:"accountEnabled"`
DisplayName string `json:"displayName"`
RawGivenName string `json:"givenName"`
ID string `json:"id"`
Mail string `json:"mail"`
Surname string `json:"surname"`
UserPrincipalName string `json:"userPrincipalName"`
Extensions []map[string]interface{} `json:"extensions"`
identityClaims map[string]interface{}
requestedScopes []string
requiredScopes []string
}
func decodeLibreGraphUser(r io.Reader) (*libreGraphUser, error) {
decoder := json.NewDecoder(r)
u := &libreGraphUser{}
if err := decoder.Decode(u); err != nil {
return nil, err
}
identityClaims := make(map[string]interface{})
identityClaims[konnect.IdentifiedUserIDClaim] = u.ID
var idTokenClaims map[string]interface{}
var accessTokenClaims map[string]interface{}
var requestedScopes []string
for _, extension := range u.Extensions {
if odataType, ok := extension["@odata.type"]; ok && odataType.(string) != OpenTypeExtensionType {
continue
}
if extensionName, ok := extension["extensionName"].(string); ok {
switch extensionName {
case IdentityClaimsExtensionName:
if v, ok := extension["claims"].(map[string]interface{}); ok {
for k, v := range v {
if k == konnect.InternalExtraIDTokenClaimsClaim || k == konnect.InternalExtraAccessTokenClaimsClaim {
// Ignore keys which areused internally by IDTokenClaimsExtensionName
// and AccessTokenClaimsExtensionName.
continue
}
identityClaims[k] = v
}
}
case IDTokenClaimsExtensionName:
if idTokenClaims == nil {
idTokenClaims = make(map[string]interface{})
}
if v, ok := extension["claims"].(map[string]interface{}); ok {
for k, v := range v {
idTokenClaims[k] = v
}
}
case AccessTokenClaimsExtensionName:
if accessTokenClaims == nil {
accessTokenClaims = make(map[string]interface{})
}
if v, ok := extension["claims"].(map[string]interface{}); ok {
for k, v := range v {
accessTokenClaims[k] = v
}
}
case RequestedScopesExtensionName:
if values, ok := extension["scopes"].([]interface{}); ok {
for _, v := range values {
if s, ok := v.(string); ok {
requestedScopes = append(requestedScopes, s)
}
}
}
case SessionExtensionName:
if sid, ok := extension[oidc.SessionIDClaim].(string); ok {
if sid != "" {
if accessTokenClaims == nil {
accessTokenClaims = make(map[string]interface{})
}
accessTokenClaims[oidc.SessionIDClaim] = sid
}
}
}
}
}
if idTokenClaims != nil {
// Inject claims as nested identity claim. The key is picket up by the
// token signer and used to extend ID token root claims.
identityClaims[konnect.InternalExtraIDTokenClaimsClaim] = idTokenClaims
}
if accessTokenClaims != nil {
// Inject claims as nested identity claims. The key is picked up by the
// token signer and userinfo handler to extend ID and access token root
// claims based on the request.
identityClaims[konnect.InternalExtraAccessTokenClaimsClaim] = accessTokenClaims
}
if requestedScopes != nil {
u.requestedScopes = requestedScopes
}
u.identityClaims = identityClaims
return u, nil
}
func (u *libreGraphUser) Subject() string {
return u.ID
}
func (u *libreGraphUser) Email() string {
return u.Mail
}
func (u *libreGraphUser) EmailVerified() bool {
return true
}
func (u *libreGraphUser) Name() string {
return u.DisplayName
}
func (u *libreGraphUser) FamilyName() string {
return u.Surname
}
func (u *libreGraphUser) GivenName() string {
return u.RawGivenName
}
func (u *libreGraphUser) Username() string {
return u.UserPrincipalName
}
func (u *libreGraphUser) UniqueID() string {
// Provide our ID as unique ID.
return u.ID
}
func (u *libreGraphUser) BackendClaims() map[string]interface{} {
return u.identityClaims
}
func (u *libreGraphUser) BackendScopes() []string {
return u.requestedScopes
}
func (u *libreGraphUser) RequiredScopes() []string {
return u.requiredScopes
}
func (u *libreGraphUser) setRequiredScopes(selectedScope string, scopeMap *ordered_map.OrderedMap) []string {
var requiredScopes []string
if selectedScope != "" {
requiredScopes = []string{selectedScope}
}
iter := scopeMap.IterFunc()
for kv, ok := iter(); ok; kv, ok = iter() {
if scope := kv.Key.(string); scope != selectedScope {
requiredScopes = append(requiredScopes, "!"+scope)
}
}
u.requiredScopes = requiredScopes
return requiredScopes
}
func (u *libreGraphUser) sessionID() string {
if accessTokenClaims, ok := u.identityClaims[""].(map[string]interface{}); ok {
if sessionID, withSessionID := accessTokenClaims[oidc.SessionIDClaim].(string); withSessionID {
if sessionID != "" {
return sessionID
}
}
}
return ""
}
func withSelectQuery(r *http.Request) {
if r.Form == nil {
r.Form = make(url.Values)
}
r.Form.Set("$select", "accountEnabled,displayName,givenName,id,mail,surname,userPrincipalName,extensions")
}
func NewLibreGraphIdentifierBackend(
c *config.Config,
tlsConfig *tls.Config,
baseURI string,
baseURIByScope *ordered_map.OrderedMap,
clients *identityClients.Registry,
) (*LibreGraphIdentifierBackend, error) {
if baseURI == "" {
return nil, fmt.Errorf("base uri must not be empty")
}
// Build supported scopes based on default scopes.
supportedScopes := make([]string, len(libreGraphSpportedScopes))
copy(supportedScopes, libreGraphSpportedScopes)
baseURLMap := ordered_map.NewOrderedMapWithArgs([]*ordered_map.KVPair{{
Key: "",
Value: baseURI,
}})
if baseURIByScope != nil {
iter := baseURIByScope.IterFunc()
for kv, ok := iter(); ok; kv, ok = iter() {
if kv.Key == "" {
return nil, fmt.Errorf("scoped base uri with empty scope is not allowed")
}
baseURLMap.Set(kv.Key, kv.Value)
}
}
transport := utils.HTTPTransportWithTLSClientConfig(tlsConfig)
transport.MaxIdleConns = 100
transport.IdleConnTimeout = 30 * time.Second
b := &LibreGraphIdentifierBackend{
supportedScopes: supportedScopes,
logger: c.Logger,
tlsConfig: tlsConfig,
client: &http.Client{
Transport: transport,
Timeout: 60 * time.Second,
},
baseURLMap: baseURLMap,
useMultipleBackends: baseURLMap.Len() > 1,
clients: clients,
}
b.logger.WithField("map", baseURLMap).Infoln("libregraph server identified backend connection set up")
return b, nil
}
// RunWithContext implements the Backend interface.
func (b *LibreGraphIdentifierBackend) RunWithContext(ctx context.Context) error {
return nil
}
// Logon implements the Backend interface, enabling Logon with user name and
// password as provided. Requests are bound to the provided context.
func (b *LibreGraphIdentifierBackend) Logon(ctx context.Context, audience, username, password string) (bool, *string, *string, backends.UserFromBackend, error) {
record, _ := identifier.FromRecordContext(ctx)
var requestedScopes map[string]bool
if record != nil {
requestedScopes = record.HelloRequest.Scopes
}
// Inject implicit scopes set by client registration. This is needed here,
// as the requested scopes might not have the implicit scopes applied yet,
// based on the calling stack chain and since we use the scopes to select
// the backend.
registration, _ := b.clients.Get(ctx, audience)
if registration != nil {
_ = registration.ApplyImplicitScopes(requestedScopes)
}
selectedScope, meURL := b.getMeURL(requestedScopes)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, meURL, nil)
if err != nil {
return false, nil, nil, nil, fmt.Errorf("libregraph identifier backend logon request error: %w", err)
}
req.SetBasicAuth(username, password)
if record != nil {
// Inject HTTP headers.
if record.HelloRequest.Flow != "" {
req.Header.Set("X-Flow", record.HelloRequest.Flow)
}
if record.HelloRequest.RawScope != "" {
req.Header.Set("X-Scope", record.HelloRequest.RawScope)
}
if record.HelloRequest.RawPrompt != "" {
req.Header.Set("X-Prompt", record.HelloRequest.RawPrompt)
}
}
req.Header.Set("User-Agent", utils.DefaultHTTPUserAgent)
// Inject select parameter.
withSelectQuery(req)
response, err := b.client.Do(req)
if err != nil {
return false, nil, nil, nil, fmt.Errorf("libregraph identifier backend logon request failed: %w", err)
}
defer response.Body.Close()
switch response.StatusCode {
case http.StatusOK:
// breaks
case http.StatusNotFound:
return false, nil, nil, nil, nil
case http.StatusUnauthorized:
return false, nil, nil, nil, nil
default:
return false, nil, nil, nil, fmt.Errorf("libregraph identifier backend logon request unexpected response status: %d", response.StatusCode)
}
user, err := decodeLibreGraphUser(response.Body)
if err != nil {
return false, nil, nil, nil, fmt.Errorf("libregraph identifier backend logon json decode error: %w", err)
}
if !user.AccountEnabled {
return false, nil, nil, nil, nil
}
requiredScopes := user.setRequiredScopes(selectedScope, b.baseURLMap)
// Use the users subject as user id.
userID := user.Subject()
sessionID := user.sessionID()
b.logger.WithFields(logrus.Fields{
"username": user.Username(),
"id": userID,
"scope": requiredScopes,
"sessionID": sessionID,
}).Debugln("libregraph identifier backend logon")
// Put the user into the record (if any).
if record != nil {
record.UserFromBackend = user
}
return true, &userID, &sessionID, user, nil
}
// GetUser implements the Backend interface, providing user meta data retrieval
// for the user specified by the userID. Requests are bound to the provided
// context.
func (b *LibreGraphIdentifierBackend) GetUser(ctx context.Context, entryID string, sessionRef *string, requestedScopes map[string]bool) (backends.UserFromBackend, error) {
record, _ := identifier.FromRecordContext(ctx)
if record != nil {
if record.UserFromBackend != nil {
if user, ok := record.UserFromBackend.(*libreGraphUser); ok {
// Fastpath, if logon previously injected the user.
if user.ID == entryID {
return user, nil
}
}
}
if requestedScopes == nil && record.HelloRequest != nil {
requestedScopes = record.HelloRequest.Scopes
}
}
selectedScope, userURL := b.getUserURL(requestedScopes)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, userURL+"/"+entryID, nil)
if err != nil {
return nil, fmt.Errorf("libregraph identifier backend get user request error: %w", err)
}
// Inject HTTP headers.
if requestedScopes != nil {
rawRequestedScopes := make([]string, 0)
for scope, enabled := range requestedScopes {
if enabled {
rawRequestedScopes = append(rawRequestedScopes, scope)
}
}
req.Header.Set("X-Scope", strings.Join(rawRequestedScopes, " "))
}
if sessionRef != nil {
sessionID := *sessionRef
if !strings.HasPrefix(sessionID, libreGraphIdentifierBackendName+":") {
// Only send the session ID if it is not a ref generated by lico.
req.Header.Set("X-SessionID", sessionID)
}
}
req.Header.Set("User-Agent", utils.DefaultHTTPUserAgent)
// Inject select parameter.
withSelectQuery(req)
response, err := b.client.Do(req)
if err != nil {
return nil, fmt.Errorf("libregraph identifier backend get user request failed: %w", err)
}
defer response.Body.Close()
switch response.StatusCode {
case http.StatusOK:
// breaks
case http.StatusNotFound:
return nil, nil
default:
return nil, fmt.Errorf("libregraph identifier backend get user request unexpected response status: %d", response.StatusCode)
}
user, err := decodeLibreGraphUser(response.Body)
if err != nil {
return nil, fmt.Errorf("libregraph identifier backend logon json decode error: %w", err)
}
if !user.AccountEnabled {
return nil, nil
}
user.setRequiredScopes(selectedScope, b.baseURLMap)
return user, nil
}
// ResolveUserByUsername implements the Beckend interface, providing lookup for
// user by providing the username. Requests are bound to the provided context.
func (b *LibreGraphIdentifierBackend) ResolveUserByUsername(ctx context.Context, username string) (backends.UserFromBackend, error) {
// Libregraph backend accept both user name and ID lookups, so this is
// the same as GetUser without a session.
return b.GetUser(ctx, username, nil, nil)
}
// RefreshSession implements the Backend interface.
func (b *LibreGraphIdentifierBackend) RefreshSession(ctx context.Context, userID string, sessionRef *string, claims map[string]interface{}) error {
return nil
}
// DestroySession implements the Backend interface providing destroy to KC session.
func (b *LibreGraphIdentifierBackend) DestroySession(ctx context.Context, sessionRef *string) error {
return nil
}
// UserClaims implements the Backend interface, providing user specific claims
// for the user specified by the userID.
func (b *LibreGraphIdentifierBackend) UserClaims(userID string, authorizedScopes map[string]bool) map[string]interface{} {
return nil
}
// ScopesSupported implements the Backend interface, providing supported scopes
// when running this backend.
func (b *LibreGraphIdentifierBackend) ScopesSupported() []string {
return b.supportedScopes
}
// ScopesMeta implements the Backend interface, providing meta data for
// supported scopes.
func (b *LibreGraphIdentifierBackend) ScopesMeta() *scopes.Scopes {
return nil
}
// Name implements the Backend interface.
func (b *LibreGraphIdentifierBackend) Name() string {
return libreGraphIdentifierBackendName
}
func (b *LibreGraphIdentifierBackend) getBaseURL(requestedScopes map[string]bool) (string, string) {
if b.useMultipleBackends && requestedScopes != nil {
// Loop through configured backends for each requested scope.
for s, v := range requestedScopes {
if !v {
continue
}
if u, ok := b.baseURLMap.Get(s); ok {
return s, u.(string)
}
}
}
// If nothing found, return default.
u, _ := b.baseURLMap.Get("")
return "", u.(string)
}
func (b *LibreGraphIdentifierBackend) getMeURL(requestedScopes map[string]bool) (string, string) {
scope, baseURL := b.getBaseURL(requestedScopes)
return scope, baseURL + apiPathMe
}
func (b *LibreGraphIdentifierBackend) getUserURL(requestedScopes map[string]bool) (string, string) {
scope, baseURL := b.getBaseURL(requestedScopes)
return scope, baseURL + apiPathUsers
}

31
vendor/github.com/libregraph/lico/identifier/claims.go generated vendored Normal file
View File

@@ -0,0 +1,31 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package identifier
// Additional claims as used by the identifier in its own tokens.
const (
SessionIDClaim = "sid"
LogonRefClaim = "lref"
ExternalAuthorityIDClaim = "eaid"
LockedScopesClaim = "lscp"
)
// History claims previously used by the identifier in its own tokens.
const (
ObsoleteUserClaimsClaim = "claims"
)

48
vendor/github.com/libregraph/lico/identifier/config.go generated vendored Normal file
View File

@@ -0,0 +1,48 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package identifier
import (
"net/url"
"github.com/libregraph/lico/config"
"github.com/libregraph/lico/identifier/backends"
)
// Config defines a Server's configuration settings.
type Config struct {
Config *config.Config
BaseURI *url.URL
LogonCookieName string
ScopesConf string
PathPrefix string
StaticFolder string
WebAppDisabled bool
AuthorizationEndpointURI *url.URL
SignedOutEndpointURI *url.URL
DefaultBannerLogo []byte
DefaultSignInPageText *string
DefaultUsernameHintText *string
UILocales []string
Backend backends.Backend
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2021 Kopano and its licensors
*
* 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.
*
*/
package identifier
import (
"context"
"github.com/libregraph/lico/identifier/backends"
)
// Record is the struct which the identifier puts into the context.
type Record struct {
HelloRequest *HelloRequest
UserFromBackend backends.UserFromBackend
}
// key is an unexported type for keys defined in this package.
// This prevents collisions with keys defined in other packages.
type key int
// recordKey is the key for identifier.Record in Contexts. It is
// unexported; clients use identifier.NewContext and identifier.FromContext
// instead of using this key directly.
var recordKey key
// NewRecordContext returns a new Context that carries value HelloRequest.
func NewRecordContext(ctx context.Context, record *Record) context.Context {
return context.WithValue(ctx, recordKey, record)
}
// FromRecordContext returns the Record value stored in ctx, if any.
func FromRecordContext(ctx context.Context) (*Record, bool) {
record, ok := ctx.Value(recordKey).(*Record)
return record, ok
}

202
vendor/github.com/libregraph/lico/identifier/cookie.go generated vendored Normal file
View File

@@ -0,0 +1,202 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package identifier
import (
"encoding/base64"
"net/http"
"golang.org/x/crypto/blake2b"
)
const (
consentCookieNamePrefix = "__Secure-KKTC" // Kopano Konnect Temorary Consent
stateCookieNamePrefix = "__Secure-KKTS" // Kopano Konnect Temporary State
)
func (i *Identifier) setLogonCookie(rw http.ResponseWriter, value string) error {
cookie := http.Cookie{
Name: i.logonCookieName,
Value: value,
Path: i.pathPrefix + "/identifier/_/",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteNoneMode,
}
http.SetCookie(rw, &cookie)
return nil
}
func (i *Identifier) getLogonCookie(req *http.Request) (*http.Cookie, error) {
return req.Cookie(i.logonCookieName)
}
func (i *Identifier) removeLogonCookie(rw http.ResponseWriter) error {
cookie := http.Cookie{
Name: i.logonCookieName,
Path: i.pathPrefix + "/identifier/_/",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteNoneMode,
Expires: farPastExpiryTime,
}
http.SetCookie(rw, &cookie)
return nil
}
func (i *Identifier) setConsentCookie(rw http.ResponseWriter, cr *ConsentRequest, value string) error {
name, err := i.getConsentCookieName(cr)
if err != nil {
return err
}
cookie := http.Cookie{
Name: name,
Value: value,
MaxAge: 60,
Path: i.pathPrefix + "/identifier/_/",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteNoneMode,
}
http.SetCookie(rw, &cookie)
return nil
}
func (i *Identifier) getConsentCookie(req *http.Request, cr *ConsentRequest) (*http.Cookie, error) {
name, err := i.getConsentCookieName(cr)
if err != nil {
return nil, err
}
return req.Cookie(name)
}
func (i *Identifier) removeConsentCookie(rw http.ResponseWriter, req *http.Request, cr *ConsentRequest) error {
name, err := i.getConsentCookieName(cr)
if err != nil {
return nil
}
cookie := http.Cookie{
Name: name,
Path: i.pathPrefix + "/identifier/_/",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteNoneMode,
Expires: farPastExpiryTime,
}
http.SetCookie(rw, &cookie)
return nil
}
func (i *Identifier) getConsentCookieName(cr *ConsentRequest) (string, error) {
// Consent cookie names are based on parameters in the request.
hasher, err := blake2b.New256(nil)
if err != nil {
return "", err
}
hasher.Write([]byte(cr.State))
hasher.Write([]byte("h"))
hasher.Write([]byte(cr.ClientID))
hasher.Write([]byte("e"))
hasher.Write([]byte(cr.RawRedirectURI))
hasher.Write([]byte("l"))
hasher.Write([]byte(cr.Ref))
hasher.Write([]byte("o"))
hasher.Write([]byte(cr.Nonce))
name := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
return consentCookieNamePrefix + "-" + name, nil
}
func (i *Identifier) setStateCookie(rw http.ResponseWriter, scope string, state string, value string) error {
name, err := i.getStateCookieName(state)
if err != nil {
return err
}
cookie := http.Cookie{
Name: name,
Value: value,
MaxAge: 600,
Path: i.pathPrefix + "/identifier/" + scope,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteNoneMode,
}
http.SetCookie(rw, &cookie)
return nil
}
func (i *Identifier) getStateCookie(req *http.Request, state string) (*http.Cookie, error) {
name, err := i.getStateCookieName(state)
if err != nil {
return nil, err
}
return req.Cookie(name)
}
func (i *Identifier) removeStateCookie(rw http.ResponseWriter, req *http.Request, scope string, state string) error {
name, err := i.getStateCookieName(state)
if err != nil {
return nil
}
cookie := http.Cookie{
Name: name,
Path: i.pathPrefix + "/identifier/" + scope,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteNoneMode,
Expires: farPastExpiryTime,
}
http.SetCookie(rw, &cookie)
return nil
}
func (i *Identifier) getStateCookieName(state string) (string, error) {
// State cookie names are based on the state value.
hasher, err := blake2b.New256(nil)
if err != nil {
return "", err
}
hasher.Write([]byte(state))
name := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
return stateCookieNamePrefix + "-" + name, nil
}

27
vendor/github.com/libregraph/lico/identifier/flows.go generated vendored Normal file
View File

@@ -0,0 +1,27 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package identifier
const (
// FlowOIDC is the string value for the oidc flow.
FlowOIDC = "oidc"
// FlowOAuth is the string value for the oauth flow.
FlowOAuth = "oauth"
// FlowConsent is the string value for the consent flow.
FlowConsent = "consent"
)

View File

@@ -0,0 +1,516 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package identifier
import (
"encoding/json"
"encoding/xml"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/libregraph/lico/identity/authorities"
"github.com/libregraph/lico/utils"
)
func (i *Identifier) staticHandler(handler http.Handler, cache bool) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
addCommonResponseHeaders(rw.Header())
if cache {
rw.Header().Set("Cache-Control", "max-age=3153600, public")
} else {
rw.Header().Set("Cache-Control", "no-cache, max-age=0, public")
}
if strings.HasSuffix(req.URL.Path, "/") {
// Do not serve folder-ish resources.
i.ErrorPage(rw, http.StatusNotFound, "", "")
return
}
handler.ServeHTTP(rw, req)
})
}
func (i *Identifier) secureHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
var err error
// TODO(longsleep): Add support for X-Forwareded-Host with trusted proxy.
// NOTE: this does not protect from DNS rebinding. Protection for that
// should be added at the frontend proxy.
requiredHost := req.Host
// This follows https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet
for {
if req.Header.Get("Kopano-Konnect-XSRF") != "1" {
err = fmt.Errorf("missing xsrf header")
break
}
origin := req.Header.Get("Origin")
referer := req.Header.Get("Referer")
// Require either Origin and Referer header.
// NOTE(longsleep): Firefox does not send Origin header for POST
// requests when on the same domain - this is fuck (tm). See
// https://bugzilla.mozilla.org/show_bug.cgi?id=446344 for reference.
if origin == "" && referer == "" {
err = fmt.Errorf("missing origin or referer header")
break
}
if origin != "" {
originURL, urlParseErr := url.Parse(origin)
if urlParseErr != nil {
err = fmt.Errorf("invalid origin value: %v", urlParseErr)
break
}
if originURL.Host != requiredHost {
err = fmt.Errorf("origin does not match request URL")
break
}
} else if referer != "" {
refererURL, urlParseErr := url.Parse(referer)
if urlParseErr != nil {
err = fmt.Errorf("invalid referer value: %v", urlParseErr)
break
}
if refererURL.Host != requiredHost {
err = fmt.Errorf("referer does not match request URL")
break
}
} else {
i.logger.WithFields(logrus.Fields{
"host": requiredHost,
"user-agent": req.UserAgent(),
}).Warn("identifier HTTP request is insecure with no Origin and Referer")
}
handler.ServeHTTP(rw, req)
return
}
if err != nil {
i.logger.WithError(err).WithFields(logrus.Fields{
"host": requiredHost,
"referer": req.Referer(),
"user-agent": req.UserAgent(),
"origin": req.Header.Get("Origin"),
}).Warn("rejecting identifier HTTP request")
}
i.ErrorPage(rw, http.StatusBadRequest, "", "")
})
}
func (i *Identifier) handleIdentifier(rw http.ResponseWriter, req *http.Request) {
addCommonResponseHeaders(rw.Header())
addNoCacheResponseHeaders(rw.Header())
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request")
return
}
switch req.Form.Get("flow") {
case FlowOIDC, FlowOAuth, "":
if req.Form.Get("identifier") != MustBeSignedIn {
// Check if there is a default authority, if so use that.
authority := i.authorities.Default(req.Context())
if authority != nil {
switch authority.AuthorityType {
case authorities.AuthorityTypeOIDC:
i.writeOAuth2Start(rw, req, authority)
case authorities.AuthorityTypeSAML2:
i.writeSAML2Start(rw, req, authority)
default:
i.ErrorPage(rw, http.StatusNotImplemented, "", "unknown authority type")
}
return
}
}
}
// Show default.
i.writeWebappIndexHTML(rw, req)
}
func (i *Identifier) handleLogon(rw http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var r LogonRequest
err := decoder.Decode(&r)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode logon request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request JSON")
return
}
var user *IdentifiedUser
response := &LogonResponse{
State: r.State,
}
addNoCacheResponseHeaders(rw.Header())
record := &Record{}
if r.Hello != nil {
err = r.Hello.parse()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to parse logon request hello")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to parse request values")
return
}
record.HelloRequest = r.Hello
}
req = req.WithContext(NewRecordContext(req.Context(), record))
// Params is an array like this [$username, $password, $mode], defining a
// extensible way to extend login modes over time. The minimal length of
// the params array is 1 with only [$username]. Second field is the password
// but its interpretation depends on the third field ($mode). The rest of the
// fields are mode specific.
params := r.Params
for {
paramSize := len(params)
if paramSize == 0 {
i.ErrorPage(rw, http.StatusBadRequest, "", "params required")
break
}
if paramSize >= 3 && params[1] == "" && params[2] == ModeLogonUsernameEmptyPasswordCookie {
// Special mode to allow when same user is logged in via cookie. This
// is used in the select account page logon flow with empty password.
identifiedUser, cookieErr := i.GetUserFromLogonCookie(req.Context(), req, 0, true)
if cookieErr != nil {
i.logger.WithError(cookieErr).Debugln("identifier failed to decode logon cookie in logon request")
}
if identifiedUser != nil {
if identifiedUser.Username() == params[0] {
user = identifiedUser
break
}
}
}
audience := ""
if r.Hello != nil {
audience = r.Hello.ClientID
}
if paramSize < 3 {
// Unsupported logon mode.
break
}
if params[1] == "" {
// Empty password, stop here - never allowed in any mode.
break
}
switch params[2] {
case ModeLogonUsernamePassword:
// Username and password validation mode.
logonedUser, logonErr := i.logonUser(req.Context(), audience, params[0], params[1])
if logonErr != nil {
i.logger.WithError(logonErr).Errorln("identifier failed to logon with backend")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to logon")
return
}
user = logonedUser
default:
i.logger.Debugln("identifier unknown logon mode: %v", params[2])
}
break
}
if user == nil || user.Subject() == "" {
rw.Header().Set("Kopano-Konnect-State", response.State)
rw.WriteHeader(http.StatusNoContent)
return
}
// Get user meta data.
// TODO(longsleep): This is an additional request to the backend. This
// should be avoided. Best would be if the backend would return everything
// in one shot (TODO in core).
err = i.updateUser(req.Context(), user, nil)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to update user data in logon request")
}
// Set logon time.
user.logonAt = time.Now()
if r.Hello != nil {
hello, errHello := i.writeHelloResponse(rw, req, r.Hello, user)
if errHello != nil {
i.logger.WithError(errHello).Debugln("rejecting identifier logon request")
i.ErrorPage(rw, http.StatusBadRequest, "", errHello.Error())
return
}
if !hello.Success {
rw.Header().Set("Kopano-Konnect-State", response.State)
rw.WriteHeader(http.StatusNoContent)
return
}
response.Hello = hello
}
err = i.SetUserToLogonCookie(req.Context(), rw, user)
if err != nil {
i.logger.WithError(err).Errorln("failed to serialize logon ticket")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to serialize logon ticket")
return
}
response.Success = true
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
i.logger.WithError(err).Errorln("logon request failed writing response")
}
}
func (i *Identifier) handleLogoff(rw http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var r StateRequest
err := decoder.Decode(&r)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode logoff request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request JSON")
return
}
addNoCacheResponseHeaders(rw.Header())
ctx := req.Context()
u, err := i.GetUserFromLogonCookie(ctx, req, 0, false)
if err != nil {
i.logger.WithError(err).Warnln("identifier logoff failed to get logon from ticket")
}
err = i.UnsetLogonCookie(ctx, u, rw)
if err != nil {
i.logger.WithError(err).Errorln("identifier failed to set logoff ticket")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to set logoff ticket")
return
}
response := &StateResponse{
State: r.State,
Success: true,
}
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
i.logger.WithError(err).Errorln("logoff request failed writing response")
}
}
func (i *Identifier) handleConsent(rw http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var r ConsentRequest
err := decoder.Decode(&r)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode consent request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request JSON")
return
}
addNoCacheResponseHeaders(rw.Header())
consent := &Consent{
Allow: r.Allow,
}
if r.Allow {
consent.RawScope = r.RawScope
}
err = i.SetConsentToConsentCookie(req.Context(), rw, &r, consent)
if err != nil {
i.logger.WithError(err).Errorln("failed to serialize consent ticket")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to serialize consent ticket")
return
}
if !r.Allow {
rw.Header().Set("Kopano-Konnect-State", r.State)
rw.WriteHeader(http.StatusNoContent)
return
}
response := &StateResponse{
State: r.State,
Success: true,
}
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
i.logger.WithError(err).Errorln("logoff request failed writing response")
}
}
func (i *Identifier) handleHello(rw http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var r HelloRequest
err := decoder.Decode(&r)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode hello request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request JSON")
return
}
err = r.parse()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to parse hello request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to parse request values")
return
}
addNoCacheResponseHeaders(rw.Header())
response, err := i.writeHelloResponse(rw, req, &r, nil)
if err != nil {
i.logger.WithError(err).Debugln("rejecting identifier hello request")
i.ErrorPage(rw, http.StatusBadRequest, "", err.Error())
return
}
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
i.logger.WithError(err).Errorln("hello request failed writing response")
}
}
func (i *Identifier) handleTrampolin(rw http.ResponseWriter, req *http.Request) {
if !strings.HasSuffix(req.URL.Path, ".js") {
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode trampolin request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request parameters")
return
}
sd, err := i.GetStateFromStateCookie(req.Context(), rw, req, "trampolin", req.Form.Get("state"))
if err != nil {
i.ErrorPage(rw, http.StatusBadRequest, "", err.Error())
return
}
if sd == nil || sd.Trampolin == nil {
i.ErrorPage(rw, http.StatusBadRequest, "", "no state")
return
}
scope := sd.Trampolin.Scope
uri, _ := url.Parse(sd.Trampolin.URI)
sd.Trampolin = nil
err = i.SetStateToStateCookie(req.Context(), rw, scope, sd)
if err != nil {
i.logger.WithError(err).Errorln("failed to write trampolin state cookie")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to write trampolin state cookie")
return
}
i.writeTrampolinHTML(rw, req, uri)
} else {
i.writeTrampolinScript(rw, req)
}
}
func (i *Identifier) handleOAuth2Start(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode oauth2 start request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request parameters")
return
}
var authority *authorities.Details
if authorityID := req.Form.Get("authority_id"); authorityID != "" {
authority, _ = i.authorities.Lookup(req.Context(), authorityID)
}
i.writeOAuth2Start(rw, req, authority)
}
func (i *Identifier) handleOAuth2Cb(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode oauth2 cb request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request parameters")
return
}
i.writeOAuth2Cb(rw, req)
}
func (i *Identifier) handleSAML2Metadata(rw http.ResponseWriter, req *http.Request) {
authorityDetails := i.authorities.Default(req.Context())
if authorityDetails == nil || authorityDetails.AuthorityType != authorities.AuthorityTypeSAML2 {
i.ErrorPage(rw, http.StatusNotFound, "", "saml not configured")
return
}
metadata := authorityDetails.Metadata()
if metadata == nil {
i.ErrorPage(rw, http.StatusNotFound, "", "saml has no meta data")
return
}
buf, _ := xml.MarshalIndent(metadata, "", " ")
rw.Header().Set("Content-Type", "application/samlmetadata+xml")
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(xml.Header))
rw.Write(buf)
}
func (i *Identifier) handleSAML2AssertionConsumerService(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode saml2 acs request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request parameters")
return
}
i.writeSAML2AssertionConsumerService(rw, req)
}
func (i *Identifier) handleSAML2SingleLogoutService(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode saml2 slo request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request parameters")
return
}
if _, ok := req.Form["SAMLRequest"]; ok {
i.writeSAMLSingleLogoutServiceRequest(rw, req)
} else if _, ok := req.Form["SAMLResponse"]; ok {
i.writeSAMLSingleLogoutServiceResponse(rw, req)
} else {
i.ErrorPage(rw, http.StatusBadRequest, "", "neither SAMLRequest nor SAMLResponse parameter found")
}
}

View File

@@ -0,0 +1,761 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package identifier
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/deckarep/golang-set"
"github.com/gorilla/mux"
"github.com/longsleep/rndm"
"github.com/sirupsen/logrus"
jose "gopkg.in/square/go-jose.v2"
jwt "gopkg.in/square/go-jose.v2/jwt"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identifier/backends"
"github.com/libregraph/lico/identifier/meta"
"github.com/libregraph/lico/identifier/meta/scopes"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/authorities"
"github.com/libregraph/lico/identity/clients"
"github.com/libregraph/lico/managers"
"github.com/libregraph/lico/utils"
"github.com/libregraph/oidc-go"
)
// audienceMarker defines the value which gets included in logon cookies. Valid
// logon cookies must have the first value of this list in their audience claim.
// Increment this value whenever logon cookie claims and format changes in
// non-backwards compatible ways. User will have to sign in again to get a new
// cookie.
var audienceMarker = jwt.Audience([]string{"2019012201"})
// Identifier defines a identification login area with its endpoints using
// a Kopano Core server as backend logon provider.
type Identifier struct {
Config *Config
baseURI *url.URL
pathPrefix string
staticFolder string
logonCookieName string
scopesConf string
webappIndexHTML []byte
authorizationEndpointURI *url.URL
signedOutEndpointURI *url.URL
oauth2CbEndpointURI *url.URL
encrypter jose.Encrypter
recipient *jose.Recipient
backend backends.Backend
clients *clients.Registry
authorities *authorities.Registry
meta *meta.Meta
defaultBannerLogo *string
onSetLogonCallbacks []func(ctx context.Context, rw http.ResponseWriter, user identity.User) error
onUnsetLogonCallbacks []func(ctx context.Context, rw http.ResponseWriter) error
logger logrus.FieldLogger
router *mux.Router
}
// NewIdentifier returns a new Identifier.
func NewIdentifier(c *Config) (*Identifier, error) {
staticFolder := c.StaticFolder
var webappIndexHTML = make([]byte, 0)
if !c.WebAppDisabled {
fn := staticFolder + "/index.html"
if _, statErr := os.Stat(fn); os.IsNotExist(statErr) {
return nil, fmt.Errorf("identifier client index.html not found: %w", statErr)
}
readData, readErr := ioutil.ReadFile(fn)
if readErr != nil {
return nil, fmt.Errorf("identifier failed to read client index.html: %w", readErr)
}
webappIndexHTML = bytes.Replace(readData, []byte("__PATH_PREFIX__"), []byte(c.PathPrefix), 1)
}
oauth2CbEndpointURI, _ := url.Parse(c.BaseURI.String())
oauth2CbEndpointURI.Path = c.PathPrefix + "/identifier/oauth2/cb"
i := &Identifier{
Config: c,
baseURI: c.BaseURI,
pathPrefix: c.PathPrefix,
staticFolder: staticFolder,
logonCookieName: c.LogonCookieName,
scopesConf: c.ScopesConf,
webappIndexHTML: webappIndexHTML,
authorizationEndpointURI: c.AuthorizationEndpointURI,
signedOutEndpointURI: c.SignedOutEndpointURI,
oauth2CbEndpointURI: oauth2CbEndpointURI,
backend: c.Backend,
onSetLogonCallbacks: make([]func(ctx context.Context, rw http.ResponseWriter, user identity.User) error, 0),
onUnsetLogonCallbacks: make([]func(ctx context.Context, rw http.ResponseWriter) error, 0),
logger: c.Config.Logger,
}
var err error
i.meta = &meta.Meta{}
i.meta.Scopes, err = scopes.NewScopesFromFile(i.scopesConf, i.logger)
if err != nil {
return nil, err
}
if c.DefaultBannerLogo != nil {
defaultBannerLogo, err := encodeImageAsDataURL(c.DefaultBannerLogo)
if err != nil {
return nil, fmt.Errorf("failed to encode default banner logo: %w", err)
}
i.defaultBannerLogo = &defaultBannerLogo
}
i.meta.Scopes.Extend(c.Backend.ScopesMeta())
return i, nil
}
// RegisterManagers registers the provided managers,
func (i *Identifier) RegisterManagers(mgrs *managers.Managers) error {
i.clients = mgrs.Must("clients").(*clients.Registry)
i.authorities = mgrs.Must("authorities").(*authorities.Registry)
if service, ok := i.backend.(managers.ServiceUsesManagers); ok {
err := service.RegisterManagers(mgrs)
if err != nil {
return err
}
}
return nil
}
// AddRoutes adds the endpoint routes of the accociated Identifier to the
// provided router with the provided context.
func (i *Identifier) AddRoutes(ctx context.Context, router *mux.Router) {
r := router.PathPrefix(i.pathPrefix).Subrouter()
r.PathPrefix("/static/").Handler(i.staticHandler(http.StripPrefix(i.pathPrefix, http.FileServer(http.Dir(i.staticFolder))), true))
r.Handle("/service-worker.js", i.staticHandler(http.StripPrefix(i.pathPrefix, http.FileServer(http.Dir(i.staticFolder))), false))
r.Handle("/identifier", http.HandlerFunc(i.handleIdentifier)).Methods(http.MethodGet).Name("index")
r.Handle("/chooseaccount", i).Methods(http.MethodGet).Name("chooseaccount")
r.Handle("/consent", i).Methods(http.MethodGet).Name("consent")
r.Handle("/welcome", i).Methods(http.MethodGet).Name("welcome")
r.Handle("/goodbye", i).Methods(http.MethodGet).Name("goodbye")
r.Handle("/index.html", i).Methods(http.MethodGet) // For service worker.
r.Handle("/identifier/_/logon", i.secureHandler(http.HandlerFunc(i.handleLogon))).Methods(http.MethodPost)
r.Handle("/identifier/_/logoff", i.secureHandler(http.HandlerFunc(i.handleLogoff))).Methods(http.MethodPost)
r.Handle("/identifier/_/hello", i.secureHandler(http.HandlerFunc(i.handleHello))).Methods(http.MethodPost)
r.Handle("/identifier/_/consent", i.secureHandler(http.HandlerFunc(i.handleConsent))).Methods(http.MethodPost)
r.Handle("/identifier/oauth2/start", http.HandlerFunc(i.handleOAuth2Start)).Methods(http.MethodGet).Name("oauth2/start")
r.Handle("/identifier/oauth2/cb", http.HandlerFunc(i.handleOAuth2Cb)).Methods(http.MethodGet).Name("oauth2/cb")
r.Handle("/identifier/saml2/metadata", http.HandlerFunc(i.handleSAML2Metadata))
r.Handle("/identifier/saml2/acs", http.HandlerFunc(i.handleSAML2AssertionConsumerService)).Methods(http.MethodPost).Name("saml2/acs")
r.Handle("/identifier/_/saml2/slo", http.HandlerFunc(i.handleSAML2SingleLogoutService)).Methods(http.MethodGet).Name("saml2/slo")
r.Handle("/identifier/trampolin", http.HandlerFunc(i.handleTrampolin)).Methods(http.MethodGet).Name("trampolin")
r.Handle("/identifier/trampolin/trampolin.js", http.HandlerFunc(i.handleTrampolin)).Methods(http.MethodGet)
i.router = r
if i.backend != nil {
i.backend.RunWithContext(ctx)
}
}
// ServeHTTP implements the http.Handler interface.
func (i *Identifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
addCommonResponseHeaders(rw.Header())
addNoCacheResponseHeaders(rw.Header())
// Show default.
i.writeWebappIndexHTML(rw, req)
}
// SetKey sets the provided key for the accociated identifier.
func (i *Identifier) SetKey(key []byte) error {
var ce jose.ContentEncryption
var algo jose.KeyAlgorithm
switch len(key) {
case 16:
ce = jose.A128GCM
algo = jose.A128GCMKW
case 24:
ce = jose.A192GCM
algo = jose.A192GCMKW
case 32:
ce = jose.A256GCM
algo = jose.A256GCMKW
default:
return fmt.Errorf("identifier invalid encryption key size. Need 16, 24 or 32 bytes")
}
if len(key) < 32 {
i.logger.Warnf("identifier using encryption key size with %d bytes which is below 32 bytes", len(key))
} else {
i.logger.WithField("security", fmt.Sprintf("%s:%s", ce, algo)).Infoln("identifier set up")
}
recipient := jose.Recipient{
Algorithm: algo,
KeyID: "",
Key: key,
}
encrypter, err := jose.NewEncrypter(
ce,
recipient,
nil,
)
if err != nil {
return err
}
i.encrypter = encrypter
i.recipient = &recipient
return nil
}
// ErrorPage writes a HTML error page to the provided ResponseWriter.
func (i *Identifier) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
utils.WriteErrorPage(rw, code, title, message)
}
// SetUserToLogonCookie serializes the provided user into an encrypted string
// and sets it as cookie on the provided http.ResponseWriter.
func (i *Identifier) SetUserToLogonCookie(ctx context.Context, rw http.ResponseWriter, user *IdentifiedUser) error {
loggedOn, logonAt := user.LoggedOn()
if !loggedOn {
return fmt.Errorf("refused to set cookie for not logged on user")
}
// Add standard claims.
claims := jwt.Claims{
Issuer: user.BackendName(),
Audience: audienceMarker,
Subject: user.Subject(),
IssuedAt: jwt.NewNumericDate(logonAt),
}
// Add expiration, if set.
if user.expiresAfter != nil {
claims.Expiry = jwt.NewNumericDate(*user.expiresAfter)
}
// Additional claims.
userClaims := map[string]interface{}(user.Claims())
if sessionRef := user.SessionRef(); sessionRef != nil {
userClaims[SessionIDClaim] = *sessionRef
}
if logonRef := user.LogonRef(); logonRef != nil {
userClaims[LogonRefClaim] = *logonRef
}
if externalAuthorityID := user.ExternalAuthorityID(); externalAuthorityID != nil {
userClaims[ExternalAuthorityIDClaim] = *externalAuthorityID
}
if lockedScopes := user.LockedScopes(); lockedScopes != nil {
userClaims[LockedScopesClaim] = strings.Join(lockedScopes, " ")
}
// Serialize and encrypt cookie value.
serialized, err := jwt.Encrypted(i.encrypter).Claims(claims).Claims(userClaims).CompactSerialize()
if err != nil {
return err
}
// Set cookie.
err = i.setLogonCookie(rw, serialized)
if err != nil {
return err
}
// Trigger callbacks.
for _, f := range i.onSetLogonCallbacks {
err = f(ctx, rw, user)
if err != nil {
return err
}
}
return nil
}
// UnsetLogonCookie adds cookie remove headers to the provided http.ResponseWriter
// effectively implementing logout.
func (i *Identifier) UnsetLogonCookie(ctx context.Context, user *IdentifiedUser, rw http.ResponseWriter) error {
// Remove cookie.
err := i.removeLogonCookie(rw)
if err != nil {
return err
}
// Destroy backend user session if any.
if user != nil {
if sessionRef := user.SessionRef(); sessionRef != nil {
err = i.backend.DestroySession(ctx, sessionRef)
if err != nil {
i.logger.WithError(err).Warnln("failed to destroy session on unset logon cookie")
}
}
}
// Trigger callbacks.
for _, f := range i.onUnsetLogonCallbacks {
err = f(ctx, rw)
if err != nil {
return err
}
}
return nil
}
// EndSession begins the process to end the session either directly or indirectly
// based on the provided user. It optionally returns an uri which shall be used
// as redirection target or an error.
func (i *Identifier) EndSession(ctx context.Context, user *IdentifiedUser, rw http.ResponseWriter, postRedirectURI *url.URL, state string) (*url.URL, error) {
err := i.UnsetLogonCookie(ctx, user, rw)
if err != nil {
return nil, err
}
var uri *url.URL
if user.externalAuthority != nil && user.externalAuthority.EndSessionEnabled {
// Generate state and set state cookie with postRedirectURI.
if state == "" {
state = rndm.GenerateRandomString(32)
}
sd := &StateData{
State: state,
Mode: StateModeEndSession,
Ref: user.externalAuthority.ID,
}
var extra map[string]interface{}
uri, extra, err = user.externalAuthority.MakeRedirectEndSessionRequestURL(user.LogonRef(), sd.State)
if err != nil {
return nil, err
}
sd.Extra = extra
if postRedirectURI != nil && postRedirectURI.String() != "" {
sd.RawQuery = postRedirectURI.String()
}
var scope string
switch user.externalAuthority.AuthorityType {
case authorities.AuthorityTypeOIDC:
// Inject post logout url target.
cb, _ := i.router.GetRoute("oauth2/cb").URL()
next, _ := url.Parse(i.baseURI.String())
next.Path = cb.Path
query := uri.Query()
query.Set("post_logout_redirect_uri", next.String())
uri.RawQuery = query.Encode()
// Redirect using trampolin, to ensure origin checks of external
// authority can pass.
sd.Trampolin = &TrampolinData{
URI: uri.String(),
Scope: "oauth2/cb",
}
scope = "trampolin"
uri, _ = i.router.GetRoute("trampolin").URL()
query = make(url.Values)
query.Add("state", sd.State)
uri.RawQuery = query.Encode()
case authorities.AuthorityTypeSAML2:
scope = "_/saml2/slo"
}
if scope != "" {
err = i.SetStateToStateCookie(ctx, rw, scope, sd)
if err != nil {
return nil, fmt.Errorf("failed to set saml2 slo state cookie: %w", err)
}
}
}
return uri, nil
}
// GetUserFromLogonCookie looks up the associated cookie name from the provided
// request, parses it and returns the user containing the information found in
// the coookie payload data.
func (i *Identifier) GetUserFromLogonCookie(ctx context.Context, req *http.Request, maxAge time.Duration, refreshSession bool) (*IdentifiedUser, error) {
cookie, err := i.getLogonCookie(req)
if err != nil {
if err == http.ErrNoCookie {
return nil, nil
}
return nil, err
}
// Decrypt and parse cookie value.
token, err := jwt.ParseEncrypted(cookie.Value)
if err != nil {
return nil, err
}
// Parse claims.
var claims jwt.Claims
var userClaims map[string]interface{}
if claimsErr := token.Claims(i.recipient.Key, &claims, &userClaims); claimsErr != nil {
return nil, claimsErr
}
// Validate claims.
if claimsErr := claims.Validate(jwt.Expected{
// Ignore cookie, when issuer does not match our backend name. This usually
// means that konnect was reconfigured. Users need to sign in again.
Issuer: i.backend.Name(),
// Ignore cookie, when audience marker does not match. This happens
// for cookies from an older version of konnect. Users need to sign in again.
Audience: jwt.Audience{audienceMarker[0]},
}); claimsErr != nil {
i.logger.WithError(claimsErr).Debugln("logon token claims validation failed")
return nil, nil
}
if claims.Subject == "" {
return nil, fmt.Errorf("invalid subject in logon token")
}
if userClaims == nil {
return nil, fmt.Errorf("invalid user claims in logon token")
}
// New user with details from claims.
user := &IdentifiedUser{
sub: claims.Subject,
// TODO(longsleep): It is not verified here that the user still exists at
// our current backend. We still assign the backend happily here - probably
// needs some sort of veritification / lookup.
backend: i.backend,
logonAt: claims.IssuedAt.Time(),
}
if claims.Expiry != nil {
expiresAfter := claims.Expiry.Time()
user.expiresAfter = &expiresAfter
}
loggedOn, logonAt := user.LoggedOn()
if !loggedOn {
// Ignore logons which are not valid.
return nil, nil
}
if maxAge > 0 {
if logonAt.Add(maxAge).Before(time.Now()) {
// Ignore logon as it is no longer valid within maxAge.
return nil, nil
}
}
// Get specific data from claims.
if v := userClaims[SessionIDClaim]; v != nil {
sessionRef := v.(string)
if sessionRef != "" {
// Remember session ref in user.
user.sessionRef = &sessionRef
// Ensure the session is still valid, by refreshing it.
if refreshSession {
err = i.backend.RefreshSession(ctx, user.Subject(), &sessionRef, userClaims)
if err != nil {
// Ignore logons which fail session refresh.
return nil, nil
}
}
}
}
if v := userClaims[LogonRefClaim]; v != nil {
logonRef := v.(string)
if logonRef != "" {
// Remember logon ref in user.
user.logonRef = &logonRef
}
}
if v := userClaims[ExternalAuthorityIDClaim]; v != nil {
externalAuthorityID := v.(string)
if externalAuthorityID != "" {
authority, err := i.authorities.Lookup(ctx, externalAuthorityID)
if err != nil {
// Ignore logons which have set an unknown external authority.
return nil, nil
}
// TODO(longsleep): Check if authority is actually enabled. For now
// we check if it is ready.
if !authority.IsReady() {
// Ignore logons which have sent an authority which is not ready.
return nil, nil
}
user.externalAuthority = authority
}
}
if v := userClaims[LockedScopesClaim]; v != nil {
lockedScopes := v.(string)
if lockedScopes != "" {
user.lockedScopes = strings.Split(lockedScopes, " ")
}
}
// Fill additional claim.
user.claims = make(map[string]interface{})
for k, v := range userClaims {
switch k {
case konnect.IdentifiedUsernameClaim:
user.username = v.(string)
case konnect.IdentifiedDisplayNameClaim:
user.displayName = v.(string)
case SessionIDClaim:
// Already handled above.
continue
case LogonRefClaim:
// Already handled above.
continue
case ExternalAuthorityIDClaim:
// Already handled above.
continue
case LockedScopesClaim:
// Already handled above.
continue
case ObsoleteUserClaimsClaim:
// Keep and ignore for history reasons.
continue
case oidc.AudienceClaim, oidc.IssuedAtClaim, oidc.ExpirationClaim, oidc.SubjectIdentifierClaim, oidc.IssuerIdentifierClaim:
// Ignore default OIDC claims when resurrecting claims data.
continue
default:
// Add the rest.
user.claims[k] = v
}
}
return user, nil
}
// GetUserFromID looks up the user identified by the provided userID by
// requesting the associated backend.
func (i *Identifier) GetUserFromID(ctx context.Context, userID string, sessionRef *string, requestedScopes map[string]bool) (*IdentifiedUser, error) {
user, err := i.backend.GetUser(ctx, userID, sessionRef, requestedScopes)
if err != nil {
return nil, err
}
if user == nil {
return nil, nil
}
// XXX(longsleep): This is quite crappy. Move IdentifiedUser to a package
// which can be imported by backends so they directly can return that shit.
identifiedUser := &IdentifiedUser{
sub: user.Subject(),
username: user.Username(),
backend: i.backend,
sessionRef: sessionRef,
claims: user.BackendClaims(),
scopes: user.BackendScopes(),
lockedScopes: user.RequiredScopes(),
}
if userWithEmail, ok := user.(identity.UserWithEmail); ok {
identifiedUser.email = userWithEmail.Email()
identifiedUser.emailVerified = userWithEmail.EmailVerified()
}
if userWithProfile, ok := user.(identity.UserWithProfile); ok {
identifiedUser.displayName = userWithProfile.Name()
identifiedUser.familyName = userWithProfile.FamilyName()
identifiedUser.givenName = userWithProfile.GivenName()
}
if userWithID, ok := user.(identity.UserWithID); ok {
identifiedUser.id = userWithID.ID()
}
if userWithUniqueID, ok := user.(identity.UserWithUniqueID); ok {
identifiedUser.uid = userWithUniqueID.UniqueID()
}
return identifiedUser, nil
}
// SetConsentToConsentCookie serializses the provided Consent using the provided
// ConsentRequest and sets it as cookie on the provided ReponseWriter.
func (i *Identifier) SetConsentToConsentCookie(ctx context.Context, rw http.ResponseWriter, cr *ConsentRequest, consent *Consent) error {
serialized, err := jwt.Encrypted(i.encrypter).Claims(consent).CompactSerialize()
if err != nil {
return err
}
return i.setConsentCookie(rw, cr, serialized)
}
// GetConsentFromConsentCookie extract consent information for the provided
// request and the provide state.
func (i *Identifier) GetConsentFromConsentCookie(ctx context.Context, rw http.ResponseWriter, req *http.Request, state string) (*Consent, error) {
if state == "" {
return nil, nil
}
cr := &ConsentRequest{
State: state,
ClientID: req.Form.Get("client_id"),
RawRedirectURI: req.Form.Get("redirect_uri"),
Ref: req.Form.Get("state"),
Nonce: req.Form.Get("nonce"),
}
cookie, err := i.getConsentCookie(req, cr)
if err != nil {
if err == http.ErrNoCookie {
return nil, nil
}
return nil, err
}
// Directly remove the cookie again after we used it.
i.removeConsentCookie(rw, req, cr)
token, err := jwt.ParseEncrypted(cookie.Value)
if err != nil {
return nil, err
}
var consent Consent
if err = token.Claims(i.recipient.Key, &consent); err != nil {
return nil, err
}
return &consent, nil
}
// SetStateToStateCookie serializses the provided StateRequest and sets it
// as cookie on the provided ReponseWriter.
func (i *Identifier) SetStateToStateCookie(ctx context.Context, rw http.ResponseWriter, scope string, sd *StateData) error {
serialized, err := jwt.Encrypted(i.encrypter).Claims(sd).CompactSerialize()
if err != nil {
return err
}
return i.setStateCookie(rw, scope, sd.State, serialized)
}
// GetStateFromStateCookie extracts state information for the provided
// request using the provided scope and state.
func (i *Identifier) GetStateFromStateCookie(ctx context.Context, rw http.ResponseWriter, req *http.Request, scope string, state string) (*StateData, error) {
if state == "" {
return nil, nil
}
cookie, err := i.getStateCookie(req, state)
if err != nil {
if err == http.ErrNoCookie {
return nil, nil
}
return nil, err
}
// Directly remove the cookie again after we used it.
i.removeStateCookie(rw, req, scope, state)
token, err := jwt.ParseEncrypted(cookie.Value)
if err != nil {
return nil, err
}
sd := &StateData{}
if err = token.Claims(i.recipient.Key, sd); err != nil {
return nil, err
}
if sd.State != state {
return nil, fmt.Errorf("state mismatch")
}
return sd, nil
}
// Name returns the active identifiers backend's name.
func (i *Identifier) Name() string {
return i.backend.Name()
}
// ScopesSupported return the scopes supported by the accociated Identifier.
func (i *Identifier) ScopesSupported() []string {
scopes := mapset.NewThreadUnsafeSet()
for scope := range i.meta.Scopes.Definitions {
scopes.Add(scope)
}
for _, scope := range i.backend.ScopesSupported() {
scopes.Add(scope)
}
supportedScopes := make([]string, 0)
it := scopes.Iterator()
for scope := range it.C {
supportedScopes = append(supportedScopes, scope.(string))
}
return supportedScopes
}
// OnSetLogon implements a way to register hooks whenever logon information is
// set by the accociated Identifier.
func (i *Identifier) OnSetLogon(cb func(ctx context.Context, rw http.ResponseWriter, user identity.User) error) error {
i.onSetLogonCallbacks = append(i.onSetLogonCallbacks, cb)
return nil
}
// OnUnsetLogon implements a way to register hooks whenever logon information is
// set by the accociated Identifier.
func (i *Identifier) OnUnsetLogon(cb func(ctx context.Context, rw http.ResponseWriter) error) error {
i.onUnsetLogonCallbacks = append(i.onUnsetLogonCallbacks, cb)
return nil
}
func (i *Identifier) absoluteURLForRoute(name string) (*url.URL, error) {
uri, _ := url.Parse(i.Config.BaseURI.String())
route := i.router.Get(name)
path, err := route.URL()
if err != nil {
return nil, err
}
uri.Path = path.Path
return uri, nil
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2021 Kopano and its licensors
*
* 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.
*
*/
package meta
// Branding is a container to hold identifier branding meta data.
type Branding struct {
BannerLogo *string `json:"bannerLogo,omitempty"`
SignInPageText *string `json:"signinPageText,omitempty"`
UsernameHintText *string `json:"usernameHintText,omitempty"`
Locales []string `json:"locales,omitempty"`
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package meta
import (
"github.com/libregraph/lico/identifier/meta/scopes"
)
// Meta is a container to hold identifier meta data which can be requested by
// clients.
type Meta struct {
Scopes *scopes.Scopes `json:"scopes"`
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package scopes
// A Definition contains the meta data for a single scope.
type Definition struct {
Priority int `json:"priority" yaml:"priority"`
Description string `json:"description,omitempty" yaml:"description"`
ID string `json:"id,omitempty"`
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package scopes
import (
"io/ioutil"
"github.com/libregraph/oidc-go"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
konnect "github.com/libregraph/lico"
)
const (
scopeAliasBasic = "basic"
scopeUnknown = "unknown"
)
const (
priorityBasic = 20
priorityOfflineAccess = 10
)
var defaultScopesMap = map[string]string{
oidc.ScopeOpenID: scopeAliasBasic,
oidc.ScopeEmail: scopeAliasBasic,
oidc.ScopeProfile: scopeAliasBasic,
konnect.ScopeNumericID: scopeAliasBasic,
konnect.ScopeUniqueUserID: scopeAliasBasic,
konnect.ScopeRawSubject: scopeAliasBasic,
}
var defaultScopesDefinitionMap = map[string]*Definition{
scopeAliasBasic: &Definition{
ID: "scope_alias_basic",
Priority: priorityBasic,
},
oidc.ScopeOfflineAccess: &Definition{
ID: "scope_offline_access",
Priority: priorityOfflineAccess,
},
}
// Scopes contain collections for scope related meta data
type Scopes struct {
Mapping map[string]string `json:"mapping" yaml:"mapping"`
Definitions map[string]*Definition `json:"definitions" yaml:"scopes"`
}
// NewScopesFromIDs creates a new scopes meta data collection from the provided
// scopes IDs optionally also adding definitions from a parent.
func NewScopesFromIDs(scopes map[string]bool, parent *Scopes) *Scopes {
mapping := make(map[string]string)
definitions := make(map[string]*Definition)
for scope, enabled := range scopes {
if !enabled {
continue
}
alias := scope
if mapped, ok := parent.Mapping[scope]; ok {
alias = mapped
mapping[scope] = mapped
} else if mapped, ok := defaultScopesMap[scope]; ok {
alias = mapped
mapping[scope] = mapped
}
if definition, ok := parent.Definitions[alias]; ok {
definitions[alias] = definition
} else if definition, ok := defaultScopesDefinitionMap[alias]; ok {
definitions[alias] = definition
}
}
return &Scopes{
Mapping: mapping,
Definitions: definitions,
}
}
// NewScopesFromFile loads scope definitions from a file.
func NewScopesFromFile(scopesConfFilepath string, logger logrus.FieldLogger) (*Scopes, error) {
scopes := &Scopes{}
if scopesConfFilepath != "" {
logger.Debugf("parsing scopes conf from %v", scopesConfFilepath)
confFile, err := ioutil.ReadFile(scopesConfFilepath)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(confFile, scopes)
if err != nil {
return nil, err
}
for id, definition := range scopes.Definitions {
fields := logrus.Fields{
"id": id,
"priority": definition.Priority,
}
logger.WithFields(fields).Debugln("registered scope")
}
for id, mapped := range scopes.Mapping {
fields := logrus.Fields{
"id": id,
"to": mapped,
}
logger.WithFields(fields).Debugln("registered scope mapping")
}
}
if scopes.Mapping == nil {
scopes.Mapping = make(map[string]string)
}
if scopes.Definitions == nil {
scopes.Definitions = make(map[string]*Definition)
}
return scopes, nil
}
// Extend adds the provided scope mappings and definitions to the accociated
// scopes mappings and definitions with replacing already existing. If scopes is
// nil, Extends is a no-op.
func (s *Scopes) Extend(scopes *Scopes) error {
if scopes == nil {
return nil
}
for scope, definition := range scopes.Definitions {
s.Definitions[scope] = definition
}
for mapped, mapping := range scopes.Mapping {
s.Mapping[mapped] = mapping
}
return nil
}

178
vendor/github.com/libregraph/lico/identifier/models.go generated vendored Normal file
View File

@@ -0,0 +1,178 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package identifier
import (
"net/url"
"strconv"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/libregraph/lico/identifier/meta"
"github.com/libregraph/lico/identity/clients"
)
// A LogonRequest is the request data as sent to the logon endpoint
type LogonRequest struct {
State string `json:"state"`
Params []string `json:"params"`
Hello *HelloRequest `json:"hello"`
}
// A LogonResponse holds a response as sent by the logon endpoint.
type LogonResponse struct {
Success bool `json:"success"`
State string `json:"state"`
Hello *HelloResponse `json:"hello"`
}
// A HelloRequest is the request data as send to the hello endpoint.
type HelloRequest struct {
State string `json:"state"`
Flow string `json:"flow"`
RawScope string `json:"scope"`
RawPrompt string `json:"prompt"`
ClientID string `json:"client_id"`
RawRedirectURI string `json:"redirect_uri"`
RawIDTokenHint string `json:"id_token_hint"`
RawMaxAge string `json:"max_age"`
Scopes map[string]bool `json:"-"`
Prompts map[string]bool `json:"-"`
RedirectURI *url.URL `json:"-"`
IDTokenHint *jwt.Token `json:"-"`
MaxAge time.Duration `json:"-"`
//TODO(longsleep): Add support to pass request parameters as JWT as
// specified in http://openid.net/specs/openid-connect-core-1_0.html#JWTRequests
}
func (hr *HelloRequest) parse() error {
hr.Scopes = make(map[string]bool)
hr.Prompts = make(map[string]bool)
hr.RedirectURI, _ = url.Parse(hr.RawRedirectURI)
if hr.RawScope != "" {
for _, scope := range strings.Split(hr.RawScope, " ") {
hr.Scopes[scope] = true
}
}
if hr.RawPrompt != "" {
for _, prompt := range strings.Split(hr.RawPrompt, " ") {
hr.Prompts[prompt] = true
}
}
if hr.RawMaxAge != "" {
maxAgeInt, err := strconv.ParseInt(hr.RawMaxAge, 10, 64)
if err != nil {
return err
}
hr.MaxAge = time.Duration(maxAgeInt) * time.Second
}
return nil
}
// A HelloResponse holds a response as sent by the hello endpoint.
type HelloResponse struct {
State string `json:"state"`
Flow string `json:"flow"`
Success bool `json:"success"`
Username string `json:"username,omitempty"`
DisplayName string `json:"displayName,omitempty"`
Next string `json:"next,omitempty"`
ContinueURI string `json:"continue_uri,omitempty"`
Scopes map[string]bool `json:"scopes,omitempty"`
ClientDetails *clients.Details `json:"client,omitempty"`
Meta *meta.Meta `json:"meta,omitempty"`
Branding *meta.Branding `json:"branding,omitempty"`
}
// A StateRequest is a general request with a state.
type StateRequest struct {
State string
}
// A StateResponse hilds a response as reply to a StateRequest.
type StateResponse struct {
Success bool `json:"success"`
State string `json:"state"`
}
// StateData contains data bound to a state.
type StateData struct {
State string `json:"state"`
Mode string `json:"mode,omitempty"`
RawQuery string `json:"raw_query,omitempty"`
ClientID string `json:"client_id"`
Ref string `json:"ref,omitempty"`
Extra map[string]interface{} `json:"extra,omitempty"`
Trampolin *TrampolinData `json:"trampolin,omitempty"`
}
type TrampolinData struct {
URI string `json:"uri"`
Scope string `json:"scope"`
}
// A ConsentRequest is the request data as sent to the consent endpoint.
type ConsentRequest struct {
State string `json:"state"`
Allow bool `json:"allow"`
RawScope string `json:"scope"`
ClientID string `json:"client_id"`
RawRedirectURI string `json:"redirect_uri"`
Ref string `json:"ref"`
Nonce string `json:"flow_nonce"`
}
// Consent is the data received and sent to allow or cancel consent flows.
type Consent struct {
Allow bool `json:"allow"`
RawScope string `json:"scope"`
}
// Scopes returns the associated consents approved scopes filtered by the
//provided requested scopes and the full unfiltered approved scopes table.
func (c *Consent) Scopes(requestedScopes map[string]bool) (map[string]bool, map[string]bool) {
scopes := make(map[string]bool)
if c.RawScope != "" {
for _, scope := range strings.Split(c.RawScope, " ") {
scopes[scope] = true
}
}
approved := make(map[string]bool)
for n, v := range requestedScopes {
if ok, _ := scopes[n]; ok && v {
approved[n] = true
}
}
return approved, scopes
}

41
vendor/github.com/libregraph/lico/identifier/modes.go generated vendored Normal file
View File

@@ -0,0 +1,41 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* 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.
*
*/
package identifier
const (
// ModeLogonUsernameEmptyPasswordCookie is the logon mode which requires a
// username which matches the currently signed in user in the cookie and an
// empty password.
ModeLogonUsernameEmptyPasswordCookie = "0"
// ModeLogonUsernamePassword is the logon mode which requires a username
// and a password.
ModeLogonUsernamePassword = "1"
)
const (
// MustBeSignedIn is a authorize mode which tells the authorization code,
// that it is expected to have a signed in user and everything else should
// be treated as error.
MustBeSignedIn = "must"
)
const (
// StateModeEndSession is a state mode which selects end session specific
// actions when processing state requests.
StateModeEndSession = "0"
)

396
vendor/github.com/libregraph/lico/identifier/oauth2.go generated vendored Normal file
View File

@@ -0,0 +1,396 @@
/*
* Copyright 2017-2020 Kopano and its licensors
*
* 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.
*
*/
package identifier
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
"golang.org/x/oauth2"
"github.com/libregraph/lico/identity/authorities"
konnectoidc "github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/payload"
"github.com/libregraph/lico/utils"
)
func (i *Identifier) writeOAuth2Start(rw http.ResponseWriter, req *http.Request, authority *authorities.Details) {
var err error
if authority == nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2TemporarilyUnavailable, "no authority")
} else if !authority.IsReady() {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2TemporarilyUnavailable, "authority not ready")
}
switch typedErr := err.(type) {
case nil:
// breaks
case *konnectoidc.OAuth2Error:
// Redirect back, with error.
i.logger.WithFields(utils.ErrorAsFields(err)).Debugln("oauth2 start error")
// NOTE(longsleep): Pass along error ID but not the description to avoid
// leaking potentially internal information to our RP.
uri, _ := url.Parse(i.authorizationEndpointURI.String())
query, _ := url.ParseQuery(req.URL.RawQuery)
query.Del("flow")
query.Set("error", typedErr.ErrorID)
query.Set("error_description", "identifier failed to authenticate")
uri.RawQuery = query.Encode()
utils.WriteRedirect(rw, http.StatusFound, uri, nil, false)
return
default:
i.logger.WithError(err).Errorln("identifier failed to process oauth2 start")
i.ErrorPage(rw, http.StatusInternalServerError, "", "oauth2 start failed")
return
}
sd := &StateData{
State: rndm.GenerateRandomString(32),
RawQuery: req.URL.RawQuery,
ClientID: authority.ClientID,
Ref: authority.ID,
}
// Construct URL to redirect client to external OAuth2 authorize endpoints.
uri, extra, err := authority.MakeRedirectAuthenticationRequestURL(sd.State)
if err != nil {
i.logger.WithError(err).Errorln("identifier failed to create authentication request: %w", err)
i.ErrorPage(rw, http.StatusInternalServerError, "", "oauth2 start failed")
return
}
if extra != nil {
sd.Extra = extra
} else {
sd.Extra = make(map[string]interface{})
}
query := uri.Query()
query.Add("client_id", authority.ClientID)
if authority.ResponseType != "" {
query.Add("response_type", authority.ResponseType)
}
if authority.ResponseMode != "" {
query.Add("response_mode", authority.ResponseMode)
}
query.Add("scope", strings.Join(authority.Scopes, " "))
query.Add("redirect_uri", i.oauth2CbEndpointURI.String())
query.Add("nonce", rndm.GenerateRandomString(32))
if authority.CodeChallengeMethod != "" {
codeVerifier := rndm.GenerateRandomString(32)
sd.Extra["code_verifier"] = codeVerifier
codeChallenge := ""
if codeChallenge, err = oidc.MakeCodeChallenge(authority.CodeChallengeMethod, codeVerifier); err == nil {
query.Add("code_challenge", codeChallenge)
query.Add("code_challenge_method", authority.CodeChallengeMethod)
} else {
i.logger.WithError(err).Debugln("identifier failed to create oauth2 code challenge")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to create code challenge")
return
}
}
if display := req.Form.Get("display"); display != "" {
query.Add("display", display)
}
if prompt := req.Form.Get("prompt"); prompt != "" && prompt != oidc.PromptConsent {
// Pass along all prompt values, except consent to external provider and
// handle consent as needed ourselves.
query.Add("prompt", prompt)
}
if maxAge := req.Form.Get("max_age"); maxAge != "" {
query.Add("max_age", maxAge)
}
if uiLocales := req.Form.Get("ui_locales"); uiLocales != "" {
query.Add("ui_locales", uiLocales)
}
if acrValues := req.Form.Get("acr_values"); acrValues != "" {
query.Add("acr_values", acrValues)
}
if claimsLocales := req.Form.Get("claims_locales"); claimsLocales != "" {
query.Add("claims_locales", claimsLocales)
}
// Set cookie which is consumed by the callback later.
err = i.SetStateToStateCookie(req.Context(), rw, "oauth2/cb", sd)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to set oauth2 state cookie")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to set cookie")
return
}
uri.RawQuery = query.Encode()
utils.WriteRedirect(rw, http.StatusFound, uri, nil, false)
}
func (i *Identifier) writeOAuth2Cb(rw http.ResponseWriter, req *http.Request) {
// Callbacks from authorization or end session. Validate as specified at
// https://tools.ietf.org/html/rfc6749#section-4.1.2 and https://tools.ietf.org/html/rfc6749#section-10.12.
var err error
var sd *StateData
var user *IdentifiedUser
var userInfoClaims jwt.MapClaims
var authority *authorities.Details
for {
sd, err = i.GetStateFromStateCookie(req.Context(), rw, req, "oauth2/cb", req.Form.Get("state"))
if err != nil {
err = fmt.Errorf("failed to decode oauth2 cb state: %w", err)
break
}
if sd == nil {
err = errors.New("state not found")
break
}
// Load authority with client_id in state.
authority, _ = i.authorities.Lookup(req.Context(), sd.Ref)
if authority == nil {
i.logger.WithField("client_id", sd.ClientID).Debugln("identifier failed to find authority in oauth2 cb")
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "unknown client_id")
break
}
if authority.AuthorityType != authorities.AuthorityTypeOIDC {
err = errors.New("unknown authority type")
break
}
// Check incoming state type.
var done bool
done, err = func() (bool, error) {
switch sd.Mode {
case StateModeEndSession:
// Special mode. When in end session, take value from state and
// redirect to it. This completes end session callback.
uri, _ := url.Parse(sd.RawQuery)
if uri == nil {
return false, konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "no uri in state")
}
if sd.State != "" {
query := uri.Query()
query.Set("state", sd.State)
uri.RawQuery = query.Encode()
}
utils.WriteRedirect(rw, http.StatusFound, uri, nil, false)
return true, nil
default:
// Continue further.
}
return false, nil
}()
if err != nil {
break
}
if done {
// Already done, nothing further so return.
return
}
if authority.ResponseType == oidc.ResponseTypeCode ||
authority.ResponseType == oidc.ResponseTypeCodeIDToken ||
authority.ResponseType == oidc.ResponseTypeCodeIDTokenToken {
// Exchange code for ID token.
md := authority.Metadata().(*oidc.WellKnown)
config := &oauth2.Config{
ClientID: authority.ClientID,
ClientSecret: authority.ClientSecret,
RedirectURL: i.oauth2CbEndpointURI.String(),
Endpoint: oauth2.Endpoint{
TokenURL: md.TokenEndpoint,
},
Scopes: authority.Scopes,
}
var httpClient *http.Client
if authority.Insecure {
httpClient = utils.InsecureHTTPClient
} else {
httpClient = utils.DefaultHTTPClient
}
t, exchangeErr := config.Exchange(
context.WithValue(req.Context(), oauth2.HTTPClient, httpClient),
req.Form.Get("code"),
oauth2.SetAuthURLParam("code_verifier",
sd.Extra["code_verifier"].(string)),
)
if exchangeErr != nil {
err = fmt.Errorf("failed to exchange code for token: %w", exchangeErr)
break
}
// Inject found data into request for later parse.
req.Form.Set("access_token", t.AccessToken)
req.Form.Set("token_type", t.TokenType)
req.Form.Set("refresh_token", t.RefreshToken)
if v, ok := t.Extra("expires_in").(string); ok {
req.Form.Set("expires_in", v)
}
if v, ok := t.Extra("id_token").(string); ok {
req.Form.Set("id_token", v)
}
// Fetch userinfo.
uiReq, requestErr := http.NewRequest(http.MethodGet, md.UserInfoEndpoint, http.NoBody)
if requestErr != nil {
err = fmt.Errorf("failed to create userinfo request: %w", requestErr)
break
}
t.SetAuthHeader(uiReq)
uiResp, responseErr := httpClient.Do(uiReq)
if responseErr != nil {
err = fmt.Errorf("failed to get userinfo: %w", responseErr)
break
}
// Decode userinfo as JSON, directly into the claims set.
if decodeErr := json.NewDecoder(uiResp.Body).Decode(&userInfoClaims); decodeErr != nil {
err = fmt.Errorf("failed to decode userinfo response: %w", decodeErr)
uiResp.Body.Close()
break
}
uiResp.Body.Close()
}
// Parse incoming state response.
var authenticationSuccess *payload.AuthenticationSuccess
if authenticationSuccessRaw, parseErr := authority.ParseStateResponse(req, sd.State, sd.Extra); parseErr == nil {
authenticationSuccess = authenticationSuccessRaw.(*payload.AuthenticationSuccess)
} else {
err = parseErr
break
}
// Parse and validate IDToken.
idToken, idTokenParseErr := jwt.ParseWithClaims(authenticationSuccess.IDToken, userInfoClaims, authority.JWTKeyfunc())
if idTokenParseErr != nil {
if authority.Insecure {
i.logger.WithField("client_id", sd.ClientID).WithError(idTokenParseErr).Warnln("identifier ignoring validation error for insecure authority")
} else {
i.logger.WithError(idTokenParseErr).Debugln("identifier failed to validate oauth2 cb id token")
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2ServerError, "authority response validation failed")
break
}
}
if claims, _ := idToken.Claims.(jwt.MapClaims); claims == nil {
err = errors.New("invalid id token claims")
break
}
// Lookup username and user.
un, extra, claimsErr := authority.IdentityClaimValue(idToken)
if claimsErr != nil {
i.logger.WithError(claimsErr).Debugln("identifier failed to get username from oauth2 cb id token claims")
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InsufficientScope, "identity claim not found")
break
}
username := &un
// TODO(longsleep): This flow currently does not provide a hello
// context, means that downwards a backend might fail to resolve the
// user when it requires additional information for multiple backend
// routing.
user, err = i.resolveUser(req.Context(), *username)
if err != nil {
i.logger.WithError(err).WithField("username", *username).Debugln("identifier failed to resolve oauth2 cb user with backend")
// TODO(longsleep): Break on validation error.
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2AccessDenied, "failed to resolve user")
break
}
if user == nil || user.Subject() == "" {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2AccessDenied, "no such user")
break
}
var logonRef string
if rawIDToken, ok := extra["RawIDToken"]; ok {
logonRef = rawIDToken.(string)
}
if logonRef != "" {
user.logonRef = &logonRef
}
// Get user meta data.
// TODO(longsleep): This is an additional request to the backend. This
// should be avoided. Best would be if the backend would return everything
// in one shot (TODO in core).
err = i.updateUser(req.Context(), user, authority)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to update user data in oauth2 cb request")
}
// Set logon time.
user.logonAt = time.Now()
err = i.SetUserToLogonCookie(req.Context(), rw, user)
if err != nil {
i.logger.WithError(err).Errorln("identifier failed to serialize logon ticket in oauth2 cb")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to serialize logon ticket")
return
}
break
}
if sd == nil {
i.logger.WithError(err).Debugln("identifier oauth2 cb without state")
i.ErrorPage(rw, http.StatusBadRequest, "", "state not found")
return
}
uri, _ := url.Parse(i.authorizationEndpointURI.String())
query, _ := url.ParseQuery(sd.RawQuery)
query.Del("flow")
query.Set("identifier", MustBeSignedIn)
if query.Get("prompt") == oidc.PromptSelectAccount {
// Remove select_acount prompt for our secondary indentifier, it was
// already processed by the external provider.
query.Del("prompt")
}
switch typedErr := err.(type) {
case nil:
// breaks
case *konnectoidc.OAuth2Error:
// Pass along OAuth2 error.
i.logger.WithFields(utils.ErrorAsFields(err)).Debugln("oauth2 cb error")
// NOTE(longsleep): Pass along error ID but not the description to avoid
// leaking potetially internal information to our RP.
query.Set("error", typedErr.ErrorID)
query.Set("error_description", "identifier failed to authenticate")
//breaks
default:
i.logger.WithError(err).Errorln("identifier failed to process oauth2 cb")
i.ErrorPage(rw, http.StatusInternalServerError, "", "oauth2 cb failed")
return
}
uri.RawQuery = query.Encode()
utils.WriteRedirect(rw, http.StatusFound, uri, nil, false)
}

View File

@@ -0,0 +1,108 @@
{
"name": "identifier",
"version": "1.0.0",
"private": true,
"homepage": ".",
"dependencies": {
"@fontsource/roboto": "^4.5.8",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@testing-library/dom": "^8.19.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"@types/jest": "^26.0.24",
"@types/node": "^12.20.55",
"@types/react": "^17.0.50",
"@types/react-dom": "^17.0.17",
"@types/react-redux": "^7.1.24",
"@types/redux-logger": "^3.0.9",
"axios": "^0.22.0",
"classnames": "^2.3.2",
"eslint": "^8.25.0",
"glob": "^8.0.3",
"i18next": "^21.9.1",
"i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4",
"i18next-resources-to-backend": "^1.0.0",
"query-string": "^7.1.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^11.18.4",
"react-redux": "^7.2.9",
"react-router": "^5.3.4",
"react-router-dom": "5.3.4",
"react-scripts": "5.0.1",
"redux": "^4.2.0",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.1",
"render-if": "^0.1.1",
"typescript": "^4.8.4",
"web-vitals": "^1.1.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint ./src/**/*.{tsx,ts,jsx,js}",
"licenses": "NODE_PATH=./node_modules node ../scripts/js-license-ranger.js",
"analyze": "source-map-explorer 'build/static/js/*.js'"
},
"devDependencies": {
"cldr": "^7.2.0",
"eslint-plugin-i18next": "^5.2.1",
"i18next-conv": "^12.1.1",
"i18next-parser": "^5.4.0",
"source-map-explorer": "^1.8.0"
},
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}"
]
},
"eslintConfig": {
"plugins": [
"i18next"
],
"extends": [
"react-app",
"react-app/jest",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:i18next/recommended"
],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error"
],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": [
"error"
],
"i18next/no-literal-string": [
"off",
{
"markupOnly": true
}
],
"react/prop-types": [
"warn"
]
}
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"packageManager": "yarn@3.2.2"
}

464
vendor/github.com/libregraph/lico/identifier/saml2.go generated vendored Normal file
View File

@@ -0,0 +1,464 @@
/*
* Copyright 2017-2020 Kopano and its licensors
*
* 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.
*
*/
package identifier
import (
"errors"
"fmt"
"net/http"
"net/url"
"time"
"github.com/crewjam/saml"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
"github.com/sirupsen/logrus"
"github.com/libregraph/lico/identity/authorities"
konnectoidc "github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/identity/authorities/samlext"
"github.com/libregraph/lico/utils"
)
func (i *Identifier) writeSAML2Start(rw http.ResponseWriter, req *http.Request, authority *authorities.Details) {
var err error
if authority == nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2TemporarilyUnavailable, "no authority")
} else if !authority.IsReady() {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2TemporarilyUnavailable, "authority not ready")
}
switch typedErr := err.(type) {
case nil:
// breaks
case *konnectoidc.OAuth2Error:
// Redirect back, with error.
i.logger.WithFields(utils.ErrorAsFields(err)).Debugln("saml2 start error")
// NOTE(longsleep): Pass along error ID but not the description to avoid
// leaking potentially internal information to our RP.
uri, _ := url.Parse(i.authorizationEndpointURI.String())
query, _ := url.ParseQuery(req.URL.RawQuery)
query.Del("flow")
query.Set("error", typedErr.ErrorID)
query.Set("error_description", "identifier failed to authenticate")
uri.RawQuery = query.Encode()
utils.WriteRedirect(rw, http.StatusFound, uri, nil, false)
return
default:
i.logger.WithError(err).Errorln("identifier failed to process saml2 start")
i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 start failed")
return
}
sd := &StateData{
State: rndm.GenerateRandomString(32),
RawQuery: req.URL.RawQuery,
Ref: authority.ID,
}
uri, extra, err := authority.MakeRedirectAuthenticationRequestURL(sd.State)
if err != nil {
i.logger.WithError(err).Errorln("identifier failed to create authentication request: %w", err)
i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 start failed")
return
}
sd.Extra = extra
// Set cookie which is consumed by the callback later.
err = i.SetStateToStateCookie(req.Context(), rw, "saml2/acs", sd)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to set saml2 state cookie")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to set cookie")
return
}
utils.WriteRedirect(rw, http.StatusFound, uri, nil, false)
}
func (i *Identifier) writeSAML2AssertionConsumerService(rw http.ResponseWriter, req *http.Request) {
var err error
var sd *StateData
var user *IdentifiedUser
var authority *authorities.Details
for {
sd, err = i.GetStateFromStateCookie(req.Context(), rw, req, "saml2/acs", req.Form.Get("RelayState"))
if err != nil {
err = fmt.Errorf("failed to decode saml2 acs state: %v", err)
break
}
if sd == nil {
err = errors.New("state not found")
break
}
// Load authority with client_id in state.
authority, _ = i.authorities.Lookup(req.Context(), sd.Ref)
if authority == nil {
i.logger.Debugln("identifier failed to find authority in saml2 acs")
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "unknown client_id")
break
}
if authority.AuthorityType != authorities.AuthorityTypeSAML2 {
err = errors.New("unknown authority type")
break
}
// Parse incoming state response.
var assertion *saml.Assertion
if assertionRaw, parseErr := authority.ParseStateResponse(req, sd.State, sd.Extra); parseErr == nil {
assertion = assertionRaw.(*saml.Assertion)
} else {
err = parseErr
break
}
// Lookup username and user.
un, claims, claimsErr := authority.IdentityClaimValue(assertion)
if claimsErr != nil {
i.logger.WithError(claimsErr).Debugln("identifier failed to get username from saml2 acs assertion")
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InsufficientScope, "identity claim not found")
break
}
username := &un
// TODO(longsleep): This flow currently does not provide a hello
// context, means that downwards a backend might fail to resolve the
// user when it requires additional information for multiple backend
// routing.
user, err = i.resolveUser(req.Context(), *username)
if err != nil {
i.logger.WithError(err).WithField("username", *username).Debugln("identifier failed to resolve saml2 acs user with backend")
// TODO(longsleep): Break on validation error.
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2AccessDenied, "failed to resolve user")
break
}
if user == nil || user.Subject() == "" {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2AccessDenied, "no such user")
break
}
// Apply additional authority claims.
if sessionNotOnOrAfter, ok := claims["SessionNotOnOrAfter"]; ok {
user.expiresAfter = sessionNotOnOrAfter.(*time.Time)
}
var logonRef string
if nameIDTransient, ok := claims["TransientNameID"]; ok {
logonRef = "transient:" + nameIDTransient.(string)
} else if nameIDPersistent, ok := claims["PersistentNameID"]; ok {
logonRef = "persistent:" + nameIDPersistent.(string)
} else if nameIDUnspecified, ok := claims["UnspecifiedNameID"]; ok {
logonRef = "unspecified:" + nameIDUnspecified.(string)
}
if logonRef != "" {
user.logonRef = &logonRef
}
if authority.Trusted {
// Use external authority session, if the external authority is trusted.
if sessionIndexString, ok := claims["SessionIndex"]; ok {
sessionIndex := sessionIndexString.(string)
user.sessionRef = &sessionIndex
}
}
// Get user meta data.
// TODO(longsleep): This is an additional request to the backend. This
// should be avoided. Best would be if the backend would return everything
// in one shot (TODO in core).
err = i.updateUser(req.Context(), user, authority)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to get user data in saml2 acs request")
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2AccessDenied, "failed to get user data")
break
}
// Set logon time.
user.logonAt = time.Now()
err = i.SetUserToLogonCookie(req.Context(), rw, user)
if err != nil {
i.logger.WithError(err).Errorln("identifier failed to serialize logon ticket in saml2 acs")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to serialize logon ticket")
return
}
break
}
if sd == nil {
i.logger.WithError(err).Debugln("identifier saml2 acs without state")
i.ErrorPage(rw, http.StatusBadRequest, "", "state not found")
return
}
uri, _ := url.Parse(i.authorizationEndpointURI.String())
query, _ := url.ParseQuery(sd.RawQuery)
query.Del("flow")
query.Set("identifier", MustBeSignedIn)
query.Set("prompt", oidc.PromptNone)
switch typedErr := err.(type) {
case nil:
// breaks
case *saml.InvalidResponseError:
i.logger.WithError(err).WithFields(logrus.Fields{
"reason": typedErr.PrivateErr,
}).Debugf("saml2 acs invalid response")
query.Set("error", oidc.ErrorCodeOAuth2AccessDenied)
query.Set("error_description", "identifier received invalid response")
// breaks
case *konnectoidc.OAuth2Error:
// Pass along OAuth2 error.
i.logger.WithFields(utils.ErrorAsFields(err)).Debugln("saml2 acs error")
// NOTE(longsleep): Pass along error ID but not the description to avoid
// leaking potetially internal information to our RP.
query.Set("error", typedErr.ErrorID)
query.Set("error_description", "identifier failed to authenticate")
//breaks
default:
i.logger.WithError(err).Errorln("identifier failed to process saml2 acs")
i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 acs failed")
return
}
uri.RawQuery = query.Encode()
utils.WriteRedirect(rw, http.StatusFound, uri, nil, false)
}
func (i *Identifier) writeSAMLSingleLogoutServiceRequest(rw http.ResponseWriter, req *http.Request) {
lor, err := samlext.NewIdpLogoutRequest(req)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to process saml2 slo request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to parse request")
return
}
err = lor.Validate()
if err != nil {
i.logger.WithError(err).Debugln("identifier saml2 slo request validation failed")
i.ErrorPage(rw, http.StatusBadRequest, "", "slo request validation failed")
return
}
// In http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf §3.4.5.2
// we get a description of the Destination attribute:
//
// If the message is signed, the Destination XML attribute in the root SAML
// element of the protocol message MUST contain the URL to which the sender
// has instructed the user agent to deliver the message. The recipient MUST
// then verify that the value matches the location at which the message has
// been received.
//
// We require the destination be correct either (a) if signing is enabled or
// (b) if it was provided.
mustHaveDestination := lor.SigAlg != nil
mustHaveDestination = mustHaveDestination || lor.Request.Destination != ""
if mustHaveDestination {
uri, _ := i.absoluteURLForRoute("saml2/slo")
if lor.Request.Destination != uri.String() {
i.logger.WithField("destination", lor.Request.Destination).Debugln("identifier saml2 slo request with wrong desitation")
i.ErrorPage(rw, http.StatusBadRequest, "", "slo request destination wrong")
return
}
}
// Find matching authority.
authority, found := i.authorities.Find(req.Context(), func(authority authorities.AuthorityRegistration) bool {
if authority.AuthorityType() != authorities.AuthorityTypeSAML2 {
return false
}
if lor.Request.Issuer.Value == authority.Issuer() {
return true
}
return false
})
if !found {
i.logger.WithField("issuer", lor.Request.Issuer.Value).Debugln("identifier saml2 slo request from unknown issuer")
i.ErrorPage(rw, http.StatusBadRequest, "", "slo request issuer unknown")
return
}
authorityDetails := authority.Authority()
if lor.SigAlg == nil {
// Never consider trusted if not signed.
authorityDetails.Trusted = false
}
if authorityDetails.AuthorityType != authorities.AuthorityTypeSAML2 {
i.logger.WithField("issuer", lor.Request.Issuer.Value).Debugln("identifier saml2 slo request for unknown authority type")
i.ErrorPage(rw, http.StatusBadRequest, "", "slo request issuer authority type unknown")
return
}
// Validate.
validated, err := authority.ValidateIdpEndSessionRequest(lor, lor.RelayState)
if err != nil {
i.logger.WithError(err).WithField("issuer", authority.Issuer()).Debugln("identifier saml2 slo request authority validation failed")
i.ErrorPage(rw, http.StatusBadRequest, "", "slo request authority validation failed")
return
}
if !validated && authorityDetails.Trusted {
// Never consider unvalidated logout requests as trusted.
authorityDetails.Trusted = false
}
user, _ := i.GetUserFromLogonCookie(req.Context(), req, 0, false)
if user != nil {
// Compare signed in SAML SessionIndex with the on provided in the LogoutRequest.
if user.SessionRef() != nil {
if lor.Request.SessionIndex == nil {
i.logger.Debugln("identifier saml2 slo request without session index")
i.ErrorPage(rw, http.StatusBadRequest, "", "slo request missing session index")
return
}
if lor.Request.SessionIndex.Value != *user.SessionRef() {
i.logger.Debugln("identifier saml2 slo request for other session index")
i.ErrorPage(rw, http.StatusBadRequest, "", "slo request session index mismatch")
return
}
}
if authorityDetails != nil && authorityDetails.Trusted {
// Directly clear identifier session when a trusted authority requests it.
err = i.UnsetLogonCookie(req.Context(), user, rw)
if err != nil {
i.logger.WithError(err).Errorln("identifier saml2 slo failed to unset logon cookie")
i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 slo logout failed")
return
}
}
} else {
// Ignore when not signed in, for end session.
}
if authorityDetails == nil || !authorityDetails.Trusted {
// Handle directly by redirecting to our logout confirm url for untrusted
// registies or when no URL was set.
uri, _ := i.absoluteURLForRoute("goodbye")
query := &url.Values{}
uri.RawQuery = query.Encode()
utils.WriteRedirect(rw, http.StatusFound, uri, nil, false)
return
}
uri, _, err := authorityDetails.MakeRedirectEndSessionResponseURL(lor.Request, lor.RelayState)
if err != nil {
i.logger.WithError(err).Errorln("failed to make saml2 slo redirect request url")
i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 slo failed")
return
}
if uri == nil {
i.logger.Warnln("saml2 slo reached dead end, no post logout redirect uri available")
// Fall back to logout confirm url.
uri, _ = i.absoluteURLForRoute("goodbye")
}
utils.WriteRedirect(rw, http.StatusFound, uri, nil, false)
}
func (i *Identifier) writeSAMLSingleLogoutServiceResponse(rw http.ResponseWriter, req *http.Request) {
lor, err := samlext.NewIdpLogoutResponse(req)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to process saml2 slo response")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to parse response")
return
}
err = lor.Validate()
if err != nil {
i.logger.WithError(err).Debugln("identifier saml2 slo response validation failed")
i.ErrorPage(rw, http.StatusBadRequest, "", "response validation failed")
return
}
sd, err := i.GetStateFromStateCookie(req.Context(), rw, req, "_/saml2/slo", lor.RelayState)
if err != nil {
i.logger.WithError(err).Debugln("identifier saml2 slo response failed to load state")
i.ErrorPage(rw, http.StatusBadRequest, "", "response state invalid")
return
}
if sd == nil {
i.logger.WithError(err).Debugln("identifier saml2 slo response failed as state is missing")
i.ErrorPage(rw, http.StatusBadRequest, "", "response state missing")
return
}
authority, found := i.authorities.Get(req.Context(), sd.Ref)
if !found {
i.ErrorPage(rw, http.StatusBadRequest, "", "no authority")
return
}
authorityDetails := authority.Authority()
if lor.SigAlg == nil {
// Never consider trusted if not signed.
authorityDetails.Trusted = false
}
if authorityDetails.AuthorityType != authorities.AuthorityTypeSAML2 {
i.logger.WithField("issuer", authority.Issuer()).Debugln("identifier saml2 slo response for unknown authority type")
i.ErrorPage(rw, http.StatusBadRequest, "", "slo response issuer authority type unknown")
return
}
// Validate.
validated, err := authority.ValidateIdpEndSessionResponse(lor, lor.RelayState)
if err != nil {
i.logger.WithError(err).WithField("issuer", authority.Issuer()).Debugln("identifier saml2 slo response authority validation failed")
i.ErrorPage(rw, http.StatusBadRequest, "", "slo response authority validation failed")
return
}
if !validated && authorityDetails.Trusted {
// Never consider unvalidated logout responses as trusted.
authorityDetails.Trusted = false
}
if lor.Response.Status.StatusCode.Value != saml.StatusSuccess {
i.logger.WithField("status", lor.Response.Status.StatusCode).Debugln("saml2 slo response without success status")
}
// Extract destination URI from state data (its put into the RawQuery field).
uri, err := url.Parse(sd.RawQuery)
if err != nil {
i.logger.WithError(err).Errorln("failed to parse slo response redirect url from state data")
i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 slo response failed")
return
}
if uri == nil || uri.String() == "" {
i.logger.Warnln("saml2 slo reached dead end, no post logout redirect uri available")
// Fall back to our signed out url or goodbye route.
if i.Config.SignedOutEndpointURI != nil {
uri = i.Config.SignedOutEndpointURI
} else {
uri, _ = i.absoluteURLForRoute("goodbye")
}
}
if sd.State != "" {
query := uri.Query()
query.Set("state", sd.State)
uri.RawQuery = query.Encode()
}
utils.WriteRedirect(rw, http.StatusFound, uri, nil, false)
}

Some files were not shown because too many files have changed in this diff Show More