mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-06 04:09:40 -06:00
switch to go vendoring
This commit is contained in:
12
vendor/github.com/libregraph/idm/.editorconfig
generated
vendored
Normal file
12
vendor/github.com/libregraph/idm/.editorconfig
generated
vendored
Normal 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
8
vendor/github.com/libregraph/idm/.gitignore
generated
vendored
Normal 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
77
vendor/github.com/libregraph/idm/.golangci.yaml
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
run:
|
||||
modules-download-mode: vendor
|
||||
issues-exit-code: 0
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
settings:
|
||||
printf:
|
||||
funcs:
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
|
||||
gocyclo:
|
||||
min-complexity: 10
|
||||
maligned:
|
||||
suggest-new: true
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 140
|
||||
goimports:
|
||||
local-prefixes: github.com/libregraph/idm
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
revive:
|
||||
min-confidence: 0
|
||||
|
||||
linters:
|
||||
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- dupl
|
||||
- errcheck
|
||||
- exportloopref
|
||||
- funlen
|
||||
- gochecknoinits
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- revive
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
|
||||
# don't enable:
|
||||
# - depguard - until https://github.com/OpenPeeDeeP/depguard/issues/7 gets fixed
|
||||
# - maligned,prealloc
|
||||
# - gochecknoglobals
|
||||
|
||||
severity:
|
||||
default-severity: warning
|
||||
6
vendor/github.com/libregraph/idm/.license-ranger.json
generated
vendored
Normal file
6
vendor/github.com/libregraph/idm/.license-ranger.json
generated
vendored
Normal 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
14
vendor/github.com/libregraph/idm/AUTHORS
generated
vendored
Normal 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
210
vendor/github.com/libregraph/idm/CHANGELOG.md
generated
vendored
Normal 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
18
vendor/github.com/libregraph/idm/CONTRIBUTORS
generated
vendored
Normal 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
33
vendor/github.com/libregraph/idm/Dockerfile.build
generated
vendored
Normal 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
202
vendor/github.com/libregraph/idm/LICENSE.txt
generated
vendored
Normal 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
158
vendor/github.com/libregraph/idm/Makefile
generated
vendored
Normal 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
100
vendor/github.com/libregraph/idm/README.md
generated
vendored
Normal 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
12
vendor/github.com/libregraph/idm/defaults.go
generated
vendored
Normal 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
6
vendor/github.com/libregraph/idm/doc.go
generated
vendored
Normal 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
94
vendor/github.com/libregraph/idm/pkg/ldapdn/ldapdn.go
generated
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
package ldapdn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/text/cases"
|
||||
)
|
||||
|
||||
// Normalize takes an ldap.DN struct and turns it into a "normalized" DN string
|
||||
// by cases folding all RDN (attributetypes and values). Note: This currently
|
||||
// handles all attributes as caseIgnoreStrings ignoring the Syntax the Attribute
|
||||
// Type might have assigned.
|
||||
func Normalize(dn *ldap.DN) string {
|
||||
var nDN string
|
||||
caseFold := cases.Fold()
|
||||
for r, rdn := range dn.RDNs {
|
||||
// FIXME to really normalize multivalued RDNs we'd need
|
||||
// to normalize the order of Attributes here as well
|
||||
for a, ava := range rdn.Attributes {
|
||||
if a > 0 {
|
||||
// This is a multivalued RDN.
|
||||
nDN += "+"
|
||||
} else if r > 0 {
|
||||
nDN += ","
|
||||
}
|
||||
nDN = nDN + caseFold.String(ava.Type) + "=" + encodeRDNValue(caseFold.String(ava.Value))
|
||||
}
|
||||
}
|
||||
return nDN
|
||||
}
|
||||
|
||||
// encodeRDNValue applies the DN escaping rules (RFC4514) to the supplied
|
||||
// string (the value part of an RDN). Returns the escaped string.
|
||||
// Note: This function is taken from https://github.com/go-ldap/ldap/pull/104
|
||||
func encodeRDNValue(rDNValue string) string {
|
||||
encodedBuf := bytes.Buffer{}
|
||||
|
||||
escapeChar := func(c byte) {
|
||||
encodedBuf.WriteByte('\\')
|
||||
encodedBuf.WriteByte(c)
|
||||
}
|
||||
|
||||
escapeHex := func(c byte) {
|
||||
encodedBuf.WriteByte('\\')
|
||||
encodedBuf.WriteString(hex.EncodeToString([]byte{c}))
|
||||
}
|
||||
|
||||
for i := 0; i < len(rDNValue); i++ {
|
||||
char := rDNValue[i]
|
||||
if i == 0 && char == ' ' || char == '#' {
|
||||
// Special case leading space or number sign.
|
||||
escapeChar(char)
|
||||
continue
|
||||
}
|
||||
if i == len(rDNValue)-1 && char == ' ' {
|
||||
// Special case trailing space.
|
||||
escapeChar(char)
|
||||
continue
|
||||
}
|
||||
|
||||
switch char {
|
||||
case '"', '+', ',', ';', '<', '>', '\\':
|
||||
// Each of these special characters must be escaped.
|
||||
escapeChar(char)
|
||||
continue
|
||||
}
|
||||
|
||||
if char < ' ' || char > '~' {
|
||||
// All special character escapes are handled first
|
||||
// above. All bytes less than ASCII SPACE and all bytes
|
||||
// greater than ASCII TILDE must be hex-escaped.
|
||||
escapeHex(char)
|
||||
continue
|
||||
}
|
||||
|
||||
// Any other character does not require escaping.
|
||||
encodedBuf.WriteByte(char)
|
||||
}
|
||||
|
||||
return encodedBuf.String()
|
||||
}
|
||||
|
||||
// ParseNormalize normalizes the passed LDAP DN string by first parsing it (using ldap.ParseDN)
|
||||
// and then casefolding all RDN using ldapdn.Normalize(). ParseNormalize will return an error
|
||||
// when parsing the DN fails.
|
||||
func ParseNormalize(dn string) (string, error) {
|
||||
parsed, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return Normalize(parsed), nil
|
||||
}
|
||||
144
vendor/github.com/libregraph/idm/pkg/ldapentry/ldapentry.go
generated
vendored
Normal file
144
vendor/github.com/libregraph/idm/pkg/ldapentry/ldapentry.go
generated
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
package ldapentry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/text/cases"
|
||||
)
|
||||
|
||||
func ApplyModify(old *ldap.Entry, mod *ldap.ModifyRequest) (newEntry *ldap.Entry, err error) {
|
||||
oldDN, err := ldap.ParseDN(old.DN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rdn := oldDN.RDNs[0]
|
||||
modDN, err := ldap.ParseDN(mod.DN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// This shouldn't happen if we ge here (TM)
|
||||
if !oldDN.EqualFold(modDN) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("DNs do not match"))
|
||||
}
|
||||
|
||||
casefold := cases.Fold()
|
||||
newEntry = ldap.NewEntry(old.DN, map[string][]string{})
|
||||
newEntry.Attributes = old.Attributes
|
||||
for _, c := range mod.Changes {
|
||||
nType := casefold.String(c.Modification.Type)
|
||||
switch c.Operation {
|
||||
case ldap.AddAttribute:
|
||||
newValues := entryApplyModAdd(newEntry.GetEqualFoldAttributeValues(nType), c.Modification.Vals)
|
||||
newEntry.Attributes = entryReplaceValues(newEntry.Attributes, c.Modification.Type, newValues)
|
||||
|
||||
case ldap.ReplaceAttribute:
|
||||
// Modifies on RDN attributes need special care to make sure that the rdn Value is not removed
|
||||
for _, rdnAttr := range rdn.Attributes {
|
||||
if nType == casefold.String(rdnAttr.Type) {
|
||||
rdnPresent := false
|
||||
nRdnVal := casefold.String(rdnAttr.Value)
|
||||
for _, newVal := range c.Modification.Vals {
|
||||
if nRdnVal == casefold.String(newVal) {
|
||||
rdnPresent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !rdnPresent {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotAllowedOnRDN, errors.New(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
newEntry.Attributes = entryReplaceValues(newEntry.Attributes, c.Modification.Type, c.Modification.Vals)
|
||||
case ldap.DeleteAttribute:
|
||||
for _, rdnAttr := range rdn.Attributes {
|
||||
// Modifies on RDN attributes need special care
|
||||
if nType == casefold.String(rdnAttr.Type) {
|
||||
if len(c.Modification.Vals) == 0 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotAllowedOnRDN, errors.New(""))
|
||||
}
|
||||
nRdnVal := casefold.String(rdnAttr.Value)
|
||||
for _, delVal := range c.Modification.Vals {
|
||||
if nRdnVal == casefold.String(delVal) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotAllowedOnRDN, errors.New(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
newValues := entryApplyModDelete(old.GetEqualFoldAttributeValues(nType), c.Modification.Vals)
|
||||
newEntry.Attributes = entryReplaceValues(newEntry.Attributes, c.Modification.Type, newValues)
|
||||
}
|
||||
}
|
||||
return newEntry, nil
|
||||
}
|
||||
|
||||
func entryReplaceValues(ea []*ldap.EntryAttribute, attrType string, newValues []string) (updatedAttrs []*ldap.EntryAttribute) {
|
||||
casefold := cases.Fold()
|
||||
nType := casefold.String(attrType)
|
||||
updated := false
|
||||
for _, attr := range ea {
|
||||
if casefold.String(attr.Name) == nType {
|
||||
updated = true
|
||||
if len(newValues) == 0 {
|
||||
continue
|
||||
}
|
||||
updatedAttrs = append(updatedAttrs, ldap.NewEntryAttribute(attr.Name, newValues))
|
||||
} else {
|
||||
updatedAttrs = append(updatedAttrs, attr)
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
if len(newValues) != 0 {
|
||||
updatedAttrs = append(updatedAttrs, ldap.NewEntryAttribute(attrType, newValues))
|
||||
}
|
||||
}
|
||||
return updatedAttrs
|
||||
}
|
||||
|
||||
func entryApplyModAdd(curVals, addVals []string) (newVals []string) {
|
||||
newVals = curVals
|
||||
casefold := cases.Fold()
|
||||
for _, newVal := range addVals {
|
||||
present := false
|
||||
for _, val := range curVals {
|
||||
if casefold.String(newVal) == casefold.String(val) {
|
||||
present = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !present {
|
||||
newVals = append(newVals, newVal)
|
||||
}
|
||||
}
|
||||
return newVals
|
||||
}
|
||||
|
||||
func entryApplyModDelete(curVals, delVals []string) (newVals []string) {
|
||||
casefold := cases.Fold()
|
||||
if len(delVals) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
for _, curVal := range curVals {
|
||||
nCurVal := casefold.String(curVal)
|
||||
keep := true
|
||||
for _, del := range delVals {
|
||||
if nCurVal == casefold.String(del) {
|
||||
keep = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if keep {
|
||||
newVals = append(newVals, curVal)
|
||||
}
|
||||
}
|
||||
return newVals
|
||||
}
|
||||
|
||||
func EntryFromAddRequest(add *ldap.AddRequest) *ldap.Entry {
|
||||
attrs := map[string][]string{}
|
||||
|
||||
for _, a := range add.Attributes {
|
||||
attrs[a.Type] = a.Vals
|
||||
}
|
||||
return ldap.NewEntry(add.DN, attrs)
|
||||
}
|
||||
16
vendor/github.com/libregraph/idm/pkg/ldappassword/crypt.go
generated
vendored
Normal file
16
vendor/github.com/libregraph/idm/pkg/ldappassword/crypt.go
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
//go:build !disable_crypt && (linux || freebsd || netbsd)
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldappassword
|
||||
|
||||
import (
|
||||
gocrypt "github.com/amoghe/go-crypt"
|
||||
)
|
||||
|
||||
func crypt(pass, salt string) (string, error) {
|
||||
return gocrypt.Crypt(pass, salt)
|
||||
}
|
||||
16
vendor/github.com/libregraph/idm/pkg/ldappassword/crypt_disabled.go
generated
vendored
Normal file
16
vendor/github.com/libregraph/idm/pkg/ldappassword/crypt_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
//go:build disable_crypt || darwin || windows
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldappassword
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func crypt(pass, salt string) (string, error) {
|
||||
return "", errors.New("CRYPT unsupported on this platform")
|
||||
}
|
||||
133
vendor/github.com/libregraph/idm/pkg/ldappassword/ldappassword.go
generated
vendored
Normal file
133
vendor/github.com/libregraph/idm/pkg/ldappassword/ldappassword.go
generated
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldappassword
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha1" //nolint,gosec
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"github.com/trustelem/zxcvbn"
|
||||
)
|
||||
|
||||
var Argon2DefaultParams = argon2id.DefaultParams
|
||||
|
||||
func Validate(password string, hash string) (bool, error) {
|
||||
algorithm := ""
|
||||
if hash[0] == '{' {
|
||||
algorithmEnd := strings.Index(hash[0:], "}")
|
||||
if algorithmEnd >= 1 {
|
||||
algorithm = hash[0 : algorithmEnd+1]
|
||||
hash = hash[algorithmEnd+1:]
|
||||
}
|
||||
}
|
||||
|
||||
hashBytes := []byte(hash)
|
||||
var passwordBytes []byte
|
||||
|
||||
switch strings.ToUpper(algorithm) {
|
||||
case "":
|
||||
// No password scheme, direct comparison.
|
||||
passwordBytes = []byte(password)
|
||||
|
||||
case "{ARGON2}":
|
||||
// Follows the format used by the Argon2 reference C implementation and looks like this:
|
||||
// $argon2id$v=19$m=65536,t=3,p=2$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
|
||||
match, err := argon2id.ComparePasswordAndHash(password, hash)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("argon2 error: %w", err)
|
||||
}
|
||||
if !match {
|
||||
return false, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
return true, nil
|
||||
|
||||
case "{CRYPT}":
|
||||
// By default the salt is a two character string.
|
||||
salt := hash[:2]
|
||||
if hash[0] == '$' {
|
||||
// In the glibc2 version, salt format for additional encryption
|
||||
// $id$salt$encrypted.
|
||||
hashParts := strings.SplitN(hash, "$", 4)
|
||||
if len(hashParts) == 4 {
|
||||
salt = strings.Join(hashParts[:4], "$")
|
||||
}
|
||||
}
|
||||
encrypted, err := crypt(password, salt)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("crypt error: %w", err)
|
||||
}
|
||||
passwordBytes = []byte(encrypted)
|
||||
|
||||
case "{SSHA}":
|
||||
// BASE64(SHA-1(clear_text + salt) + salt)
|
||||
// The salt is 4 bytes long.
|
||||
decodedBytes, err := base64.StdEncoding.DecodeString(hash)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("ssha error: %w", err)
|
||||
}
|
||||
salt := decodedBytes[len(decodedBytes)-4:]
|
||||
h := sha1.New() //nolint,gosec
|
||||
h.Write([]byte(password))
|
||||
h.Write(salt)
|
||||
passwordBytes = h.Sum(nil)
|
||||
passwordBytes = append(passwordBytes, salt...)
|
||||
hashBytes = decodedBytes
|
||||
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported password algorithm: %s", algorithm)
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(hashBytes, passwordBytes) != 1 {
|
||||
return false, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func Hash(password string, algorithm string) (string, error) {
|
||||
var result string
|
||||
|
||||
switch algorithm {
|
||||
case "", "{CLEARTEXT}":
|
||||
result = password
|
||||
|
||||
case "{ARGON2}":
|
||||
hash, hashErr := argon2id.CreateHash(password, Argon2DefaultParams)
|
||||
if hashErr != nil {
|
||||
return "", fmt.Errorf("password hash error: %w", hashErr)
|
||||
}
|
||||
result = "{ARGON2}" + hash
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("password hash alg not supported: %s", algorithm)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func EstimatePasswordStrength(password string, userInputs []string) int {
|
||||
result := zxcvbn.PasswordStrength(password, userInputs)
|
||||
return result.Score
|
||||
}
|
||||
|
||||
func GenerateRandomPassword(length int) (string, error) {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-=+!@#$%^&*."
|
||||
ret := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ret[i] = chars[num.Int64()]
|
||||
}
|
||||
|
||||
return string(ret), nil
|
||||
}
|
||||
28
vendor/github.com/libregraph/idm/pkg/ldapserver/LICENSE
generated
vendored
Normal file
28
vendor/github.com/libregraph/idm/pkg/ldapserver/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
Copyright (c) 2021 The LibreGraph Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
21
vendor/github.com/libregraph/idm/pkg/ldapserver/README.md
generated
vendored
Normal file
21
vendor/github.com/libregraph/idm/pkg/ldapserver/README.md
generated
vendored
Normal 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
127
vendor/github.com/libregraph/idm/pkg/ldapserver/add.go
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleAddRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
|
||||
if boundDN == "" {
|
||||
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
|
||||
}
|
||||
addReq, err := parseAddRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fnNames := []string{}
|
||||
for k := range server.AddFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(addReq.DN, fnNames)
|
||||
var adder Adder
|
||||
if adder = server.AddFns[fn]; adder == nil {
|
||||
if fn == "" {
|
||||
err = fmt.Errorf("no suitable handler found for dn: '%s'", addReq.DN)
|
||||
} else {
|
||||
err = fmt.Errorf("handler '%s' does not support add", fn)
|
||||
}
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
|
||||
}
|
||||
code, err := adder.Add(boundDN, addReq, conn)
|
||||
return ldap.NewError(uint16(code), err)
|
||||
}
|
||||
|
||||
func parseAddRequest(req *ber.Packet) (*ldap.AddRequest, error) {
|
||||
addReq := ldap.AddRequest{}
|
||||
// LDAP Add request have 2 Elements (DN and AttributeList)
|
||||
if len(req.Children) != 2 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid add request"))
|
||||
}
|
||||
|
||||
dn, ok := req.Children[0].Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry DN"))
|
||||
}
|
||||
|
||||
_, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
|
||||
}
|
||||
addReq.DN = dn
|
||||
|
||||
al, err := parseAttributeList(req.Children[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addReq.Attributes = al
|
||||
|
||||
return &addReq, nil
|
||||
}
|
||||
|
||||
func parseAttributeList(req *ber.Packet) ([]ldap.Attribute, error) {
|
||||
ldapAttrs := []ldap.Attribute{}
|
||||
|
||||
if req.ClassType != ber.ClassUniversal || req.TagType != ber.TypeConstructed || req.Tag != ber.TagSequence {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute List"))
|
||||
}
|
||||
|
||||
for _, a := range req.Children {
|
||||
attr, err := parseAttribute(a, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ldapAttrs = append(ldapAttrs, *attr)
|
||||
|
||||
}
|
||||
return ldapAttrs, nil
|
||||
}
|
||||
|
||||
func parseAttribute(attr *ber.Packet, partial bool) (*ldap.Attribute, error) {
|
||||
var la ldap.Attribute
|
||||
var ok bool
|
||||
var err error
|
||||
|
||||
// Partial attributes, might just contain a type and allow the Value to be absent
|
||||
if partial && (len(attr.Children) < 1 || len(attr.Children) > 2) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding partial Attribute"))
|
||||
} else if !partial && len(attr.Children) != 2 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute"))
|
||||
}
|
||||
|
||||
ad := attr.Children[0]
|
||||
if ad.ClassType != ber.ClassUniversal || ad.TagType != ber.TypePrimitive || ad.Tag != ber.TagOctetString {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Description"))
|
||||
}
|
||||
la.Type, ok = ad.Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Description"))
|
||||
}
|
||||
|
||||
// We can return here if this is a Partial Attribute without values
|
||||
if partial && len(attr.Children) == 1 {
|
||||
return &la, nil
|
||||
}
|
||||
if la.Vals, err = parseAttributeValues(attr.Children[1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &la, nil
|
||||
}
|
||||
|
||||
func parseAttributeValues(values *ber.Packet) ([]string, error) {
|
||||
var strVals []string
|
||||
if values.ClassType != ber.ClassUniversal || values.TagType != ber.TypeConstructed || values.Tag != ber.TagSet {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Values"))
|
||||
}
|
||||
for _, value := range values.Children {
|
||||
strVal, ok := value.Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Value"))
|
||||
}
|
||||
strVals = append(strVals, strVal)
|
||||
}
|
||||
return strVals, nil
|
||||
}
|
||||
79
vendor/github.com/libregraph/idm/pkg/ldapserver/bind.go
generated
vendored
Normal file
79
vendor/github.com/libregraph/idm/pkg/ldapserver/bind.go
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleBindRequest(req *ber.Packet, fns map[string]Binder, conn net.Conn) (resultCode LDAPResultCode) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
}
|
||||
}()
|
||||
|
||||
// we only support ldapv3
|
||||
ldapVersion, ok := req.Children[0].Value.(int64)
|
||||
if !ok {
|
||||
return ldap.LDAPResultProtocolError
|
||||
}
|
||||
if ldapVersion != 3 {
|
||||
logger.V(1).Info("Unsupported LDAP version", "version", ldapVersion)
|
||||
return ldap.LDAPResultInappropriateAuthentication
|
||||
}
|
||||
|
||||
// auth types
|
||||
bindDN, ok := req.Children[1].Value.(string)
|
||||
if !ok {
|
||||
return ldap.LDAPResultProtocolError
|
||||
}
|
||||
bindAuth := req.Children[2]
|
||||
switch bindAuth.Tag {
|
||||
default:
|
||||
logger.V(1).Info("Unknown LDAP authentication method", "tag", bindAuth.Tag)
|
||||
return ldap.LDAPResultInappropriateAuthentication
|
||||
case LDAPBindAuthSimple:
|
||||
if len(req.Children) == 3 {
|
||||
fnNames := []string{}
|
||||
for k := range fns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(bindDN, fnNames)
|
||||
resultCode, err := fns[fn].Bind(bindDN, bindAuth.Data.String(), conn)
|
||||
if err != nil {
|
||||
logger.Error(err, "BindFn Error")
|
||||
return ldap.LDAPResultOperationsError
|
||||
}
|
||||
return resultCode
|
||||
} else {
|
||||
logger.V(1).Info("Simple bind request has wrong # children. len(req.Children) != 3")
|
||||
return ldap.LDAPResultInappropriateAuthentication
|
||||
}
|
||||
case LDAPBindAuthSASL:
|
||||
logger.V(1).Info("SASL authentication is not supported")
|
||||
return ldap.LDAPResultInappropriateAuthentication
|
||||
}
|
||||
return ldap.LDAPResultOperationsError
|
||||
}
|
||||
|
||||
func encodeBindResponse(messageID int64, ldapResultCode LDAPResultCode) *ber.Packet {
|
||||
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
|
||||
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
|
||||
|
||||
bindReponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ldap.ApplicationBindResponse, nil, "Bind Response")
|
||||
bindReponse.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: "))
|
||||
bindReponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
|
||||
bindReponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "errorMessage: "))
|
||||
|
||||
responsePacket.AppendChild(bindReponse)
|
||||
|
||||
// ber.PrintPacket(responsePacket)
|
||||
return responsePacket
|
||||
}
|
||||
54
vendor/github.com/libregraph/idm/pkg/ldapserver/delete.go
generated
vendored
Normal file
54
vendor/github.com/libregraph/idm/pkg/ldapserver/delete.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleDeleteRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
|
||||
if boundDN == "" {
|
||||
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
|
||||
}
|
||||
delReq, err := parseDeleteRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fnNames := []string{}
|
||||
for k := range server.DeleteFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(delReq.DN, fnNames)
|
||||
var del Deleter
|
||||
if del = server.DeleteFns[fn]; del == nil {
|
||||
if fn == "" {
|
||||
err = fmt.Errorf("no suitable handler found for dn: '%s'", delReq.DN)
|
||||
} else {
|
||||
err = fmt.Errorf("handler '%s' does not support add", fn)
|
||||
}
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
|
||||
}
|
||||
code, err := del.Delete(boundDN, delReq, conn)
|
||||
return ldap.NewError(uint16(code), err)
|
||||
}
|
||||
|
||||
func parseDeleteRequest(req *ber.Packet) (*ldap.DelRequest, error) {
|
||||
delReq := ldap.DelRequest{}
|
||||
// LDAP Delete requests contain just the DN (no Sequence, or set)
|
||||
// i.e. they have no childre
|
||||
if len(req.Children) != 0 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid delete request"))
|
||||
}
|
||||
dn := req.Data.String()
|
||||
|
||||
_, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
|
||||
}
|
||||
delReq.DN = dn
|
||||
|
||||
return &delReq, nil
|
||||
}
|
||||
90
vendor/github.com/libregraph/idm/pkg/ldapserver/extended.go
generated
vendored
Normal file
90
vendor/github.com/libregraph/idm/pkg/ldapserver/extended.go
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type ExopHandler func(req *ber.Packet, boundDN string, server *Server, conn net.Conn) (*ber.Packet, error)
|
||||
|
||||
type ExtendedRequest struct {
|
||||
OID string
|
||||
Body *ber.Packet
|
||||
}
|
||||
|
||||
var exopRegistry = map[string]ExopHandler{}
|
||||
|
||||
func RegisterExtendedOperation(oid string, handler ExopHandler) {
|
||||
exopRegistry[oid] = handler
|
||||
}
|
||||
|
||||
func HandleExtendedRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) (*ber.Packet, error) {
|
||||
extReq, err := parseExtendedRequest(req)
|
||||
if err != nil {
|
||||
logger.V(1).Info("parsing extened request failed", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
if handler, ok := exopRegistry[extReq.OID]; ok {
|
||||
innerBer, err := handler(extReq.Body, boundDN, server, conn)
|
||||
var resCode LDAPResultCode = ldap.LDAPResultSuccess
|
||||
msg := ""
|
||||
if err != nil {
|
||||
if lerr, ok := err.(*ldap.Error); ok {
|
||||
msg = lerr.Err.Error()
|
||||
resCode = LDAPResultCode(lerr.ResultCode)
|
||||
} else {
|
||||
msg = err.Error()
|
||||
resCode = ldap.LDAPResultOther
|
||||
}
|
||||
}
|
||||
return encodeExtendedResponse(resCode, msg, extReq.OID, innerBer), nil
|
||||
} else {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Unsupported Extented Operations"))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func parseExtendedRequest(req *ber.Packet) (*ExtendedRequest, error) {
|
||||
// RFC 4511 Extended Operation:
|
||||
// ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
|
||||
// requestName [0] LDAPOID,
|
||||
// requestValue [1] OCTET STRING OPTIONAL }
|
||||
extReq := ExtendedRequest{}
|
||||
if len(req.Children) < 1 || len(req.Children) > 2 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid extented request"))
|
||||
}
|
||||
|
||||
if req.Children[0].Identifier.ClassType != ber.ClassContext || req.Children[0].Identifier.Tag != 0 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("error decoding extented request OID"))
|
||||
}
|
||||
|
||||
extReq.OID = req.Children[0].Data.String()
|
||||
if len(req.Children) == 2 {
|
||||
if req.Children[1].Identifier.ClassType != ber.ClassContext || req.Children[1].Identifier.Tag != 1 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("error decoding extented request OID"))
|
||||
}
|
||||
extReq.Body = req.Children[1]
|
||||
}
|
||||
|
||||
logger.V(1).Info("Extended Request", "oid", extReq.OID)
|
||||
return &extReq, nil
|
||||
}
|
||||
|
||||
func encodeExtendedResponse(rescode LDAPResultCode, msg, oid string, responseValue *ber.Packet) *ber.Packet {
|
||||
respBer := ber.Encode(ber.ClassApplication, ber.TypeConstructed,
|
||||
ber.Tag(ldap.ApplicationExtendedResponse), nil,
|
||||
ldap.ApplicationMap[ldap.ApplicationExtendedResponse])
|
||||
respBer.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(rescode), "resultCode: "))
|
||||
respBer.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
|
||||
respBer.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, msg, "errorMessage: "))
|
||||
respBer.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 10, oid, "responseName"))
|
||||
if responseValue != nil {
|
||||
encValue := ber.Encode(ber.ClassContext, ber.TypePrimitive, 11, nil, "responseValue")
|
||||
encValue.AppendChild(responseValue)
|
||||
respBer.AppendChild(encValue)
|
||||
}
|
||||
return respBer
|
||||
}
|
||||
180
vendor/github.com/libregraph/idm/pkg/ldapserver/filter.go
generated
vendored
Normal file
180
vendor/github.com/libregraph/idm/pkg/ldapserver/filter.go
generated
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/text/cases"
|
||||
)
|
||||
|
||||
const (
|
||||
FilterAnd = ldap.FilterAnd
|
||||
FilterOr = ldap.FilterOr
|
||||
FilterNot = ldap.FilterNot
|
||||
FilterEqualityMatch = ldap.FilterEqualityMatch
|
||||
FilterSubstrings = ldap.FilterSubstrings
|
||||
FilterGreaterOrEqual = ldap.FilterGreaterOrEqual
|
||||
FilterLessOrEqual = ldap.FilterLessOrEqual
|
||||
FilterPresent = ldap.FilterPresent
|
||||
FilterApproxMatch = ldap.FilterApproxMatch
|
||||
FilterExtensibleMatch = ldap.FilterExtensibleMatch
|
||||
)
|
||||
|
||||
var (
|
||||
FilterMap = ldap.FilterMap
|
||||
casefold = cases.Fold()
|
||||
)
|
||||
|
||||
const (
|
||||
FilterSubstringsInitial = ldap.FilterSubstringsInitial
|
||||
FilterSubstringsAny = ldap.FilterSubstringsAny
|
||||
FilterSubstringsFinal = ldap.FilterSubstringsFinal
|
||||
)
|
||||
|
||||
func CompileFilter(filter string) (*ber.Packet, error) {
|
||||
return ldap.CompileFilter(filter)
|
||||
}
|
||||
|
||||
func DecompileFilter(packet *ber.Packet) (ret string, err error) {
|
||||
return ldap.DecompileFilter(packet)
|
||||
}
|
||||
|
||||
func ServerApplyFilter(f *ber.Packet, entry *ldap.Entry) (bool, LDAPResultCode) {
|
||||
switch FilterMap[uint64(f.Tag)] {
|
||||
default:
|
||||
//log.Fatalf("Unknown LDAP filter code: %d", f.Tag)
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
case "Equality Match":
|
||||
if len(f.Children) != 2 {
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
attribute := f.Children[0].Value.(string)
|
||||
value := f.Children[1].Value.(string)
|
||||
for _, a := range entry.Attributes {
|
||||
if strings.EqualFold(a.Name, attribute) {
|
||||
for _, v := range a.Values {
|
||||
if strings.EqualFold(v, value) {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "Present":
|
||||
for _, a := range entry.Attributes {
|
||||
if strings.EqualFold(a.Name, f.Data.String()) {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
}
|
||||
case "And":
|
||||
for _, child := range f.Children {
|
||||
ok, exitCode := ServerApplyFilter(child, entry)
|
||||
if exitCode != ldap.LDAPResultSuccess {
|
||||
return false, exitCode
|
||||
}
|
||||
if !ok {
|
||||
return false, ldap.LDAPResultSuccess
|
||||
}
|
||||
}
|
||||
return true, ldap.LDAPResultSuccess
|
||||
case "Or":
|
||||
anyOk := false
|
||||
for _, child := range f.Children {
|
||||
ok, exitCode := ServerApplyFilter(child, entry)
|
||||
if exitCode != ldap.LDAPResultSuccess {
|
||||
return false, exitCode
|
||||
} else if ok {
|
||||
anyOk = true
|
||||
}
|
||||
}
|
||||
if anyOk {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
case "Not":
|
||||
if len(f.Children) != 1 {
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
ok, exitCode := ServerApplyFilter(f.Children[0], entry)
|
||||
if exitCode != ldap.LDAPResultSuccess {
|
||||
return false, exitCode
|
||||
} else if !ok {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
case "Substrings":
|
||||
if len(f.Children) != 2 {
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
attribute := f.Children[0].Value.(string)
|
||||
bytes := f.Children[1].Children[0].Data.Bytes()
|
||||
value := casefold.String(string(bytes))
|
||||
for _, a := range entry.Attributes {
|
||||
if strings.EqualFold(a.Name, attribute) {
|
||||
for _, v := range a.Values {
|
||||
v = casefold.String(v)
|
||||
switch f.Children[1].Children[0].Tag {
|
||||
case FilterSubstringsInitial:
|
||||
if strings.HasPrefix(v, value) {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
case FilterSubstringsAny:
|
||||
if strings.Contains(v, value) {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
case FilterSubstringsFinal:
|
||||
if strings.HasSuffix(v, value) {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "FilterGreaterOrEqual": // TODO
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
case "FilterLessOrEqual": // TODO
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
case "FilterApproxMatch": // TODO
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
case "FilterExtensibleMatch": // TODO
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
|
||||
return false, ldap.LDAPResultSuccess
|
||||
}
|
||||
|
||||
func ServerFilterScope(baseDN string, scope int, entry *ldap.Entry) (bool, LDAPResultCode) {
|
||||
// constrained search scope
|
||||
switch scope {
|
||||
case ldap.ScopeWholeSubtree: // The scope is constrained to the entry named by baseObject and to all its subordinates.
|
||||
case ldap.ScopeBaseObject: // The scope is constrained to the entry named by baseObject.
|
||||
if entry.DN != baseDN {
|
||||
return false, ldap.LDAPResultSuccess
|
||||
}
|
||||
case ldap.ScopeSingleLevel: // The scope is constrained to the immediate subordinates of the entry named by baseObject.
|
||||
parts := strings.Split(entry.DN, ",")
|
||||
if len(parts) < 2 && entry.DN != baseDN {
|
||||
return false, ldap.LDAPResultSuccess
|
||||
}
|
||||
if dn := strings.Join(parts[1:], ","); dn != baseDN {
|
||||
return false, ldap.LDAPResultSuccess
|
||||
}
|
||||
}
|
||||
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
|
||||
func ServerFilterAttributes(attributes []string, entry *ldap.Entry) (LDAPResultCode, error) {
|
||||
// attributes
|
||||
if len(attributes) > 1 || (len(attributes) == 1 && len(attributes[0]) > 0) {
|
||||
_, err := filterAttributes(entry, attributes)
|
||||
if err != nil {
|
||||
return ldap.LDAPResultOperationsError, err
|
||||
}
|
||||
}
|
||||
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
13
vendor/github.com/libregraph/idm/pkg/ldapserver/ldap.go
generated
vendored
Normal file
13
vendor/github.com/libregraph/idm/pkg/ldapserver/ldap.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
type LDAPResultCode uint8
|
||||
|
||||
const (
|
||||
LDAPBindAuthSimple = 0
|
||||
LDAPBindAuthSASL = 3
|
||||
)
|
||||
107
vendor/github.com/libregraph/idm/pkg/ldapserver/modify.go
generated
vendored
Normal file
107
vendor/github.com/libregraph/idm/pkg/ldapserver/modify.go
generated
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleModifyRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
|
||||
if boundDN == "" {
|
||||
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
|
||||
}
|
||||
modReq, err := parseModifyRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.V(1).Info("Parsed Modification", "request", dumpModRequest(modReq))
|
||||
|
||||
fnNames := []string{}
|
||||
for k := range server.ModifyFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(modReq.DN, fnNames)
|
||||
var modifier Modifier
|
||||
if modifier = server.ModifyFns[fn]; modifier == nil {
|
||||
if fn == "" {
|
||||
err = fmt.Errorf("no suitable handler found for dn: '%s'", modReq.DN)
|
||||
} else {
|
||||
err = fmt.Errorf("handler '%s' does not support modify", fn)
|
||||
}
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
|
||||
}
|
||||
code, err := modifier.Modify(boundDN, modReq, conn)
|
||||
return ldap.NewError(uint16(code), err)
|
||||
}
|
||||
|
||||
func dumpModRequest(mr *ldap.ModifyRequest) string {
|
||||
str := fmt.Sprintf("dn: %s\n", mr.DN)
|
||||
for _, change := range mr.Changes {
|
||||
str += fmt.Sprintf("op: %d\n attr: %s values: %v\n", change.Operation, change.Modification.Type, change.Modification.Vals)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func parseModifyRequest(req *ber.Packet) (*ldap.ModifyRequest, error) {
|
||||
modReq := ldap.ModifyRequest{}
|
||||
// LDAP Modify requests have 2 Elements (DN and AttributeList)
|
||||
if len(req.Children) != 2 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid modify request"))
|
||||
}
|
||||
|
||||
dn, ok := req.Children[0].Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry DN"))
|
||||
}
|
||||
|
||||
_, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
|
||||
}
|
||||
modReq.DN = dn
|
||||
|
||||
ml, err := parseModList(req.Children[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modReq.Changes = ml
|
||||
|
||||
return &modReq, nil
|
||||
}
|
||||
|
||||
func parseModList(req *ber.Packet) ([]ldap.Change, error) {
|
||||
var changes []ldap.Change
|
||||
|
||||
if req.ClassType != ber.ClassUniversal || req.TagType != ber.TypeConstructed || req.Tag != ber.TagSequence {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Changes List"))
|
||||
}
|
||||
|
||||
for _, c := range req.Children {
|
||||
var change ldap.Change
|
||||
switch c.Children[0].Data.Bytes()[0] {
|
||||
default:
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid change operation"))
|
||||
case ldap.AddAttribute:
|
||||
change.Operation = ldap.AddAttribute
|
||||
logger.V(1).Info("op=add")
|
||||
case ldap.ReplaceAttribute:
|
||||
change.Operation = ldap.ReplaceAttribute
|
||||
logger.V(1).Info("op=replace")
|
||||
case ldap.DeleteAttribute:
|
||||
change.Operation = ldap.DeleteAttribute
|
||||
logger.V(1).Info("op=delete")
|
||||
}
|
||||
attr, err := parseAttribute(c.Children[1], true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
change.Modification = ldap.PartialAttribute{Type: attr.Type, Vals: attr.Vals}
|
||||
changes = append(changes, change)
|
||||
|
||||
}
|
||||
return changes, nil
|
||||
}
|
||||
83
vendor/github.com/libregraph/idm/pkg/ldapserver/modifydn.go
generated
vendored
Normal file
83
vendor/github.com/libregraph/idm/pkg/ldapserver/modifydn.go
generated
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleModifyDNRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
|
||||
if boundDN == "" {
|
||||
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
|
||||
}
|
||||
|
||||
modDNReq, err := parseModifyDNRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fnNames := []string{}
|
||||
for k := range server.ModifyDNFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(modDNReq.DN, fnNames)
|
||||
var rename Renamer
|
||||
if rename = server.ModifyDNFns[fn]; rename == nil {
|
||||
if fn == "" {
|
||||
err = fmt.Errorf("no suitable handler found for dn: '%s'", modDNReq.DN)
|
||||
} else {
|
||||
err = fmt.Errorf("handler '%s' does not support rename", fn)
|
||||
}
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
|
||||
}
|
||||
code, err := rename.ModifyDN(boundDN, modDNReq, conn)
|
||||
return ldap.NewError(uint16(code), err)
|
||||
return ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid ModifyDN request"))
|
||||
}
|
||||
|
||||
func parseModifyDNRequest(req *ber.Packet) (*ldap.ModifyDNRequest, error) {
|
||||
modDNReq := ldap.ModifyDNRequest{}
|
||||
// LDAP ModifyDN requests have up to 4 Elements (DN, newRDN, deleteOld flag and new superior)
|
||||
if len(req.Children) < 3 || len(req.Children) > 4 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid ModifyDN request"))
|
||||
}
|
||||
|
||||
dn, ok := req.Children[0].Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry DN"))
|
||||
}
|
||||
|
||||
_, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
|
||||
}
|
||||
modDNReq.DN = dn
|
||||
|
||||
newRDN, ok := req.Children[1].Value.(string)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry new RDN"))
|
||||
}
|
||||
|
||||
_, err = ldap.ParseDN(newRDN)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
|
||||
}
|
||||
modDNReq.NewRDN = newRDN
|
||||
|
||||
removeOld, ok := req.Children[2].Value.(bool)
|
||||
if !ok {
|
||||
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding 'deleteOldRDN` flag"))
|
||||
}
|
||||
|
||||
modDNReq.DeleteOldRDN = removeOld
|
||||
|
||||
// moving to a new subtree is not yet supported
|
||||
if len(req.Children) == 4 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("moving to 'newSuperior' is not implemented"))
|
||||
}
|
||||
|
||||
return &modDNReq, nil
|
||||
}
|
||||
119
vendor/github.com/libregraph/idm/pkg/ldapserver/pwmodifyexop.go
generated
vendored
Normal file
119
vendor/github.com/libregraph/idm/pkg/ldapserver/pwmodifyexop.go
generated
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
)
|
||||
|
||||
const pwmodOID = "1.3.6.1.4.1.4203.1.11.1"
|
||||
|
||||
const (
|
||||
TagReqIdentity = 0
|
||||
TagReqOldPW = 1
|
||||
TagReqNewPW = 2
|
||||
TagRespGenPW = 0
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterExtendedOperation(pwmodOID, HandlePasswordModifyExOp)
|
||||
}
|
||||
|
||||
func HandlePasswordModifyExOp(req *ber.Packet, boundDN string, server *Server, conn net.Conn) (*ber.Packet, error) {
|
||||
var passwordGenerated bool
|
||||
logger.V(1).Info("HandlePasswordModifyExOp")
|
||||
if boundDN == "" {
|
||||
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("authentication required"))
|
||||
}
|
||||
|
||||
pwReq, err := parsePasswordModifyExop(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If `UserIdentity` is empty, this is a request to update the bound user's own password
|
||||
if pwReq.UserIdentity == "" {
|
||||
pwReq.UserIdentity = boundDN
|
||||
}
|
||||
|
||||
if pwReq.NewPassword == "" {
|
||||
// New password empty means, we're requested to generate a new password
|
||||
var err error
|
||||
if pwReq.NewPassword, err = ldappassword.GenerateRandomPassword(server.GeneratedPasswordLength); err != nil {
|
||||
logger.Error(err, "Failed to generate new password")
|
||||
return nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("Failed to generate new Password"))
|
||||
}
|
||||
passwordGenerated = true
|
||||
}
|
||||
pwReq.NewPassword, err = ldappassword.Hash(pwReq.NewPassword, "{ARGON2}")
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultOperationsError, err)
|
||||
}
|
||||
|
||||
logger.V(1).Info("Modify password extended operation", "dn", pwReq.UserIdentity)
|
||||
|
||||
fnNames := []string{}
|
||||
for k := range server.PasswordExOpFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(pwReq.UserIdentity, fnNames)
|
||||
var pwUpdatefn PasswordUpdater
|
||||
if pwUpdatefn = server.PasswordExOpFns[fn]; pwUpdatefn == nil {
|
||||
if fn == "" {
|
||||
err = fmt.Errorf("no suitable handler found for dn: '%s'", pwReq.UserIdentity)
|
||||
} else {
|
||||
err = fmt.Errorf("handler '%s' does not support add", fn)
|
||||
}
|
||||
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
|
||||
}
|
||||
code, err := pwUpdatefn.ModifyPasswordExop(boundDN, pwReq, conn)
|
||||
if code != ldap.LDAPResultSuccess {
|
||||
return nil, ldap.NewError(uint16(code), err)
|
||||
}
|
||||
var response *ber.Packet
|
||||
if passwordGenerated {
|
||||
response = ber.NewSequence("PasswdModifyResponseValue")
|
||||
response.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, pwReq.NewPassword, "genPasswd"))
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func parsePasswordModifyExop(req *ber.Packet) (*ldap.PasswordModifyRequest, error) {
|
||||
pwReq := ldap.PasswordModifyRequest{}
|
||||
|
||||
// An absent (or empty) body of the request is valid. Translates into: "generate a new password for
|
||||
// for the current user"
|
||||
if req == nil {
|
||||
return &pwReq, nil
|
||||
}
|
||||
|
||||
inner := ber.DecodePacket(req.Data.Bytes())
|
||||
if inner == nil {
|
||||
return &pwReq, nil
|
||||
}
|
||||
|
||||
if len(inner.Children) > 3 {
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid request"))
|
||||
}
|
||||
|
||||
for _, kid := range inner.Children {
|
||||
if kid.ClassType != ber.ClassContext {
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid request"))
|
||||
}
|
||||
switch kid.Tag {
|
||||
default:
|
||||
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid request"))
|
||||
case TagReqIdentity:
|
||||
pwReq.UserIdentity = kid.Data.String()
|
||||
case TagReqOldPW:
|
||||
pwReq.OldPassword = kid.Data.String()
|
||||
case TagReqNewPW:
|
||||
pwReq.NewPassword = kid.Data.String()
|
||||
}
|
||||
}
|
||||
return &pwReq, nil
|
||||
}
|
||||
217
vendor/github.com/libregraph/idm/pkg/ldapserver/search.go
generated
vendored
Normal file
217
vendor/github.com/libregraph/idm/pkg/ldapserver/search.go
generated
vendored
Normal file
@@ -0,0 +1,217 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func HandleSearchRequest(req *ber.Packet, controls *[]ldap.Control, messageID int64, boundDN string, server *Server, conn net.Conn) (doneControls *[]ldap.Control, resultErr error) {
|
||||
searchReq, err := parseSearchRequest(boundDN, req, controls)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultOperationsError, err)
|
||||
}
|
||||
|
||||
var filterPacket *ber.Packet
|
||||
if server.EnforceLDAP {
|
||||
filterPacket, err = ldap.CompileFilter(searchReq.Filter)
|
||||
if err != nil {
|
||||
return nil, ldap.NewError(ldap.LDAPResultOperationsError, err)
|
||||
}
|
||||
}
|
||||
|
||||
fnNames := []string{}
|
||||
for k := range server.SearchFns {
|
||||
fnNames = append(fnNames, k)
|
||||
}
|
||||
fn := routeFunc(searchReq.BaseDN, fnNames)
|
||||
searchResp, err := server.SearchFns[fn].Search(boundDN, searchReq, conn)
|
||||
if err != nil {
|
||||
return &searchResp.Controls, ldap.NewError(uint16(searchResp.ResultCode), err)
|
||||
}
|
||||
|
||||
if server.EnforceLDAP {
|
||||
if searchReq.DerefAliases != ldap.NeverDerefAliases { // [-a {never|always|search|find}
|
||||
// TODO: Server DerefAliases not supported: RFC4511 4.5.1.3
|
||||
}
|
||||
if searchReq.TimeLimit > 0 {
|
||||
// TODO: Server TimeLimit not implemented
|
||||
}
|
||||
}
|
||||
|
||||
i := 0
|
||||
for _, entry := range searchResp.Entries {
|
||||
if server.EnforceLDAP {
|
||||
// filter
|
||||
keep, resultCode := ServerApplyFilter(filterPacket, entry)
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
return &searchResp.Controls, ldap.NewError(uint16(resultCode), errors.New("ServerApplyFilter error"))
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
|
||||
keep, resultCode = ServerFilterScope(searchReq.BaseDN, searchReq.Scope, entry)
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
return &searchResp.Controls, ldap.NewError(uint16(resultCode), errors.New("ServerApplyScope error"))
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
|
||||
resultCode, err = ServerFilterAttributes(searchReq.Attributes, entry)
|
||||
if err != nil {
|
||||
return &searchResp.Controls, ldap.NewError(uint16(resultCode), err)
|
||||
}
|
||||
|
||||
// size limit
|
||||
if searchReq.SizeLimit > 0 && i >= searchReq.SizeLimit {
|
||||
resultErr = ldap.NewError(
|
||||
ldap.LDAPResultSizeLimitExceeded,
|
||||
errors.New(ldap.LDAPResultCodeMap[ldap.LDAPResultSizeLimitExceeded]),
|
||||
)
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// respond
|
||||
responsePacket := encodeSearchResponse(messageID, searchReq, entry)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
return &searchResp.Controls, ldap.NewError(ldap.LDAPResultOperationsError, err)
|
||||
}
|
||||
}
|
||||
return &searchResp.Controls, resultErr
|
||||
}
|
||||
|
||||
func parseSearchRequest(boundDN string, req *ber.Packet, controls *[]ldap.Control) (*ldap.SearchRequest, error) {
|
||||
if len(req.Children) != 8 {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("Bad search request"))
|
||||
}
|
||||
|
||||
// Parse the request.
|
||||
baseObject, ok := req.Children[0].Value.(string)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
s, ok := req.Children[1].Value.(int64)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
scope := int(s)
|
||||
d, ok := req.Children[2].Value.(int64)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
derefAliases := int(d)
|
||||
s, ok = req.Children[3].Value.(int64)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
sizeLimit := int(s)
|
||||
t, ok := req.Children[4].Value.(int64)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
timeLimit := int(t)
|
||||
typesOnly := false
|
||||
if req.Children[5].Value != nil {
|
||||
typesOnly, ok = req.Children[5].Value.(bool)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
}
|
||||
filter, err := DecompileFilter(req.Children[6])
|
||||
if err != nil {
|
||||
return &ldap.SearchRequest{}, err
|
||||
}
|
||||
attributes := []string{}
|
||||
for _, attr := range req.Children[7].Children {
|
||||
a, ok := attr.Value.(string)
|
||||
if !ok {
|
||||
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
|
||||
}
|
||||
attributes = append(attributes, a)
|
||||
}
|
||||
searchReq := &ldap.SearchRequest{baseObject, scope,
|
||||
derefAliases, sizeLimit, timeLimit,
|
||||
typesOnly, filter, attributes, *controls}
|
||||
|
||||
return searchReq, nil
|
||||
}
|
||||
|
||||
func filterAttributes(entry *ldap.Entry, attributes []string) (*ldap.Entry, error) {
|
||||
// Only return requested attributes.
|
||||
newAttributes := []*ldap.EntryAttribute{}
|
||||
|
||||
for _, attr := range entry.Attributes {
|
||||
for _, requested := range attributes {
|
||||
if requested == "*" || strings.EqualFold(attr.Name, requested) {
|
||||
newAttributes = append(newAttributes, attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.Attributes = newAttributes
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func encodeSearchResponse(messageID int64, req *ldap.SearchRequest, res *ldap.Entry) *ber.Packet {
|
||||
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
|
||||
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
|
||||
|
||||
searchEntry := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ldap.ApplicationSearchResultEntry, nil, "Search Result Entry")
|
||||
searchEntry.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, res.DN, "Object Name"))
|
||||
|
||||
attrs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes: ")
|
||||
for _, attribute := range res.Attributes {
|
||||
attrs.AppendChild(encodeSearchAttribute(attribute.Name, attribute.Values))
|
||||
}
|
||||
|
||||
searchEntry.AppendChild(attrs)
|
||||
responsePacket.AppendChild(searchEntry)
|
||||
|
||||
return responsePacket
|
||||
}
|
||||
|
||||
func encodeSearchAttribute(name string, values []string) *ber.Packet {
|
||||
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute")
|
||||
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, name, "Attribute Name"))
|
||||
|
||||
valuesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "Attribute Values: ")
|
||||
for _, value := range values {
|
||||
valuesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Attribute Value"))
|
||||
}
|
||||
|
||||
packet.AppendChild(valuesPacket)
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
func encodeSearchDone(messageID int64, ldapResultCode LDAPResultCode, doneControls *[]ldap.Control) *ber.Packet {
|
||||
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
|
||||
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
|
||||
donePacket := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ldap.ApplicationSearchResultDone, nil, "Search result done")
|
||||
donePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: "))
|
||||
donePacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
|
||||
donePacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "errorMessage: "))
|
||||
responsePacket.AppendChild(donePacket)
|
||||
|
||||
if doneControls != nil {
|
||||
contextPacket := ber.Encode(ber.ClassContext, ber.TypeConstructed, ber.TagEOC, nil, "Controls: ")
|
||||
for _, control := range *doneControls {
|
||||
contextPacket.AppendChild(control.Encode())
|
||||
}
|
||||
responsePacket.AppendChild(contextPacket)
|
||||
}
|
||||
// ber.PrintPacket(responsePacket)
|
||||
return responsePacket
|
||||
}
|
||||
492
vendor/github.com/libregraph/idm/pkg/ldapserver/server.go
generated
vendored
Normal file
492
vendor/github.com/libregraph/idm/pkg/ldapserver/server.go
generated
vendored
Normal file
@@ -0,0 +1,492 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Copyright 2021 The LibreGraph Authors.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ldapserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
ber "github.com/go-asn1-ber/asn1-ber"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/go-logr/stdr"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapdn"
|
||||
)
|
||||
|
||||
type Adder interface {
|
||||
Add(boundDN string, req *ldap.AddRequest, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type Binder interface {
|
||||
Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type Deleter interface {
|
||||
Delete(boundDN string, req *ldap.DelRequest, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type Modifier interface {
|
||||
Modify(boundDN string, req *ldap.ModifyRequest, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type PasswordUpdater interface {
|
||||
ModifyPasswordExop(boundDN string, req *ldap.PasswordModifyRequest, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type Renamer interface {
|
||||
ModifyDN(boundDN string, req *ldap.ModifyDNRequest, conn net.Conn) (LDAPResultCode, error)
|
||||
}
|
||||
|
||||
type Searcher interface {
|
||||
Search(boundDN string, req *ldap.SearchRequest, conn net.Conn) (ServerSearchResult, error)
|
||||
}
|
||||
|
||||
type Closer interface {
|
||||
Close(boundDN string, conn net.Conn) error
|
||||
}
|
||||
|
||||
var logger logr.Logger = stdr.New(log.Default())
|
||||
|
||||
type Server struct {
|
||||
AddFns map[string]Adder
|
||||
BindFns map[string]Binder
|
||||
DeleteFns map[string]Deleter
|
||||
ModifyFns map[string]Modifier
|
||||
ModifyDNFns map[string]Renamer
|
||||
PasswordExOpFns map[string]PasswordUpdater
|
||||
SearchFns map[string]Searcher
|
||||
CloseFns map[string]Closer
|
||||
Quit chan bool
|
||||
EnforceLDAP bool
|
||||
GeneratedPasswordLength int
|
||||
Stats *Stats
|
||||
}
|
||||
|
||||
type ServerSearchResult struct {
|
||||
Entries []*ldap.Entry
|
||||
Referrals []string
|
||||
Controls []ldap.Control
|
||||
ResultCode LDAPResultCode
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
s := new(Server)
|
||||
s.Quit = make(chan bool)
|
||||
|
||||
d := defaultHandler{}
|
||||
s.AddFns = make(map[string]Adder)
|
||||
s.BindFns = make(map[string]Binder)
|
||||
s.DeleteFns = make(map[string]Deleter)
|
||||
s.ModifyFns = make(map[string]Modifier)
|
||||
s.ModifyDNFns = make(map[string]Renamer)
|
||||
s.PasswordExOpFns = make(map[string]PasswordUpdater)
|
||||
s.SearchFns = make(map[string]Searcher)
|
||||
s.CloseFns = make(map[string]Closer)
|
||||
s.BindFunc("", d)
|
||||
s.SearchFunc("", d)
|
||||
s.CloseFunc("", d)
|
||||
s.GeneratedPasswordLength = 16
|
||||
s.Stats = nil
|
||||
return s
|
||||
}
|
||||
|
||||
func Logger(l logr.Logger) {
|
||||
logger = l
|
||||
}
|
||||
|
||||
func (server *Server) AddFunc(baseDN string, f Adder) {
|
||||
server.AddFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) BindFunc(baseDN string, f Binder) {
|
||||
server.BindFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) DeleteFunc(baseDN string, f Deleter) {
|
||||
server.DeleteFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) ModifyFunc(baseDN string, f Modifier) {
|
||||
server.ModifyFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) ModifyDNFunc(baseDN string, f Renamer) {
|
||||
server.ModifyDNFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) PasswordExOpFunc(baseDN string, f PasswordUpdater) {
|
||||
server.PasswordExOpFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) SearchFunc(baseDN string, f Searcher) {
|
||||
server.SearchFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) CloseFunc(baseDN string, f Closer) {
|
||||
server.CloseFns[baseDN] = f
|
||||
}
|
||||
|
||||
func (server *Server) QuitChannel(quit chan bool) {
|
||||
server.Quit = quit
|
||||
}
|
||||
|
||||
func (server *Server) ListenAndServeTLS(listenString string, certFile string, keyFile string) error {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
tlsConfig.ServerName = "localhost"
|
||||
ln, err := tls.Listen("tcp", listenString, &tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = server.Serve(ln)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) SetStats(enable bool) {
|
||||
if enable {
|
||||
server.Stats = &Stats{}
|
||||
} else {
|
||||
server.Stats = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) GetStats() Stats {
|
||||
return *server.Stats.Clone()
|
||||
}
|
||||
|
||||
func (server *Server) ListenAndServe(listenString string) error {
|
||||
ln, err := net.Listen("tcp", listenString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = server.Serve(ln)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) Serve(ln net.Listener) error {
|
||||
newConn := make(chan net.Conn)
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
if !strings.HasSuffix(err.Error(), "use of closed network connection") {
|
||||
logger.Error(err, "Error accepting network connection")
|
||||
}
|
||||
break
|
||||
}
|
||||
logger.V(1).Info("New Connection", "addr", ln.Addr())
|
||||
newConn <- conn
|
||||
}
|
||||
}()
|
||||
|
||||
listener:
|
||||
for {
|
||||
select {
|
||||
case c := <-newConn:
|
||||
server.Stats.countConns(1)
|
||||
go server.handleConnection(c)
|
||||
case <-server.Quit:
|
||||
ln.Close()
|
||||
break listener
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
func (server *Server) handleConnection(conn net.Conn) {
|
||||
boundDN := "" // "" == anonymous
|
||||
|
||||
handler:
|
||||
for {
|
||||
// Read incoming LDAP packet.
|
||||
packet, err := ber.ReadPacket(conn)
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF { // Client closed connection.
|
||||
break
|
||||
} else if err != nil {
|
||||
logger.Error(err, "handleConnection ber.ReadPacket")
|
||||
break
|
||||
}
|
||||
|
||||
// Sanity check this packet.
|
||||
if len(packet.Children) < 2 {
|
||||
logger.V(1).Info("len(packet.Children) < 2")
|
||||
break
|
||||
}
|
||||
// Check the message ID and ClassType.
|
||||
messageID, ok := packet.Children[0].Value.(int64)
|
||||
if !ok {
|
||||
logger.V(1).Info("malformed messageID")
|
||||
break
|
||||
}
|
||||
req := packet.Children[1]
|
||||
if req.ClassType != ber.ClassApplication {
|
||||
logger.V(1).Info("req.ClassType != ber.ClassApplication")
|
||||
break
|
||||
}
|
||||
// Handle controls if present.
|
||||
controls := []ldap.Control{}
|
||||
if len(packet.Children) > 2 {
|
||||
for _, child := range packet.Children[2].Children {
|
||||
c, err := ldap.DecodeControl(child)
|
||||
if err != nil {
|
||||
logger.Error(err, "handleConnection decode control")
|
||||
continue
|
||||
}
|
||||
controls = append(controls, c)
|
||||
}
|
||||
}
|
||||
|
||||
// log.Printf("DEBUG: handling operation: %s [%d]", ldap.ApplicationMap[uint8(req.Tag)], req.Tag)
|
||||
// ber.PrintPacket(packet) // DEBUG
|
||||
|
||||
// Dispatch the LDAP operation.
|
||||
switch req.Tag { // LDAP op code.
|
||||
default:
|
||||
op, ok := ldap.ApplicationMap[uint8(req.Tag)]
|
||||
if !ok {
|
||||
op = "unknown"
|
||||
}
|
||||
|
||||
logger.V(1).Info("Unhandled operation", "type", op, "tag", req.Tag)
|
||||
break handler
|
||||
|
||||
case ldap.ApplicationAddRequest:
|
||||
server.Stats.countAdds(1)
|
||||
|
||||
resultCode := uint16(ldap.LDAPResultSuccess)
|
||||
resultMsg := ""
|
||||
if err = HandleAddRequest(req, boundDN, server, conn); err != nil {
|
||||
var lErr *ldap.Error
|
||||
if errors.As(err, &lErr) {
|
||||
resultCode = lErr.ResultCode
|
||||
if lErr.Err != nil {
|
||||
resultMsg = lErr.Err.Error()
|
||||
}
|
||||
} else {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
resultMsg = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationAddResponse, LDAPResultCode(resultCode), resultMsg)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationBindRequest:
|
||||
server.Stats.countBinds(1)
|
||||
ldapResultCode := HandleBindRequest(req, server.BindFns, conn)
|
||||
if ldapResultCode == ldap.LDAPResultSuccess {
|
||||
boundDN, ok = req.Children[1].Value.(string)
|
||||
if !ok {
|
||||
logger.V(1).Info("Malformed Bind DN")
|
||||
break handler
|
||||
}
|
||||
if boundDN, err = ldapdn.ParseNormalize(boundDN); err != nil {
|
||||
logger.V(1).Info("Error normalizing Bind DN", "error", err.Error())
|
||||
break handler
|
||||
}
|
||||
|
||||
}
|
||||
responsePacket := encodeBindResponse(messageID, ldapResultCode)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationCompareRequest:
|
||||
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationCompareRequest, ldap.LDAPResultOperationsError, "Unsupported operation: compare")
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
}
|
||||
logger.V(1).Info("Unhandled operation", "type", ldap.ApplicationMap[uint8(req.Tag)], "tag", req.Tag)
|
||||
break handler
|
||||
|
||||
case ldap.ApplicationDelRequest:
|
||||
server.Stats.countDeletes(1)
|
||||
resultCode := uint16(ldap.LDAPResultSuccess)
|
||||
resultMsg := ""
|
||||
if err = HandleDeleteRequest(req, boundDN, server, conn); err != nil {
|
||||
var lErr *ldap.Error
|
||||
if errors.As(err, &lErr) {
|
||||
resultCode = lErr.ResultCode
|
||||
if lErr.Err != nil {
|
||||
resultMsg = lErr.Err.Error()
|
||||
}
|
||||
} else {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
resultMsg = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationDelResponse, LDAPResultCode(resultCode), resultMsg)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationExtendedRequest:
|
||||
resultCode := uint16(ldap.LDAPResultSuccess)
|
||||
resultMsg := ""
|
||||
var innerBer, responsePacket *ber.Packet
|
||||
if innerBer, err = HandleExtendedRequest(req, boundDN, server, conn); err != nil {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
resultMsg = err.Error()
|
||||
responsePacket = encodeLDAPResponse(messageID, ldap.ApplicationExtendedResponse, LDAPResultCode(resultCode), resultMsg)
|
||||
} else {
|
||||
responsePacket = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
|
||||
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
|
||||
responsePacket.AppendChild(innerBer)
|
||||
}
|
||||
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationModifyDNRequest:
|
||||
server.Stats.countModifyDNs(1)
|
||||
resultCode := uint16(ldap.LDAPResultSuccess)
|
||||
resultMsg := ""
|
||||
if err = HandleModifyDNRequest(req, boundDN, server, conn); err != nil {
|
||||
var lErr *ldap.Error
|
||||
if errors.As(err, &lErr) {
|
||||
resultCode = lErr.ResultCode
|
||||
if lErr.Err != nil {
|
||||
resultMsg = lErr.Err.Error()
|
||||
}
|
||||
} else {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
resultMsg = err.Error()
|
||||
}
|
||||
}
|
||||
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationModifyDNResponse, LDAPResultCode(resultCode), resultMsg)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationModifyRequest:
|
||||
server.Stats.countModifies(1)
|
||||
resultCode := uint16(ldap.LDAPResultSuccess)
|
||||
resultMsg := ""
|
||||
if err = HandleModifyRequest(req, boundDN, server, conn); err != nil {
|
||||
var lErr *ldap.Error
|
||||
if errors.As(err, &lErr) {
|
||||
resultCode = lErr.ResultCode
|
||||
if lErr.Err != nil {
|
||||
resultMsg = lErr.Err.Error()
|
||||
}
|
||||
} else {
|
||||
resultCode = ldap.LDAPResultOperationsError
|
||||
resultMsg = err.Error()
|
||||
}
|
||||
}
|
||||
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationModifyResponse, LDAPResultCode(resultCode), resultMsg)
|
||||
if err = sendPacket(conn, responsePacket); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
|
||||
case ldap.ApplicationSearchRequest:
|
||||
server.Stats.countSearches(1)
|
||||
if doneControls, err := HandleSearchRequest(req, &controls, messageID, boundDN, server, conn); err != nil {
|
||||
// TODO: make this more testable/better err handling - stop using log, stop using breaks?
|
||||
logger.V(1).Info("handleSearchRequest", "error", err.Error())
|
||||
e := err.(*ldap.Error)
|
||||
if err = sendPacket(conn, encodeSearchDone(messageID, LDAPResultCode(e.ResultCode), doneControls)); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
break handler
|
||||
} else {
|
||||
if err = sendPacket(conn, encodeSearchDone(messageID, ldap.LDAPResultSuccess, doneControls)); err != nil {
|
||||
logger.Error(err, "sendPacket error")
|
||||
break handler
|
||||
}
|
||||
}
|
||||
case ldap.ApplicationUnbindRequest:
|
||||
server.Stats.countUnbinds(1)
|
||||
break handler // Simply disconnect.
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range server.CloseFns {
|
||||
c.Close(boundDN, conn)
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
server.Stats.countConnsClose(1)
|
||||
}
|
||||
|
||||
func sendPacket(conn net.Conn, packet *ber.Packet) error {
|
||||
_, err := conn.Write(packet.Bytes())
|
||||
if err != nil {
|
||||
logger.Error(err, "Error Sending Message")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func routeFunc(dn string, funcNames []string) string {
|
||||
bestPick := ""
|
||||
for _, fn := range funcNames {
|
||||
if strings.HasSuffix(dn, fn) {
|
||||
l := len(strings.Split(bestPick, ","))
|
||||
if bestPick == "" {
|
||||
l = 0
|
||||
}
|
||||
if len(strings.Split(fn, ",")) > l {
|
||||
bestPick = fn
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestPick
|
||||
}
|
||||
|
||||
func encodeLDAPResponse(messageID int64, responseType uint8, ldapResultCode LDAPResultCode, message string) *ber.Packet {
|
||||
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
|
||||
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
|
||||
response := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ber.Tag(responseType), nil, ldap.ApplicationMap[responseType])
|
||||
response.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: "))
|
||||
response.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
|
||||
response.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, message, "errorMessage: "))
|
||||
responsePacket.AppendChild(response)
|
||||
return responsePacket
|
||||
}
|
||||
|
||||
type defaultHandler struct {
|
||||
}
|
||||
|
||||
func (h defaultHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) {
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
func (h defaultHandler) Search(boundDN string, req *ldap.SearchRequest, conn net.Conn) (ServerSearchResult, error) {
|
||||
return ServerSearchResult{make([]*ldap.Entry, 0), []string{}, []ldap.Control{}, ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
func (h defaultHandler) Close(boundDN string, conn net.Conn) error {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
119
vendor/github.com/libregraph/idm/pkg/ldapserver/stats.go
generated
vendored
Normal file
119
vendor/github.com/libregraph/idm/pkg/ldapserver/stats.go
generated
vendored
Normal 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
485
vendor/github.com/libregraph/idm/pkg/ldbbolt/ldbbolt.go
generated
vendored
Normal 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
42
vendor/github.com/libregraph/idm/server/config.go
generated
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Config bundles server configuration settings.
|
||||
type Config struct {
|
||||
Logger logrus.FieldLogger
|
||||
|
||||
LDAPHandler string
|
||||
|
||||
LDAPListenAddr string
|
||||
LDAPSListenAddr string
|
||||
|
||||
TLSCertFile string
|
||||
TLSKeyFile string
|
||||
|
||||
LDAPBaseDN string
|
||||
LDAPAdminDN string
|
||||
|
||||
LDAPAllowLocalAnonymousBind bool
|
||||
|
||||
BoltDBFile string
|
||||
|
||||
LDIFMain string
|
||||
LDIFConfig string
|
||||
|
||||
LDIFDefaultCompany string
|
||||
LDIFDefaultMailDomain string
|
||||
LDIFTemplateExtraVars map[string]interface{}
|
||||
|
||||
Metrics prometheus.Registerer
|
||||
|
||||
OnReady func(*Server)
|
||||
}
|
||||
333
vendor/github.com/libregraph/idm/server/handler/boltdb/handler.go
generated
vendored
Normal file
333
vendor/github.com/libregraph/idm/server/handler/boltdb/handler.go
generated
vendored
Normal file
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapdn"
|
||||
"github.com/libregraph/idm/pkg/ldapentry"
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/pkg/ldbbolt"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
)
|
||||
|
||||
type boltdbHandler struct {
|
||||
logger logrus.FieldLogger
|
||||
dbfile string
|
||||
baseDN string
|
||||
adminDN string
|
||||
allowLocalAnonymousBind bool
|
||||
ctx context.Context
|
||||
bdb *ldbbolt.LdbBolt
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
BaseDN string
|
||||
AdminDN string
|
||||
|
||||
AllowLocalAnonymousBind bool
|
||||
}
|
||||
|
||||
func NewBoltDBHandler(logger logrus.FieldLogger, fn string, options *Options) (handler.Handler, error) {
|
||||
if fn == "" {
|
||||
return nil, fmt.Errorf("file name is empty")
|
||||
}
|
||||
if options.BaseDN == "" {
|
||||
return nil, fmt.Errorf("base dn is empty")
|
||||
}
|
||||
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h := &boltdbHandler{
|
||||
logger: logger,
|
||||
dbfile: fn,
|
||||
|
||||
allowLocalAnonymousBind: options.AllowLocalAnonymousBind,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
if h.baseDN, err = ldapdn.ParseNormalize(options.BaseDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h.adminDN, err = ldapdn.ParseNormalize(options.AdminDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = h.setup()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) setup() error {
|
||||
bdb := &ldbbolt.LdbBolt{}
|
||||
|
||||
if err := bdb.Configure(h.logger, h.baseDN, h.dbfile, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bdb.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
h.bdb = bdb
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Add(boundDN string, req *ldap.AddRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "add",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
e := ldapentry.EntryFromAddRequest(req)
|
||||
|
||||
if err := h.bdb.EntryPut(e); err != nil {
|
||||
logger.WithError(err).WithField("entrydn", e.DN).Debugln("ldap add failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "bind",
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
// Handle anoymous bind
|
||||
if bindDN == "" {
|
||||
if !h.allowLocalAnonymousBind {
|
||||
logger.Debugln("ldap anonymous Bind disabled")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
} else if bindSimplePw == "" {
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
}
|
||||
|
||||
bindDN, err := ldapdn.ParseNormalize(bindDN)
|
||||
if err != nil {
|
||||
logger.WithError(err).Debugln("ldap bind request BindDN validation failed")
|
||||
return ldap.LDAPResultInvalidDNSyntax, nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(bindDN, h.baseDN) {
|
||||
logger.WithError(err).Debugln("ldap bind request BindDN outside of Database tree")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
// Disallow empty password
|
||||
if bindSimplePw == "" {
|
||||
logger.Debugf("BindDN without password")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
return h.validatePassword(logger, bindDN, bindSimplePw)
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) validatePassword(logger logrus.FieldLogger, bindDN, bindSimplePw string) (ldapserver.LDAPResultCode, error) {
|
||||
// Lookup Bind DN in database
|
||||
entries, err := h.bdb.Search(bindDN, ldap.ScopeBaseObject)
|
||||
if err != nil || len(entries) != 1 {
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
logger.Debugf("Entry '%s' does not exist", bindDN)
|
||||
}
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
userPassword := entries[0].GetEqualFoldAttributeValue("userPassword")
|
||||
match, err := ldappassword.Validate(bindSimplePw, userPassword)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
if match {
|
||||
logger.Debug("success")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Delete(boundDN string, req *ldap.DelRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "delete",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
logger.Debug("Calling boltdb delete")
|
||||
if err := h.bdb.EntryDelete(req.DN); err != nil {
|
||||
logger.WithError(err).WithField("entrydn", req.DN).Debugln("ldap delete failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
logger.Debug("delete succeeded")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Modify(boundDN string, req *ldap.ModifyRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "modify",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
"entrydn": req.DN,
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
logger.Debug("Calling boltdb modify")
|
||||
if err := h.bdb.EntryModify(req); err != nil {
|
||||
logger.WithError(err).Debug("ldap modify failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
ldapError, ok := err.(*ldap.Error)
|
||||
if !ok {
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
return ldapserver.LDAPResultCode(ldapError.ResultCode), ldapError.Err
|
||||
}
|
||||
logger.Debug("modify succeeded")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) ModifyDN(boundDN string, req *ldap.ModifyDNRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "modifyDN",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
"entrydn": req.DN,
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
logger.Debug("Calling boltdb modify DN")
|
||||
if err := h.bdb.EntryModifyDN(req); err != nil {
|
||||
logger.WithError(err).Debug("ldap modifyDN failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
ldapError, ok := err.(*ldap.Error)
|
||||
if !ok {
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
return ldapserver.LDAPResultCode(ldapError.ResultCode), ldapError.Err
|
||||
}
|
||||
logger.Debug("modify DN succeeded")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) ModifyPasswordExop(boundDN string, req *ldap.PasswordModifyRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "modpw_exop",
|
||||
"binddn": boundDN,
|
||||
"UserIdentity": req.UserIdentity,
|
||||
"OldPWPresent": req.OldPassword != "",
|
||||
"NewPwPresent": req.NewPassword != "",
|
||||
})
|
||||
|
||||
if boundDN == "" {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
if req.UserIdentity != "" {
|
||||
pwUserDN, err := ldapdn.ParseNormalize(req.UserIdentity)
|
||||
if err != nil {
|
||||
return ldap.LDAPResultOperationsError, errors.New("Error parsing DN in password modify extended operation.")
|
||||
}
|
||||
if boundDN != pwUserDN && boundDN != h.adminDN {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Calling boltdb UpdatePassword")
|
||||
err := h.bdb.UpdatePassword(req)
|
||||
if err != nil {
|
||||
logger.Debugf("boltdb UpdatePassword returned '%s'", err)
|
||||
return ldap.LDAPResultOther, err
|
||||
}
|
||||
return ldap.LDAPResultSuccess, err
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Search(boundDN string, req *ldap.SearchRequest, conn net.Conn) (ldapserver.ServerSearchResult, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "search",
|
||||
"binddn": boundDN,
|
||||
"basedn": req.BaseDN,
|
||||
"filter": req.Filter,
|
||||
"attrs": req.Attributes,
|
||||
})
|
||||
|
||||
logger.Debug("Calling boltdb search")
|
||||
entries, _ := h.bdb.Search(req.BaseDN, req.Scope)
|
||||
logger.Debugf("boltdb search returned %d entries", len(entries))
|
||||
|
||||
return ldapserver.ServerSearchResult{
|
||||
Entries: entries,
|
||||
Referrals: []string{},
|
||||
Controls: []ldap.Control{},
|
||||
ResultCode: ldap.LDAPResultSuccess,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Close(boundDN string, conn net.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) WithContext(ctx context.Context) handler.Handler {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
|
||||
h2 := new(boltdbHandler)
|
||||
*h2 = *h
|
||||
h2.ctx = ctx
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Reload(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) writeAllowed(boundDN string) bool {
|
||||
if h.adminDN != "" && h.adminDN == boundDN {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
32
vendor/github.com/libregraph/idm/server/handler/handler.go
generated
vendored
Normal file
32
vendor/github.com/libregraph/idm/server/handler/handler.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
)
|
||||
|
||||
// Interface for handlers.
|
||||
type Handler interface {
|
||||
ldapserver.Adder
|
||||
ldapserver.Binder
|
||||
ldapserver.Deleter
|
||||
ldapserver.Modifier
|
||||
ldapserver.PasswordUpdater
|
||||
ldapserver.Renamer
|
||||
ldapserver.Searcher
|
||||
ldapserver.Closer
|
||||
|
||||
WithContext(context.Context) Handler
|
||||
Reload(context.Context) error
|
||||
}
|
||||
|
||||
// Interface for middlewares.
|
||||
type Middleware interface {
|
||||
WithHandler(next Handler) Handler
|
||||
}
|
||||
31
vendor/github.com/libregraph/idm/server/handler/ldif/entry.go
generated
vendored
Normal file
31
vendor/github.com/libregraph/idm/server/handler/ldif/entry.go
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
)
|
||||
|
||||
type ldifEntry struct {
|
||||
*ldap.Entry
|
||||
|
||||
UserPassword *ldap.EntryAttribute
|
||||
}
|
||||
|
||||
func (entry *ldifEntry) validatePassword(bindSimplePw string) error {
|
||||
match, err := ldappassword.Validate(bindSimplePw, entry.UserPassword.Values[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !match {
|
||||
return fmt.Errorf("password mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
113
vendor/github.com/libregraph/idm/server/handler/ldif/filter.go
generated
vendored
Normal file
113
vendor/github.com/libregraph/idm/server/handler/ldif/filter.go
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-asn1-ber/asn1-ber"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
)
|
||||
|
||||
func parseFilterToIndexFilter(filter string) ([][]string, error) {
|
||||
f, err := ldapserver.CompileFilter(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result [][]string
|
||||
matches, err := parseFilterMatchLeavesForIndex(f, nil, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse filter for index failed: %w", err)
|
||||
} else {
|
||||
withObjectClass := true
|
||||
if len(matches) > 1 {
|
||||
// NOTE(longsleep): In case of more than one filter, we remove the
|
||||
// index lookup for objectClass since pretty much everything has
|
||||
// an object class anyways and our index lookup does not support
|
||||
// "and" lookups. Thus this gains a lot of efficiency.
|
||||
for _, f := range matches {
|
||||
if !strings.EqualFold("objectClass", f[1]) {
|
||||
withObjectClass = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range matches {
|
||||
if !withObjectClass && strings.EqualFold("objectClass", f[1]) {
|
||||
continue
|
||||
}
|
||||
result = append(result, f[1:])
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func parseFilterMatchLeavesForIndex(f *ber.Packet, parent [][]string, level string) ([][]string, error) {
|
||||
var err error
|
||||
|
||||
if parent == nil {
|
||||
parent = make([][]string, 0)
|
||||
}
|
||||
|
||||
switch f.Tag {
|
||||
case ldapserver.FilterEqualityMatch:
|
||||
if len(f.Children) != 2 {
|
||||
return nil, errors.New("unsupported number of children in equality match filter")
|
||||
}
|
||||
attribute := f.Children[0].Value.(string)
|
||||
value := f.Children[1].Value.(string)
|
||||
parent = append(parent, []string{level, attribute, "eq", value})
|
||||
|
||||
case ldapserver.FilterPresent:
|
||||
if len(f.Children) != 0 {
|
||||
return nil, errors.New("unsupported number of children in presence match filter")
|
||||
}
|
||||
attribute := f.Data.String()
|
||||
parent = append(parent, []string{level, attribute, "pres", ""})
|
||||
|
||||
case ldapserver.FilterSubstrings:
|
||||
if len(f.Children) != 2 {
|
||||
return nil, errors.New("unsupported number of children in substrings filter")
|
||||
}
|
||||
attribute := f.Children[0].Value.(string)
|
||||
if len(f.Children[1].Children) != 1 {
|
||||
return nil, errors.New("unsupported number of children in substrings filter")
|
||||
}
|
||||
value := f.Children[1].Children[0].Value.(string)
|
||||
switch f.Children[1].Children[0].Tag {
|
||||
case ldapserver.FilterSubstringsInitial, ldapserver.FilterSubstringsAny, ldapserver.FilterSubstringsFinal:
|
||||
parent = append(parent, []string{level, attribute, "sub", value, strconv.FormatInt(int64(f.Children[1].Children[0].Tag), 10)})
|
||||
}
|
||||
|
||||
case ldapserver.FilterAnd:
|
||||
for idx, child := range f.Children {
|
||||
parent, err = parseFilterMatchLeavesForIndex(child, parent, fmt.Sprintf("%s.&%d", level, idx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case ldapserver.FilterOr:
|
||||
for idx, child := range f.Children {
|
||||
parent, err = parseFilterMatchLeavesForIndex(child, parent, fmt.Sprintf("%s.|%d", level, idx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case ldapserver.FilterNot:
|
||||
// Ignored for now.
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
return parent, nil
|
||||
}
|
||||
512
vendor/github.com/libregraph/idm/server/handler/ldif/handler.go
generated
vendored
Normal file
512
vendor/github.com/libregraph/idm/server/handler/ldif/handler.go
generated
vendored
Normal file
@@ -0,0 +1,512 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/go-ldap/ldif"
|
||||
"github.com/longsleep/rndm"
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapdn"
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
)
|
||||
|
||||
type ldifHandler struct {
|
||||
logger logrus.FieldLogger
|
||||
fn string
|
||||
options *Options
|
||||
|
||||
baseDN string
|
||||
adminDN string
|
||||
allowLocalAnonymousBind bool
|
||||
|
||||
ctx context.Context
|
||||
|
||||
current atomic.Value
|
||||
|
||||
activeSearchPagings cmap.ConcurrentMap
|
||||
}
|
||||
|
||||
var _ handler.Handler = (*ldifHandler)(nil) // Verify that *ldifHandler implements handler.Handler.
|
||||
|
||||
func NewLDIFHandler(logger logrus.FieldLogger, fn string, options *Options) (handler.Handler, error) {
|
||||
if fn == "" {
|
||||
return nil, fmt.Errorf("file name is empty")
|
||||
}
|
||||
if options.BaseDN == "" {
|
||||
return nil, fmt.Errorf("base dn is empty")
|
||||
}
|
||||
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger = logger.WithField("fn", fn)
|
||||
|
||||
h := &ldifHandler{
|
||||
logger: logger,
|
||||
fn: fn,
|
||||
options: options,
|
||||
|
||||
allowLocalAnonymousBind: options.AllowLocalAnonymousBind,
|
||||
|
||||
ctx: context.Background(),
|
||||
|
||||
activeSearchPagings: cmap.New(),
|
||||
}
|
||||
if h.baseDN, err = ldapdn.ParseNormalize(options.BaseDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h.adminDN, err = ldapdn.ParseNormalize(options.AdminDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = h.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) open() error {
|
||||
if !strings.EqualFold(h.options.BaseDN, h.baseDN) {
|
||||
return fmt.Errorf("mismatched BaseDN")
|
||||
}
|
||||
|
||||
info, err := os.Stat(h.fn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open LDIF: %w", err)
|
||||
}
|
||||
|
||||
var l *ldif.LDIF
|
||||
index := newIndexMapRegister()
|
||||
|
||||
if info.IsDir() {
|
||||
h.logger.Debugln("loading LDIF files from folder")
|
||||
h.options.templateBasePath = h.fn
|
||||
var parseErrors []error
|
||||
l, parseErrors, err = parseLDIFDirectory(h.fn, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(parseErrors) > 0 {
|
||||
for _, parseErr := range parseErrors {
|
||||
h.logger.WithError(parseErr).Errorln("LDIF error")
|
||||
}
|
||||
return fmt.Errorf("error in LDIF files")
|
||||
}
|
||||
} else {
|
||||
h.logger.Debugln("loading LDIF")
|
||||
h.options.templateBasePath = filepath.Dir(h.fn)
|
||||
l, err = parseLDIFFile(h.fn, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
t, err := treeFromLDIF(l, index, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store parsed data as memory value.
|
||||
value := &ldifMemoryValue{
|
||||
l: l,
|
||||
t: t,
|
||||
|
||||
index: index,
|
||||
}
|
||||
h.current.Store(value)
|
||||
|
||||
h.logger.WithFields(logrus.Fields{
|
||||
"version": l.Version,
|
||||
"entries_count": len(l.Entries),
|
||||
"tree_length": t.Len(),
|
||||
"base_dn": h.options.BaseDN,
|
||||
"indexes": len(index),
|
||||
}).Debugln("loaded LDIF")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) load() *ldifMemoryValue {
|
||||
value := h.current.Load()
|
||||
return value.(*ldifMemoryValue)
|
||||
}
|
||||
|
||||
func (h *ldifHandler) WithContext(ctx context.Context) handler.Handler {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
|
||||
h2 := new(ldifHandler)
|
||||
*h2 = *h
|
||||
h2.ctx = ctx
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Reload(ctx context.Context) error {
|
||||
return h.open()
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Add(_ string, _ *ldap.AddRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if err := h.validateBindDN(bindDN, conn); err != nil {
|
||||
logger.WithError(err).Debugln("ldap bind request BindDN validation failed")
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
if bindSimplePw == "" {
|
||||
logger.Debugf("ldap anonymous bind request")
|
||||
if bindDN == "" {
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
} else {
|
||||
return ldap.LDAPResultUnwillingToPerform, nil
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("ldap bind request")
|
||||
}
|
||||
|
||||
current := h.load()
|
||||
|
||||
entryRecord, found := current.t.Get([]byte(bindDN))
|
||||
if !found {
|
||||
err := fmt.Errorf("user not found")
|
||||
logger.WithError(err).Debugf("ldap bind error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
entry := entryRecord.(*ldifEntry)
|
||||
|
||||
if err := entry.validatePassword(bindSimplePw); err != nil {
|
||||
logger.WithError(err).Debugf("ldap bind credentials error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Delete(_ string, _ *ldap.DelRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Modify(_ string, _ *ldap.ModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) ModifyDN(_ string, _ *ldap.ModifyDNRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) ModifyPasswordExop(_ string, _ *ldap.PasswordModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Search(bindDN string, searchReq *ldap.SearchRequest, conn net.Conn) (ldapserver.ServerSearchResult, error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
searchBaseDN := strings.ToLower(searchReq.BaseDN)
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"search_base_dn": searchBaseDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
"controls": searchReq.Controls,
|
||||
"size_limit": searchReq.SizeLimit,
|
||||
})
|
||||
|
||||
logger.Debugf("ldap search request for %s", searchReq.Filter)
|
||||
|
||||
if err := h.validateBindDN(bindDN, conn); err != nil {
|
||||
logger.WithError(err).Debugln("ldap search request BindDN validation failed")
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: ldap.LDAPResultInsufficientAccessRights,
|
||||
}, err
|
||||
}
|
||||
|
||||
indexFilter, _ := parseFilterToIndexFilter(searchReq.Filter)
|
||||
|
||||
if !strings.HasSuffix(searchBaseDN, h.baseDN) {
|
||||
err := fmt.Errorf("ldap search BaseDN is not in our BaseDN %s", h.baseDN)
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: ldap.LDAPResultInsufficientAccessRights,
|
||||
}, err
|
||||
}
|
||||
|
||||
doneControls := []ldap.Control{}
|
||||
var pagingControl *ldap.ControlPaging
|
||||
var pagingCookie []byte
|
||||
if paging := ldap.FindControl(searchReq.Controls, ldap.ControlTypePaging); paging != nil {
|
||||
pagingControl = paging.(*ldap.ControlPaging)
|
||||
if searchReq.SizeLimit > 0 && pagingControl.PagingSize >= uint32(searchReq.SizeLimit) {
|
||||
pagingControl = nil
|
||||
} else {
|
||||
pagingCookie = pagingControl.Cookie
|
||||
}
|
||||
}
|
||||
|
||||
pumpCh, resultCode := func() (<-chan *ldifEntry, ldapserver.LDAPResultCode) {
|
||||
var pumpCh chan *ldifEntry
|
||||
var start = true
|
||||
if pagingControl != nil {
|
||||
if len(pagingCookie) == 0 {
|
||||
pagingCookie = []byte(base64.RawStdEncoding.EncodeToString(rndm.GenerateRandomBytes(8)))
|
||||
pagingControl.Cookie = pagingCookie
|
||||
pumpCh = make(chan *ldifEntry)
|
||||
h.activeSearchPagings.Set(string(pagingControl.Cookie), pumpCh)
|
||||
logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump start")
|
||||
} else {
|
||||
pumpChRecord, ok := h.activeSearchPagings.Get(string(pagingControl.Cookie))
|
||||
if !ok {
|
||||
return nil, ldap.LDAPResultUnwillingToPerform
|
||||
}
|
||||
if pagingControl.PagingSize > 0 {
|
||||
logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump continue")
|
||||
pumpCh = pumpChRecord.(chan *ldifEntry)
|
||||
start = false
|
||||
} else {
|
||||
// No paging size with cookie, means abandon.
|
||||
start = false
|
||||
logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("search paging pump abandon")
|
||||
// TODO(longsleep): Cancel paging pump context.
|
||||
h.activeSearchPagings.Remove(string(pagingControl.Cookie))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pumpCh = make(chan *ldifEntry)
|
||||
}
|
||||
if start {
|
||||
current := h.load()
|
||||
go h.searchEntriesPump(h.ctx, current, pumpCh, searchReq, pagingControl, indexFilter)
|
||||
}
|
||||
|
||||
return pumpCh, ldap.LDAPResultSuccess
|
||||
}()
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
err := fmt.Errorf("search unable to perform: %d", resultCode)
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, err
|
||||
}
|
||||
|
||||
filterPacket, err := ldapserver.CompileFilter(searchReq.Filter)
|
||||
if err != nil {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: ldap.LDAPResultOperationsError,
|
||||
}, err
|
||||
}
|
||||
|
||||
var entryRecord *ldifEntry
|
||||
var entries []*ldap.Entry
|
||||
var entry *ldap.Entry
|
||||
var count uint32
|
||||
var keep bool
|
||||
results:
|
||||
for {
|
||||
select {
|
||||
case entryRecord = <-pumpCh:
|
||||
if entryRecord == nil {
|
||||
// All done, set cookie to empty.
|
||||
pagingCookie = []byte{}
|
||||
break results
|
||||
|
||||
} else {
|
||||
entry = entryRecord.Entry
|
||||
|
||||
// Apply filter.
|
||||
keep, resultCode = ldapserver.ServerApplyFilter(filterPacket, entry)
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, errors.New("search filter apply error")
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter scope.
|
||||
keep, resultCode = ldapserver.ServerFilterScope(searchReq.BaseDN, searchReq.Scope, entry)
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, errors.New("search scope apply error")
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
|
||||
// Make a copy, before filtering attributes.
|
||||
e := &ldap.Entry{
|
||||
DN: entry.DN,
|
||||
Attributes: make([]*ldap.EntryAttribute, len(entry.Attributes)),
|
||||
}
|
||||
copy(e.Attributes, entry.Attributes)
|
||||
|
||||
// Filter attributes from entry.
|
||||
resultCode, err = ldapserver.ServerFilterAttributes(searchReq.Attributes, e)
|
||||
if err != nil {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Append entry as result.
|
||||
entries = append(entries, e)
|
||||
|
||||
// Count and more.
|
||||
count++
|
||||
if pagingControl != nil {
|
||||
if count >= pagingControl.PagingSize {
|
||||
break results
|
||||
}
|
||||
}
|
||||
if searchReq.SizeLimit > 0 && count >= uint32(searchReq.SizeLimit) {
|
||||
// TODO(longsleep): handle total sizelimit for paging.
|
||||
break results
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pagingControl != nil {
|
||||
doneControls = append(doneControls, &ldap.ControlPaging{
|
||||
PagingSize: 0,
|
||||
Cookie: pagingCookie,
|
||||
})
|
||||
}
|
||||
|
||||
return ldapserver.ServerSearchResult{
|
||||
Entries: entries,
|
||||
Referrals: []string{},
|
||||
Controls: doneControls,
|
||||
ResultCode: ldap.LDAPResultSuccess,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) searchEntriesPump(ctx context.Context, current *ldifMemoryValue, pumpCh chan<- *ldifEntry, searchReq *ldap.SearchRequest, pagingControl *ldap.ControlPaging, indexFilter [][]string) {
|
||||
defer func() {
|
||||
if pagingControl != nil {
|
||||
h.activeSearchPagings.Remove(string(pagingControl.Cookie))
|
||||
close(pumpCh)
|
||||
h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump ended")
|
||||
} else {
|
||||
close(pumpCh)
|
||||
}
|
||||
}()
|
||||
|
||||
pump := func(entryRecord *ldifEntry) bool {
|
||||
select {
|
||||
case pumpCh <- entryRecord:
|
||||
case <-ctx.Done():
|
||||
if pagingControl != nil {
|
||||
h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Warnln("ldap search paging pump context done")
|
||||
} else {
|
||||
h.logger.Warnln("ldap search pump context done")
|
||||
}
|
||||
return false
|
||||
case <-time.After(1 * time.Minute):
|
||||
if pagingControl != nil {
|
||||
h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Warnln("ldap search paging pump timeout")
|
||||
} else {
|
||||
h.logger.Warnln("ldap search pump timeout")
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
searchBaseDN := strings.ToLower(searchReq.BaseDN)
|
||||
|
||||
load := true
|
||||
if len(indexFilter) > 0 {
|
||||
// Get entries with help of index.
|
||||
load = false
|
||||
var results []*[]*ldifEntry
|
||||
for _, f := range indexFilter {
|
||||
indexed, found := current.index.Load(f[0], f[1], f[2:]...)
|
||||
if !found {
|
||||
load = true
|
||||
break
|
||||
}
|
||||
results = append(results, &indexed)
|
||||
}
|
||||
if !load {
|
||||
cache := make(map[*ldifEntry]struct{})
|
||||
for _, indexed := range results {
|
||||
for _, entryRecord := range *indexed {
|
||||
if _, cached := cache[entryRecord]; cached {
|
||||
// Prevent duplicates.
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(entryRecord.DN, searchBaseDN) {
|
||||
if ok := pump(entryRecord); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
cache[entryRecord] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if load {
|
||||
// Walk through all entries (this is slow).
|
||||
h.logger.WithField("filter", searchReq.Filter).Warnln("ldap search filter does not match any index, using slow walk")
|
||||
current.t.WalkSuffix([]byte(searchBaseDN), func(key []byte, entryRecord interface{}) bool {
|
||||
if ok := pump(entryRecord.(*ldifEntry)); !ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ldifHandler) validateBindDN(bindDN string, conn net.Conn) error {
|
||||
if bindDN == "" {
|
||||
if h.allowLocalAnonymousBind {
|
||||
host, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
if net.ParseIP(host).IsLoopback() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("anonymous BindDN rejected")
|
||||
}
|
||||
return fmt.Errorf("anonymous BindDN not allowed")
|
||||
}
|
||||
|
||||
if strings.HasSuffix(bindDN, h.baseDN) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("the BindDN is not in our BaseDN: %s", h.baseDN)
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Close(bindDN string, conn net.Conn) error {
|
||||
h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
}).Debugln("ldap close")
|
||||
|
||||
return nil
|
||||
}
|
||||
225
vendor/github.com/libregraph/idm/server/handler/ldif/index.go
generated
vendored
Normal file
225
vendor/github.com/libregraph/idm/server/handler/ldif/index.go
generated
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/spacewander/go-suffix-tree"
|
||||
)
|
||||
|
||||
var indexAttributes = map[string]string{
|
||||
"entryCSN": "eq",
|
||||
"entryUUID": "eq",
|
||||
"objectClass": "eq",
|
||||
"cn": "pres,eq,sub",
|
||||
"gidNumber": "eq",
|
||||
"mail": "pres,eq,sub",
|
||||
"memberUid": "eq",
|
||||
"ou": "eq",
|
||||
"uid": "eq",
|
||||
"uidNumber": "eq",
|
||||
"uniqueMember": "eq",
|
||||
|
||||
"sn": "pres,eq,sub",
|
||||
"givenName": "pres,eq,sub",
|
||||
|
||||
"mailAlternateAddress": "eq",
|
||||
|
||||
// Additional indexes for attributes usually used with AD.
|
||||
"objectGUID": "eq",
|
||||
"objectSID": "eq",
|
||||
"otherMailbox": "eq",
|
||||
"samAccountName": "eq",
|
||||
}
|
||||
|
||||
func AddIndexAttribute(name string, indices string) {
|
||||
indexAttributes[name] = indices
|
||||
}
|
||||
|
||||
func RemoveIndexAttribute(name string) {
|
||||
delete(indexAttributes, name)
|
||||
}
|
||||
|
||||
type Index interface {
|
||||
Add(name, op string, values []string, entry *ldifEntry) bool
|
||||
Load(name, op string, params ...string) ([]*ldifEntry, bool)
|
||||
}
|
||||
|
||||
type indexMap map[string][]*ldifEntry
|
||||
|
||||
func newIndexMap() indexMap {
|
||||
return make(indexMap)
|
||||
}
|
||||
|
||||
func (im indexMap) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
for _, value := range values {
|
||||
value = strings.ToLower(value)
|
||||
im[value] = append(im[value], entry)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (im indexMap) Load(name, op string, value ...string) ([]*ldifEntry, bool) {
|
||||
entries := im[strings.ToLower(value[0])]
|
||||
return entries, true
|
||||
}
|
||||
|
||||
type indexSuffixTree struct {
|
||||
t *suffix.Tree
|
||||
}
|
||||
|
||||
func newIndexSuffixTree() *indexSuffixTree {
|
||||
return &indexSuffixTree{
|
||||
t: suffix.NewTree(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ist indexSuffixTree) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
for _, value := range values {
|
||||
sfx := []byte(value)
|
||||
var entries []*ldifEntry
|
||||
if v, ok := ist.t.Get(sfx); ok {
|
||||
entries = v.([]*ldifEntry)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
ist.t.Insert(sfx, entries)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ist indexSuffixTree) Load(name, op string, value ...string) ([]*ldifEntry, bool) {
|
||||
var entries []*ldifEntry
|
||||
sfx := []byte(value[0])
|
||||
ist.t.WalkSuffix(sfx, func(key []byte, value interface{}) bool {
|
||||
entries = append(entries, value.([]*ldifEntry)...)
|
||||
return false
|
||||
})
|
||||
return entries, true
|
||||
}
|
||||
|
||||
type indexRadixTree struct {
|
||||
t *radix.Tree
|
||||
}
|
||||
|
||||
func newIndexRadixTree() *indexRadixTree {
|
||||
return &indexRadixTree{
|
||||
t: radix.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (irt *indexRadixTree) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
for _, value := range values {
|
||||
pfx := value
|
||||
var entries []*ldifEntry
|
||||
if v, ok := irt.t.Get(pfx); ok {
|
||||
entries = v.([]*ldifEntry)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
irt.t.Insert(pfx, entries)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (irt *indexRadixTree) Load(name, op string, value ...string) ([]*ldifEntry, bool) {
|
||||
var entries []*ldifEntry
|
||||
pfx := value[0]
|
||||
irt.t.WalkPrefix(pfx, func(key string, value interface{}) bool {
|
||||
entries = append(entries, value.([]*ldifEntry)...)
|
||||
return false
|
||||
})
|
||||
return entries, true
|
||||
}
|
||||
|
||||
type indexSubTree struct {
|
||||
pres indexMap
|
||||
irt *indexRadixTree
|
||||
ist *indexSuffixTree
|
||||
}
|
||||
|
||||
func newIndexSubTree() *indexSubTree {
|
||||
return &indexSubTree{
|
||||
pres: newIndexMap(),
|
||||
irt: newIndexRadixTree(),
|
||||
ist: newIndexSuffixTree(),
|
||||
}
|
||||
}
|
||||
|
||||
func (idx *indexSubTree) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
ok0 := idx.pres.Add(name, op, []string{""}, entry)
|
||||
ok1 := idx.irt.Add(name, op, values, entry)
|
||||
ok2 := idx.ist.Add(name, op, values, entry)
|
||||
return ok0 || ok1 || ok2
|
||||
}
|
||||
|
||||
func (idx *indexSubTree) Load(name, op string, params ...string) ([]*ldifEntry, bool) {
|
||||
if len(params) != 2 {
|
||||
// Require one value and sub tag.
|
||||
return nil, false
|
||||
}
|
||||
tag, err := strconv.ParseInt(params[1], 10, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case ldapserver.FilterSubstringsAny:
|
||||
// TODO(longsleep): Find a suitable way for full text search substring
|
||||
// matching, for example with Knuth-Morris-Pratt algorithm. Currently
|
||||
// we just do a presence match.
|
||||
return idx.pres.Load(name, op, "")
|
||||
case ldapserver.FilterSubstringsInitial:
|
||||
return idx.irt.Load(name, op, params[0])
|
||||
case ldapserver.FilterSubstringsFinal:
|
||||
return idx.ist.Load(name, op, params[0])
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
type indexMapRegister map[string]Index
|
||||
|
||||
func newIndexMapRegister() indexMapRegister {
|
||||
imr := make(indexMapRegister)
|
||||
for name, ops := range indexAttributes {
|
||||
for _, op := range strings.Split(ops, ",") {
|
||||
switch op {
|
||||
case "sub":
|
||||
imr[imr.getKey(name, op)] = newIndexSubTree()
|
||||
case "pres":
|
||||
imr[imr.getKey(name, op)] = newIndexMap()
|
||||
case "eq":
|
||||
imr[imr.getKey(name, op)] = newIndexMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
return imr
|
||||
}
|
||||
|
||||
func (imr indexMapRegister) getKey(name, op string) string {
|
||||
return strings.ToLower(name) + "," + op
|
||||
}
|
||||
|
||||
func (imr indexMapRegister) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
index, ok := imr[imr.getKey(name, op)]
|
||||
if !ok {
|
||||
// No matching index, refuse to add.
|
||||
return false
|
||||
}
|
||||
return index.Add(name, op, values, entry)
|
||||
}
|
||||
|
||||
func (imr indexMapRegister) Load(name, op string, params ...string) ([]*ldifEntry, bool) {
|
||||
index, ok := imr[imr.getKey(name, op)]
|
||||
if !ok {
|
||||
// No such index.
|
||||
return nil, false
|
||||
}
|
||||
return index.Load(name, op, params...)
|
||||
}
|
||||
190
vendor/github.com/libregraph/idm/server/handler/ldif/ldif.go
generated
vendored
Normal file
190
vendor/github.com/libregraph/idm/server/handler/ldif/ldif.go
generated
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/go-ldap/ldif"
|
||||
"github.com/spacewander/go-suffix-tree"
|
||||
)
|
||||
|
||||
// parseLDIFFile opens the named file for reading and parses it as LDIF.
|
||||
func parseLDIFFile(fn string, options *Options) (*ldif.LDIF, error) {
|
||||
f, err := os.Open(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var r io.Reader
|
||||
|
||||
if options.TemplateEngineDisabled {
|
||||
r = f
|
||||
} else {
|
||||
r, err = parseLDIFTemplate(f, options, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return parseLDIF(r, options)
|
||||
}
|
||||
|
||||
// parseLDIFDirectory opens all ldif files in the given path in sorted order,
|
||||
// cats them all together and parses the result as LDIF.
|
||||
func parseLDIFDirectory(pn string, options *Options) (*ldif.LDIF, []error, error) {
|
||||
matches, err := filepath.Glob(filepath.Join(pn, "*.ldif"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
return matches[i] < matches[j]
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
var matchErrors []error
|
||||
for _, match := range matches {
|
||||
err = func() error {
|
||||
f, openErr := os.Open(match)
|
||||
if openErr != nil {
|
||||
matchErrors = append(matchErrors, fmt.Errorf("file read error: %w", openErr))
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
if options.TemplateEngineDisabled {
|
||||
_, copyErr := io.Copy(&buf, f)
|
||||
if copyErr != nil {
|
||||
return fmt.Errorf("file read error: %w", copyErr)
|
||||
}
|
||||
} else {
|
||||
p, parseErr := parseLDIFTemplate(f, options, nil)
|
||||
if parseErr != nil {
|
||||
matchErrors = append(matchErrors, fmt.Errorf("parse error in %s: %w", match, parseErr))
|
||||
return nil
|
||||
}
|
||||
_, copyErr := io.Copy(&buf, p)
|
||||
if copyErr != nil {
|
||||
return fmt.Errorf("template read error: %w", copyErr)
|
||||
}
|
||||
}
|
||||
buf.WriteString("\n\n")
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, matchErrors, err
|
||||
}
|
||||
}
|
||||
|
||||
l, err := parseLDIF(&buf, options)
|
||||
return l, matchErrors, err
|
||||
}
|
||||
|
||||
// parseLDIFTemplate exectues the provided text template and then parses the
|
||||
// result as LDIF.
|
||||
func parseLDIFTemplate(r io.Reader, options *Options, m map[string]interface{}) (io.Reader, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
var text []string
|
||||
for scanner.Scan() {
|
||||
t := scanner.Text()
|
||||
if t != "" && t[0] == '#' {
|
||||
// Ignore commented lines.
|
||||
continue
|
||||
}
|
||||
text = append(text, scanner.Text())
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
m = make(map[string]interface{})
|
||||
}
|
||||
tpl, err := template.New("tpl").Funcs(TemplateFuncs(m, options)).Parse(strings.Join(text, "\n"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse LDIF template: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tpl.Execute(&buf, m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to process LDIF template: %w", err)
|
||||
}
|
||||
|
||||
if options.TemplateDebug {
|
||||
fmt.Println("---\n", buf.String(), "\n----")
|
||||
}
|
||||
|
||||
return &buf, nil
|
||||
}
|
||||
|
||||
func parseLDIF(r io.Reader, options *Options) (*ldif.LDIF, error) {
|
||||
l := &ldif.LDIF{}
|
||||
err := ldif.Unmarshal(r, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// treeFromLDIF makes a tree out of the provided LDIF and if index is not nil,
|
||||
// also indexes each entry in the provided index.
|
||||
func treeFromLDIF(l *ldif.LDIF, index Index, options *Options) (*suffix.Tree, error) {
|
||||
t := suffix.NewTree()
|
||||
|
||||
// NOTE(longsleep): Create in memory tree records from LDIF data.
|
||||
var entry *ldap.Entry
|
||||
for _, entryRecord := range l.Entries {
|
||||
if entryRecord == nil || entryRecord.Entry == nil {
|
||||
// NOTE(longsleep): We don't use l.AllEntries as "nil" records can happen.
|
||||
continue
|
||||
}
|
||||
entry = entryRecord.Entry
|
||||
e := &ldifEntry{
|
||||
Entry: &ldap.Entry{
|
||||
DN: strings.ToLower(entry.DN),
|
||||
},
|
||||
}
|
||||
for _, a := range entry.Attributes {
|
||||
switch strings.ToLower(a.Name) {
|
||||
case "userpassword":
|
||||
// Don't include the password in the normal attributes.
|
||||
e.UserPassword = &ldap.EntryAttribute{
|
||||
Name: a.Name,
|
||||
Values: a.Values,
|
||||
}
|
||||
default:
|
||||
// Append it.
|
||||
e.Entry.Attributes = append(e.Entry.Attributes, &ldap.EntryAttribute{
|
||||
Name: a.Name,
|
||||
Values: a.Values,
|
||||
})
|
||||
}
|
||||
if index != nil {
|
||||
// Index equalityMatch.
|
||||
index.Add(a.Name, "eq", a.Values, e)
|
||||
// Index present.
|
||||
index.Add(a.Name, "pres", []string{""}, e)
|
||||
// Index substrings.
|
||||
index.Add(a.Name, "sub", a.Values, e)
|
||||
}
|
||||
}
|
||||
v, ok := t.Insert([]byte(e.DN), e)
|
||||
if !ok || v != nil {
|
||||
return nil, fmt.Errorf("duplicate dn value: %s", e.DN)
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
18
vendor/github.com/libregraph/idm/server/handler/ldif/memory.go
generated
vendored
Normal file
18
vendor/github.com/libregraph/idm/server/handler/ldif/memory.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"github.com/go-ldap/ldif"
|
||||
"github.com/spacewander/go-suffix-tree"
|
||||
)
|
||||
|
||||
type ldifMemoryValue struct {
|
||||
l *ldif.LDIF
|
||||
t *suffix.Tree
|
||||
|
||||
index Index
|
||||
}
|
||||
190
vendor/github.com/libregraph/idm/server/handler/ldif/middleware.go
generated
vendored
Normal file
190
vendor/github.com/libregraph/idm/server/handler/ldif/middleware.go
generated
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
)
|
||||
|
||||
type ldifMiddleware struct {
|
||||
logger logrus.FieldLogger
|
||||
fn string
|
||||
options *Options
|
||||
|
||||
baseDN string
|
||||
|
||||
current atomic.Value
|
||||
|
||||
next handler.Handler
|
||||
}
|
||||
|
||||
var _ handler.Handler = (*ldifMiddleware)(nil) // Verify that *configHandler implements handler.Handler.
|
||||
|
||||
func NewLDIFMiddleware(logger logrus.FieldLogger, fn string, options *Options) (handler.Middleware, error) {
|
||||
if fn == "" {
|
||||
return nil, fmt.Errorf("file name is empty")
|
||||
}
|
||||
if options.BaseDN == "" {
|
||||
return nil, fmt.Errorf("base dn is empty")
|
||||
}
|
||||
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger = logger.WithField("fn", fn)
|
||||
|
||||
h := &ldifMiddleware{
|
||||
logger: logger,
|
||||
fn: fn,
|
||||
options: options,
|
||||
|
||||
baseDN: strings.ToLower(options.BaseDN),
|
||||
}
|
||||
|
||||
err = h.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) open() error {
|
||||
if !strings.EqualFold(h.options.BaseDN, h.baseDN) {
|
||||
return fmt.Errorf("mismatched BaseDN")
|
||||
}
|
||||
|
||||
h.logger.Debugln("loading LDIF")
|
||||
l, err := parseLDIFFile(h.fn, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, err := treeFromLDIF(l, nil, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store parsed data as memory value.
|
||||
value := &ldifMemoryValue{
|
||||
t: t,
|
||||
}
|
||||
h.current.Store(value)
|
||||
|
||||
h.logger.WithFields(logrus.Fields{
|
||||
"version": l.Version,
|
||||
"entries_count": len(l.Entries),
|
||||
"tree_length": t.Len(),
|
||||
"base_dn": h.options.BaseDN,
|
||||
}).Debugln("loaded LDIF")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) load() *ldifMemoryValue {
|
||||
value := h.current.Load()
|
||||
return value.(*ldifMemoryValue)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) WithHandler(next handler.Handler) handler.Handler {
|
||||
h.next = next
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) WithContext(ctx context.Context) handler.Handler {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
|
||||
h2 := new(ldifMiddleware)
|
||||
*h2 = *h
|
||||
h2.next = h.next.WithContext(ctx)
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Reload(ctx context.Context) error {
|
||||
err := h.open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.next.Reload(ctx)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Add(_ string, _ *ldap.AddRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldapserver.LDAPResultCode, err error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
|
||||
if bindSimplePw == "" { // Empty password means anonymous bind.
|
||||
return h.next.Bind(bindDN, bindSimplePw, conn)
|
||||
}
|
||||
|
||||
current := h.load()
|
||||
|
||||
entryRecord, found := current.t.Get([]byte(bindDN))
|
||||
if found {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if !strings.HasSuffix(bindDN, h.baseDN) {
|
||||
err := fmt.Errorf("the BindDN is not in our BaseDN %s", h.baseDN)
|
||||
logger.WithError(err).Infoln("ldap bind error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
if err := entryRecord.(*ldifEntry).validatePassword(bindSimplePw); err != nil {
|
||||
logger.WithError(err).Infoln("bind error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
return h.next.Bind(bindDN, bindSimplePw, conn)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Delete(_ string, _ *ldap.DelRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Modify(_ string, _ *ldap.ModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) ModifyDN(_ string, _ *ldap.ModifyDNRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) ModifyPasswordExop(_ string, _ *ldap.PasswordModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Search(bindDN string, searchReq *ldap.SearchRequest, conn net.Conn) (result ldapserver.ServerSearchResult, err error) {
|
||||
return h.next.Search(bindDN, searchReq, conn)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Close(bindDN string, conn net.Conn) error {
|
||||
return h.next.Close(bindDN, conn)
|
||||
}
|
||||
21
vendor/github.com/libregraph/idm/server/handler/ldif/options.go
generated
vendored
Normal file
21
vendor/github.com/libregraph/idm/server/handler/ldif/options.go
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
type Options struct {
|
||||
BaseDN string
|
||||
AdminDN string
|
||||
AllowLocalAnonymousBind bool
|
||||
|
||||
DefaultCompany string
|
||||
DefaultMailDomain string
|
||||
|
||||
TemplateExtraVars map[string]interface{}
|
||||
TemplateEngineDisabled bool
|
||||
TemplateDebug bool
|
||||
|
||||
templateBasePath string
|
||||
}
|
||||
119
vendor/github.com/libregraph/idm/server/handler/ldif/template.go
generated
vendored
Normal file
119
vendor/github.com/libregraph/idm/server/handler/ldif/template.go
generated
vendored
Normal 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
125
vendor/github.com/libregraph/idm/server/metrics.go
generated
vendored
Normal 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
254
vendor/github.com/libregraph/idm/server/server.go
generated
vendored
Normal 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
7
vendor/github.com/libregraph/lico/.dependabot.yml
generated
vendored
Normal 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
32
vendor/github.com/libregraph/lico/.editorconfig
generated
vendored
Normal 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
26
vendor/github.com/libregraph/lico/.gitignore
generated
vendored
Normal 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
996
vendor/github.com/libregraph/lico/CHANGELOG.md
generated
vendored
Normal 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
53
vendor/github.com/libregraph/lico/Caddyfile.dev
generated
vendored
Normal 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
24
vendor/github.com/libregraph/lico/Caddyfile.example
generated
vendored
Normal 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
59
vendor/github.com/libregraph/lico/Dockerfile.build
generated
vendored
Normal 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
202
vendor/github.com/libregraph/lico/LICENSE.txt
generated
vendored
Normal 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
214
vendor/github.com/libregraph/lico/Makefile
generated
vendored
Normal 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
5
vendor/github.com/libregraph/lico/NOTICE.txt
generated
vendored
Normal 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
256
vendor/github.com/libregraph/lico/README.md
generated
vendored
Normal 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.
|
||||
|
||||
[](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.
|
||||
53
vendor/github.com/libregraph/lico/bootstrap/backends/guest/guest.go
generated
vendored
Normal file
53
vendor/github.com/libregraph/lico/bootstrap/backends/guest/guest.go
generated
vendored
Normal 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
|
||||
}
|
||||
168
vendor/github.com/libregraph/lico/bootstrap/backends/ldap/ldap.go
generated
vendored
Normal file
168
vendor/github.com/libregraph/lico/bootstrap/backends/ldap/ldap.go
generated
vendored
Normal 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
|
||||
}
|
||||
151
vendor/github.com/libregraph/lico/bootstrap/backends/libregraph/libregraph.go
generated
vendored
Normal file
151
vendor/github.com/libregraph/lico/bootstrap/backends/libregraph/libregraph.go
generated
vendored
Normal 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
|
||||
}
|
||||
541
vendor/github.com/libregraph/lico/bootstrap/bootstrap.go
generated
vendored
Normal file
541
vendor/github.com/libregraph/lico/bootstrap/bootstrap.go
generated
vendored
Normal 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
67
vendor/github.com/libregraph/lico/bootstrap/config.go
generated
vendored
Normal 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
|
||||
}
|
||||
88
vendor/github.com/libregraph/lico/bootstrap/managers.go
generated
vendored
Normal file
88
vendor/github.com/libregraph/lico/bootstrap/managers.go
generated
vendored
Normal 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
|
||||
}
|
||||
55
vendor/github.com/libregraph/lico/bootstrap/settings.go
generated
vendored
Normal file
55
vendor/github.com/libregraph/lico/bootstrap/settings.go
generated
vendored
Normal 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
410
vendor/github.com/libregraph/lico/bootstrap/utils.go
generated
vendored
Normal 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
169
vendor/github.com/libregraph/lico/claims.go
generated
vendored
Normal 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
42
vendor/github.com/libregraph/lico/config/config.go
generated
vendored
Normal 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
44
vendor/github.com/libregraph/lico/context.go
generated
vendored
Normal 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
22
vendor/github.com/libregraph/lico/doc.go
generated
vendored
Normal 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"
|
||||
64
vendor/github.com/libregraph/lico/encryption/encryption.go
generated
vendored
Normal file
64
vendor/github.com/libregraph/lico/encryption/encryption.go
generated
vendored
Normal 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
|
||||
}
|
||||
45
vendor/github.com/libregraph/lico/encryption/generate.go
generated
vendored
Normal file
45
vendor/github.com/libregraph/lico/encryption/generate.go
generated
vendored
Normal 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
|
||||
}
|
||||
96
vendor/github.com/libregraph/lico/identifier-registration.yaml.in
generated
vendored
Normal file
96
vendor/github.com/libregraph/lico/identifier-registration.yaml.in
generated
vendored
Normal 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
9
vendor/github.com/libregraph/lico/identifier/.env
generated
vendored
Normal 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
|
||||
38
vendor/github.com/libregraph/lico/identifier/.gitignore
generated
vendored
Normal file
38
vendor/github.com/libregraph/lico/identifier/.gitignore
generated
vendored
Normal 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
|
||||
7
vendor/github.com/libregraph/lico/identifier/.yarnrc.yml
generated
vendored
Normal file
7
vendor/github.com/libregraph/lico/identifier/.yarnrc.yml
generated
vendored
Normal 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
65
vendor/github.com/libregraph/lico/identifier/Makefile
generated
vendored
Normal 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)
|
||||
3
vendor/github.com/libregraph/lico/identifier/README.md
generated
vendored
Normal file
3
vendor/github.com/libregraph/lico/identifier/README.md
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# LibreGraph Connect Identifier
|
||||
|
||||
Web app for browser sign-in, sign-out and account management.
|
||||
2164
vendor/github.com/libregraph/lico/identifier/README.react.md
generated
vendored
Normal file
2164
vendor/github.com/libregraph/lico/identifier/README.react.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
145
vendor/github.com/libregraph/lico/identifier/api.go
generated
vendored
Normal file
145
vendor/github.com/libregraph/lico/identifier/api.go
generated
vendored
Normal 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
|
||||
}
|
||||
54
vendor/github.com/libregraph/lico/identifier/backends/backend.go
generated
vendored
Normal file
54
vendor/github.com/libregraph/lico/identifier/backends/backend.go
generated
vendored
Normal 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
|
||||
}
|
||||
41
vendor/github.com/libregraph/lico/identifier/backends/ldap/const.go
generated
vendored
Normal file
41
vendor/github.com/libregraph/lico/identifier/backends/ldap/const.go
generated
vendored
Normal 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"
|
||||
)
|
||||
670
vendor/github.com/libregraph/lico/identifier/backends/ldap/ldap.go
generated
vendored
Normal file
670
vendor/github.com/libregraph/lico/identifier/backends/ldap/ldap.go
generated
vendored
Normal 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)
|
||||
}
|
||||
560
vendor/github.com/libregraph/lico/identifier/backends/libregraph/libregraph.go
generated
vendored
Normal file
560
vendor/github.com/libregraph/lico/identifier/backends/libregraph/libregraph.go
generated
vendored
Normal 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
31
vendor/github.com/libregraph/lico/identifier/claims.go
generated
vendored
Normal 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
48
vendor/github.com/libregraph/lico/identifier/config.go
generated
vendored
Normal 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
|
||||
}
|
||||
50
vendor/github.com/libregraph/lico/identifier/context.go
generated
vendored
Normal file
50
vendor/github.com/libregraph/lico/identifier/context.go
generated
vendored
Normal 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
202
vendor/github.com/libregraph/lico/identifier/cookie.go
generated
vendored
Normal 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
27
vendor/github.com/libregraph/lico/identifier/flows.go
generated
vendored
Normal 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"
|
||||
)
|
||||
516
vendor/github.com/libregraph/lico/identifier/handlers.go
generated
vendored
Normal file
516
vendor/github.com/libregraph/lico/identifier/handlers.go
generated
vendored
Normal 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")
|
||||
}
|
||||
}
|
||||
761
vendor/github.com/libregraph/lico/identifier/identifier.go
generated
vendored
Normal file
761
vendor/github.com/libregraph/lico/identifier/identifier.go
generated
vendored
Normal 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
|
||||
}
|
||||
26
vendor/github.com/libregraph/lico/identifier/meta/branding.go
generated
vendored
Normal file
26
vendor/github.com/libregraph/lico/identifier/meta/branding.go
generated
vendored
Normal 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"`
|
||||
}
|
||||
28
vendor/github.com/libregraph/lico/identifier/meta/meta.go
generated
vendored
Normal file
28
vendor/github.com/libregraph/lico/identifier/meta/meta.go
generated
vendored
Normal 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"`
|
||||
}
|
||||
25
vendor/github.com/libregraph/lico/identifier/meta/scopes/definition.go
generated
vendored
Normal file
25
vendor/github.com/libregraph/lico/identifier/meta/scopes/definition.go
generated
vendored
Normal 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"`
|
||||
}
|
||||
161
vendor/github.com/libregraph/lico/identifier/meta/scopes/scopes.go
generated
vendored
Normal file
161
vendor/github.com/libregraph/lico/identifier/meta/scopes/scopes.go
generated
vendored
Normal 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
178
vendor/github.com/libregraph/lico/identifier/models.go
generated
vendored
Normal 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
41
vendor/github.com/libregraph/lico/identifier/modes.go
generated
vendored
Normal 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
396
vendor/github.com/libregraph/lico/identifier/oauth2.go
generated
vendored
Normal 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)
|
||||
}
|
||||
108
vendor/github.com/libregraph/lico/identifier/package.json
generated
vendored
Normal file
108
vendor/github.com/libregraph/lico/identifier/package.json
generated
vendored
Normal 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
464
vendor/github.com/libregraph/lico/identifier/saml2.go
generated
vendored
Normal 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
Reference in New Issue
Block a user