Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy auth #62

Closed
wants to merge 10 commits into from
1 change: 1 addition & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Client interface {

Compare(dn, attribute, value string) (bool, error)
PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error)
WhoAmI(controls []Control) (*WhoAmIResult, error)

Search(searchRequest *SearchRequest) (*SearchResult, error)
SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error)
Expand Down
40 changes: 40 additions & 0 deletions control.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ const (
ControlTypeVChuPasswordWarning = "2.16.840.1.113730.3.4.5"
// ControlTypeManageDsaIT - https://tools.ietf.org/html/rfc3296
ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2"
// ControlTypeProxiedAuthorization - https://tools.ietf.org/html/rfc4370
ControlTypeProxiedAuthorization = "2.16.840.1.113730.3.4.18"
)

// ControlTypeMap maps controls to text descriptions
var ControlTypeMap = map[string]string{
ControlTypePaging: "Paging",
ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft",
ControlTypeManageDsaIT: "Manage DSA IT",
ControlTypeProxiedAuthorization: "Proxied Authorization",
}

// Control defines an interface controls provide to encode and describe themselves
Expand Down Expand Up @@ -242,6 +245,43 @@ func NewControlManageDsaIT(Criticality bool) *ControlManageDsaIT {
return &ControlManageDsaIT{Criticality: Criticality}
}

type ControlProxiedAuthorization struct {
Criticality bool
AuthzId string
}

// GetControlType returns the OID
func (c *ControlProxiedAuthorization) GetControlType() string {
return ControlTypeProxiedAuthorization
}

// Encode returns the ber packet representation
func (c *ControlProxiedAuthorization) Encode() *ber.Packet {
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control")
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeProxiedAuthorization, "Control Type ("+ControlTypeMap[ControlTypeProxiedAuthorization]+")"))
packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality"))
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.AuthzId, "AuthzId"))
return packet
}

// String returns a human-readable description
func (c *ControlProxiedAuthorization) String() string {
return fmt.Sprintf(
"Control Type: %s (%q) Criticality: %t",
ControlTypeMap[ControlTypeProxiedAuthorization],
ControlTypeProxiedAuthorization,
c.Criticality)
}

// NewControlProxiedAuthoization returns a new ProxiedAuthorization
// control for the given authzId which is usually a DN
func NewControlProxiedAuthoization(authzId string) *ControlProxiedAuthorization {
return &ControlProxiedAuthorization{
Criticality: true,
AuthzId: authzId,
}
}

