Initial QSfera import
This commit is contained in:
+54
@@ -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
@@ -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
@@ -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.GetEqualFoldAttributeValue(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.GetEqualFoldAttributeValue(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.GetEqualFoldAttributeValues(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, ldap.EscapeFilter(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, ldap.EscapeFilter(username))
|
||||
}
|
||||
Generated
Vendored
+660
@@ -0,0 +1,660 @@
|
||||
/*
|
||||
* 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"
|
||||
apiPathRevokeSignInSession = "/api/v1/me/revokeSignInSession"
|
||||
)
|
||||
|
||||
var libreGraphSpportedScopes = []string{
|
||||
oidc.ScopeProfile,
|
||||
oidc.ScopeEmail,
|
||||
konnect.ScopeUniqueUserID,
|
||||
konnect.ScopeRawSubject,
|
||||
}
|
||||
|
||||
type LibreGraphIdentifierBackend struct {
|
||||
config *config.Config
|
||||
|
||||
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[konnect.InternalExtraAccessTokenClaimsClaim].(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{
|
||||
config: c,
|
||||
|
||||
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 {
|
||||
record, _ = identifier.RecordFromRequestContext(ctx, b.config)
|
||||
}
|
||||
if record != nil {
|
||||
// Inject HTTP headers.
|
||||
if record.HelloRequest != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
if record.RealIP != "" {
|
||||
req.Header.Set("X-User-Real-IP", record.RealIP)
|
||||
}
|
||||
if record.UserAgent != "" {
|
||||
req.Header.Set("X-User-Real-User-Agent", record.UserAgent)
|
||||
}
|
||||
}
|
||||
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.BackendUser = 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.BackendUser != nil {
|
||||
if user, ok := record.BackendUser.(*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)
|
||||
}
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
record, _ = identifier.RecordFromRequestContext(ctx, b.config)
|
||||
}
|
||||
if record != nil {
|
||||
if record.IdentifiedUser != nil {
|
||||
if ok, ts := record.IdentifiedUser.LoggedOn(); ok {
|
||||
req.Header.Set("X-User-Logon-At", ts.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
}
|
||||
if record.RealIP != "" {
|
||||
req.Header.Set("X-User-Real-IP", record.RealIP)
|
||||
}
|
||||
if record.UserAgent != "" {
|
||||
req.Header.Set("X-User-Real-User-Agent", record.UserAgent)
|
||||
}
|
||||
}
|
||||
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 Backend 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 {
|
||||
user, err := b.GetUser(ctx, userID, sessionRef, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return fmt.Errorf("refresh session did not yield a user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DestroySession implements the Backend interface providing explicit revoke
|
||||
// of the backend session.
|
||||
func (b *LibreGraphIdentifierBackend) DestroySession(ctx context.Context, sessionRef *string) error {
|
||||
var requestedScopes map[string]bool
|
||||
record, _ := identifier.FromRecordContext(ctx)
|
||||
if record != nil {
|
||||
if record.HelloRequest != nil {
|
||||
requestedScopes = record.HelloRequest.Scopes
|
||||
}
|
||||
}
|
||||
|
||||
_, revokeSessionURL := b.getRevokeSigninSessionURL(requestedScopes)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, revokeSessionURL, http.NoBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("libregraph identifier backend destroy session request error: %w", err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
record, _ = identifier.RecordFromRequestContext(ctx, b.config)
|
||||
}
|
||||
if record != nil {
|
||||
// Inject HTTP headers.
|
||||
if record.RealIP != "" {
|
||||
req.Header.Set("X-User-Real-IP", record.RealIP)
|
||||
}
|
||||
if record.UserAgent != "" {
|
||||
req.Header.Set("X-User-Real-User-Agent", record.UserAgent)
|
||||
}
|
||||
}
|
||||
req.Header.Set("User-Agent", utils.DefaultHTTPUserAgent)
|
||||
|
||||
response, err := b.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("libregraph identifier backend destroy session request failed: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusOK:
|
||||
// breaks
|
||||
case http.StatusNotFound:
|
||||
return nil
|
||||
case http.StatusUnauthorized:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("libregraph identifier backend logon request unexpected response status: %d", response.StatusCode)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (b *LibreGraphIdentifierBackend) getRevokeSigninSessionURL(requestedScopes map[string]bool) (string, string) {
|
||||
scope, baseURL := b.getBaseURL(requestedScopes)
|
||||
|
||||
return scope, baseURL + apiPathRevokeSignInSession
|
||||
}
|
||||
Reference in New Issue
Block a user