// FindControl returns the first control of the given type in the list, or nil
func FindControl(controls []Control, controlType string) Control {
for _, c := range controls {
Expand Down
216 changes: 216 additions & 0 deletions dn_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package ldap

import (
enchex "encoding/hex"
"errors"
"strings"
)

// When true, uses strings.EqualFold to compare the values of an RDN.
// This is usually needed as true for most values, set to false when
// your DNs contain case sensitive values.
var RDNCompareFold bool = true

var ErrDNNotSubordinate = errors.New("Not a subordinate")

// Returns the stringified version of a *DN, the RDN values are escaped
func (dn *DN) String() string {
var rdns []string
for _, r := range dn.RDNs {
var tv []string
for _, av := range r.Attributes {
tv = append(tv, strings.ToLower(av.Type)+"="+EscapeValue(av.Value))
}
rdns = append(rdns, strings.Join(tv, "+"))
}
return strings.Join(rdns, ",")
}

func EscapeValue(value string) (escaped string) {
for _, r := range value {
switch r {
case ',', '+', '"', '\\', '<', '>', ';', '#', '=':
escaped += "\\" + string(r)
default:
if uint(r) < 32 {
escaped += "\\" + enchex.EncodeToString([]byte(string(r)))
} else {
escaped += string(r)
}
}
}
return
}

// check if all RDNs of both DNs are equal
func (dn *DN) Equal(other *DN) bool {
if len(dn.RDNs) != len(other.RDNs) {
return false
}
for i, rdn := range dn.RDNs {
if !rdn.Equal(other.RDNs[i]) {
return false
}
}
return true
}

// Check if all types and values of both RDNs are equal, the result may be
// influenced by the value of RDNCompareFold.
func (r *RelativeDN) Equal(o *RelativeDN) bool {
if len(r.Attributes) != len(o.Attributes) {
return false
}
for i, av := range r.Attributes {
if strings.ToLower(av.Type) != strings.ToLower(o.Attributes[i].Type) {
return false
}
if RDNCompareFold {
if !strings.EqualFold(av.Value, o.Attributes[i].Value) {
return false
}
} else {
if av.Value != o.Attributes[i].Value {
return false
}
}
}
return true
}

// Returns true if the "other" DN is a parent of "dn"
func (dn *DN) IsSubordinate(other *DN) bool {
if off := len(dn.RDNs) - len(other.RDNs); off <= 0 {
return false
} else {
for i, rdn := range other.RDNs {
if !rdn.Equal(dn.RDNs[i+off]) {
return false
}
}
}
return true
}

// appends the "other" DN to the "dn", e.g.
//
// dn, err := ldap.ParseDN("CN=Someone")
// base, err := ldap.ParseDN("ou=people,dc=example,dc=org")
// dn.Append(base) -> "cn=Someone,ou=people,dc=example,dc=org"
func (dn *DN) Append(other *DN) {
dn.RDNs = append(dn.RDNs, other.RDNs...)
}

// removes the "other" DN from the "dn", e.g.
//
// dn, err := ldap.ParseDN(""cn=Someone,ou=people,dc=example,dc=org")
// base, err := ldap.ParseDN("ou=people,dc=example,dc=org")
// dn.Strip(base) -> "cn=Someone"
//
// Note: the "other" DN must be a parent of the "dn"
func (dn *DN) Strip(base *DN) error {
if !dn.IsSubordinate(base) {
return ErrDNNotSubordinate
}
dn.RDNs = dn.RDNs[0 : len(dn.RDNs)-len(base.RDNs)]
return nil
}

// Changes the first RDN of DN to the given one
func (dn *DN) Rename(rdn *RelativeDN) {
dn.RDNs[0] = rdn
}

// Moves the first RDN to the new base
func (dn *DN) Move(newBase *DN) {
dn.RDNs = dn.RDNs[:1]
dn.Append(newBase)
}

// Returns the value of the first RDN, e.g.
//
// dn, err := ldap.ParseDN("uid=someone,ou=people,dc=example,dc=org")
// dn.RDN() -> "someone"
func (dn *DN) RDN() string {
if len(dn.RDNs) == 0 || len(dn.RDNs[0].Attributes) == 0 {
return ""
}
return dn.RDNs[0].Attributes[0].Value
}

// Returns the parent of the "dn" as a cloned *DN
func (dn *DN) Parent() *DN {
c := dn.Clone()
if len(c.RDNs) > 0 {
c.RDNs = c.RDNs[1:]
return c
}
c.RDNs = []*RelativeDN{}
return c
}

// Returns a clone of the DN
func (dn *DN) Clone() *DN {
c := &DN{}
for _, r := range dn.RDNs {
rc := &RelativeDN{}
for _, tv := range r.Attributes {
rc.Attributes = append(rc.Attributes, &AttributeTypeAndValue{Type: tv.Type, Value: tv.Value})
}
c.RDNs = append(c.RDNs, rc)
}
return c
}

// Sorting DNs:
// all := []*ldap.DN{dn1, dn2, dn3, dn4}
// sort.Sort(DNs(all))
// for _, dn := range all {
// println(dn.String())
// }
//
// The result order from deepest part in tree upwards, so you could
// easily search for all dns in a base, sort them and then remove
// every DN in that order to remove the tree (including the search base)
type DNs []*DN

func (d DNs) Len() int {
return len(([]*DN)(d))
}

func (d DNs) Swap(i, j int) {
([]*DN)(d)[i], ([]*DN)(d)[j] = ([]*DN)(d)[j], ([]*DN)(d)[i]
}

func (d DNs) Less(i, j int) bool {
if ([]*DN)(d)[i].IsSubordinate(([]*DN)(d)[j]) {
return true
}
if ([]*DN)(d)[i].Parent().Equal(([]*DN)(d)[j].Parent()) {
return ([]*DN)(d)[i].RDNs[0].Less(([]*DN)(d)[j].RDNs[0])
}
return false
}

func (r *RelativeDN) Less(o *RelativeDN) bool {
if len(r.Attributes) != len(o.Attributes) {
return len(r.Attributes) < len(o.Attributes)
}
for i, a := range r.Attributes {
if strings.ToLower(a.Type) < strings.ToLower(o.Attributes[i].Type) {
return true
}
if RDNCompareFold {
if strings.ToLower(a.Value) < strings.ToLower(o.Attributes[i].Value) {
return true
}
} else {
if a.Value < o.Attributes[i].Value {
return true
}
}
}
return false
}

// vim: ts=4 sw=4 noexpandtab
93 changes: 93 additions & 0 deletions dn_util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package ldap_test

import (
"fmt"
"gopkg.in/ldap.v2"
"sort"
"testing"
)

func TestDNString(t *testing.T) {
fmt.Printf("DNString: starting...\n")
dn, _ := ldap.ParseDN("OU=Sales+CN=J. Smith,DC=example,DC=net")
strdn := dn.String()
if strdn != "ou=Sales+cn=J. Smith,dc=example,dc=net" {
t.Errorf("Failed to stringify: %v\n", strdn)
}
fmt.Printf("DNString: -> %v\n", strdn)
dn2, _ := ldap.ParseDN("CN=Lučić\\+Ma\\=><foo")
if _, err := ldap.ParseDN(dn2.String()); err != nil {
t.Errorf("Failed to parse stringified DN: %s", err)
}
}

func TestDNParent(t *testing.T) {
fmt.Printf("DN Parent: starting...\n")
dn, _ := ldap.ParseDN("OU=Sales+CN=J. Smith,DC=example,DC=net")
parent := dn.Parent()
if dn.String() != "ou=Sales+cn=J. Smith,dc=example,dc=net" {
t.Errorf("original dn modified -> %s\n", dn)
}
if parent.String() != "dc=example,dc=net" {
t.Errorf("wrong parent -> %s\n", parent)
}
fmt.Printf("DN Parent: %s -> %s\n", dn, parent)
}

func TestDNMove(t *testing.T) {
fmt.Printf("DN Rename and Move: starting...\n")
dn, _ := ldap.ParseDN("OU=Sales+CN=J. Smith,DC=example,DC=net")
base, _ := ldap.ParseDN("OU=People,DC=example,DC=net")
rdn, _ := ldap.ParseDN("cn=J. Smith")
dn.Move(base)
if dn.String() != "ou=Sales+cn=J. Smith,ou=People,dc=example,dc=net" {
t.Errorf("Failed to move: %s\n", dn)
}
dn.Rename(rdn.RDNs[0])
if dn.String() != "cn=J. Smith,ou=People,dc=example,dc=net" {
t.Errorf("Failed to rename: %s\n", dn)
}
fmt.Printf("DN Rename and Move: %s\n", dn)
}

func TestDNEqual(t *testing.T) {
dn1, _ := ldap.ParseDN("OU=people,DC=example,DC=org")
dn2, _ := ldap.ParseDN("ou=People,dc=Example,dc=ORG")
ldap.RDNCompareFold = true
if !dn1.Equal(dn2) {
t.Errorf("both dns not equal")
}
ldap.RDNCompareFold = false
if dn1.Equal(dn2) {
t.Errorf("both dns equal with ldap.RDNCompareFold = false")
}
ldap.RDNCompareFold = true
}

func TestDNSort(t *testing.T) {
var dns []*ldap.DN
dnStrings := []string{
"ou=people,dc=example,dc=org",
"uid=another,ou=people,dc=example,dc=org",
"uid=another+cn=one,ou=people,dc=example,dc=org",
"dc=example,dc=org",
"uid=someone,ou=people,dc=example,dc=org",
"ou=robots,dc=example,dc=org",
"uid=someone,ou=robots,dc=example,dc=org",
}

for _, s := range dnStrings {
dn, _ := ldap.ParseDN(s)
dns = append(dns, dn)
}
sort.Sort(ldap.DNs(dns))
for _, dn := range dns {
fmt.Printf("DN: %s\n", dn.String())
}
if dns[len(dns)-1].String() != "dc=example,dc=org" {
t.Errorf("DN dc=example,dc=org is not last")
}
if dns[0].String() != "uid=another,ou=people,dc=example,dc=org" {
t.Errorf("DN uid=another,ou=people,dc=example,dc=org is not first")
}
}
6 changes: 6 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ const (
LDAPResultObjectClassModsProhibited = 69
LDAPResultAffectsMultipleDSAs = 71
LDAPResultOther = 80
// https://tools.ietf.org/html/rfc4370 chap 6:
// "A result code (123) has been assigned by the IANA for the case where
// the server does not execute a request using the proxy authorization
// identity."
LDAPResultAuthorizationDenied = 123

ErrorNetwork = 200
ErrorFilterCompile = 201
Expand Down Expand Up @@ -96,6 +101,7 @@ var LDAPResultCodeMap = map[uint8]string{
LDAPResultEntryAlreadyExists: "Entry Already Exists",
LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited",
LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs",
LDAPResultAuthorizationDenied: "Authorization Denied",
LDAPResultOther: "Other",
}

Expand Down
Loading