From 0c277e9cdfbf91dde5de607434642a3fc5961d7f Mon Sep 17 00:00:00 2001 From: Hanno Hecker Date: Sun, 17 Jul 2016 16:38:26 +0200 Subject: [PATCH 1/2] Add Proxied Authorization (RFC 4370), Who Am I? (RFC 4532) --- client.go | 1 + control.go | 41 +++++++++++++++++++++ error.go | 6 ++++ whoami.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ whoami_test.go | 52 +++++++++++++++++++++++++++ 5 files changed, 197 insertions(+) create mode 100644 whoami.go create mode 100644 whoami_test.go diff --git a/client.go b/client.go index 055b27b5..be178a95 100644 --- a/client.go +++ b/client.go @@ -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) diff --git a/control.go b/control.go index 4bd84c99..7a66579c 100644 --- a/control.go +++ b/control.go @@ -22,6 +22,8 @@ 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 @@ -29,6 +31,7 @@ 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 @@ -242,6 +245,44 @@ func NewControlManageDsaIT(Criticality bool) *ControlManageDsaIT { return &ControlManageDsaIT{Criticality: Criticality} } +// ControlProxiedAuthorization implements the control described in +// https://tools.ietf.org/html/rfc4370 +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 ProxiedAuthoization control +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 { diff --git a/error.go b/error.go index ff697873..02eb39c5 100644 --- a/error.go +++ b/error.go @@ -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 @@ -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", } diff --git a/whoami.go b/whoami.go new file mode 100644 index 00000000..bba3d6ea --- /dev/null +++ b/whoami.go @@ -0,0 +1,97 @@ +// This file contains the "Who Am I?" extended operation as specified in rfc 4532 +// +// https://tools.ietf.org/html/rfc4532 +// + +package ldap + +import ( + "errors" + "fmt" + + "gopkg.in/asn1-ber.v1" +) + +const ( + whoamiOID = "1.3.6.1.4.1.4203.1.11.3" +) + +type whoAmIRequest bool + +// WhoAmIResult is returned by the WhoAmI() call +type WhoAmIResult struct { + AuthzID string +} + +func (r whoAmIRequest) encode() (*ber.Packet, error) { + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Who Am I? Extended Operation") + request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, whoamiOID, "Extended Request Name: Who Am I? OID")) + return request, nil +} + +// WhoAmI returns the authzId the server thinks we are, you may pass controls +// like a Proxied Authorization control +func (l *Conn) WhoAmI(controls []Control) (*WhoAmIResult, error) { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + req := whoAmIRequest(true) + encodedWhoAmIRequest, err := req.encode() + if err != nil { + return nil, err + } + packet.AppendChild(encodedWhoAmIRequest) + + if len(controls) != 0 { + packet.AppendChild(encodeControls(controls)) + } + + l.Debug.PrintPacket(packet) + + msgCtx, err := l.sendMessage(packet) + if err != nil { + return nil, err + } + defer l.finishMessage(msgCtx) + + result := &WhoAmIResult{} + + l.Debug.Printf("%d: waiting for response", msgCtx.id) + packetResponse, ok := <-msgCtx.responses + if !ok { + return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return nil, err + } + + if packet == nil { + return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return nil, err + } + ber.PrintPacket(packet) + } + + if packet.Children[1].Tag == ApplicationExtendedResponse { + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return nil, NewError(resultCode, errors.New(resultDescription)) + } + } else { + return nil, NewError(ErrorUnexpectedResponse, fmt.Errorf("Unexpected Response: %d", packet.Children[1].Tag)) + } + + extendedResponse := packet.Children[1] + for _, child := range extendedResponse.Children { + if child.Tag == 11 { + result.AuthzID = ber.DecodeString(child.Data.Bytes()) + } + } + + return result, nil +} diff --git a/whoami_test.go b/whoami_test.go new file mode 100644 index 00000000..ffe2dc26 --- /dev/null +++ b/whoami_test.go @@ -0,0 +1,52 @@ +package ldap_test + +import ( + "fmt" + "log" + + "gopkg.in/ldap.v2" +) + +func ExampleWhoAmI() { + conn, err := ldap.Dial("tcp", "ldap.example.org:389") + if err != nil { + log.Fatalf("Failed to connect: %s\n", err) + } + + _, err = conn.SimpleBind(&ldap.SimpleBindRequest{ + Username: "uid=someone,ou=people,dc=example,dc=org", + Password: "MySecretPass", + }) + if err != nil { + log.Fatalf("Failed to bind: %s\n", err) + } + + res, err := conn.WhoAmI(nil) + if err != nil { + log.Fatalf("Failed to call WhoAmI(): %s\n", err) + } + fmt.Printf("I am: %s\n", res.AuthzID) +} + +func ExampleWhoAmIProxied() { + conn, err := ldap.Dial("tcp", "ldap.example.org:389") + if err != nil { + log.Fatalf("Failed to connect: %s\n", err) + } + + _, err = conn.SimpleBind(&ldap.SimpleBindRequest{ + Username: "uid=someone,ou=people,dc=example,dc=org", + Password: "MySecretPass", + }) + if err != nil { + log.Fatalf("Failed to bind: %s\n", err) + } + + pa := ldap.NewControlProxiedAuthoization("dn:uid=other,ou=people,dc=example,dc=org") + + res, err := conn.WhoAmI([]ldap.Control{pa}) + if err != nil { + log.Fatalf("Failed to call WhoAmI(): %s\n", err) + } + fmt.Printf("For this call only I am now: %s\n", res.AuthzID) +} From 4913eefab3219fd80138d3ef3eec09d1522bf26f Mon Sep 17 00:00:00 2001 From: Hanno Hecker Date: Tue, 2 May 2017 17:44:04 +0200 Subject: [PATCH 2/2] add encode/decode control test also: fix typo: s/Authoization/Authorization/ --- control.go | 10 ++++++++-- control_test.go | 4 ++++ whoami_test.go | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/control.go b/control.go index 4fb37e8d..b0d4eb34 100644 --- a/control.go +++ b/control.go @@ -275,8 +275,8 @@ func (c *ControlProxiedAuthorization) String() string { c.Criticality) } -// NewControlProxiedAuthoization returns a ProxiedAuthoization control -func NewControlProxiedAuthoization(authzID string) *ControlProxiedAuthorization { +// NewControlProxiedAuthorization returns a ProxiedAuthorization control +func NewControlProxiedAuthorization(authzID string) *ControlProxiedAuthorization { return &ControlProxiedAuthorization{ Criticality: true, AuthzID: authzID, @@ -418,6 +418,12 @@ func DecodeControl(packet *ber.Packet) Control { value.Value = c.Expire return c + + case ControlTypeProxiedAuthorization: + c := &ControlProxiedAuthorization{Criticality: true} + authzID := ber.DecodeString(value.Data.Bytes()) + c.AuthzID = authzID + return c default: c := new(ControlString) c.ControlType = ControlType diff --git a/control_test.go b/control_test.go index 11527463..5cec1edb 100644 --- a/control_test.go +++ b/control_test.go @@ -27,6 +27,10 @@ func TestControlString(t *testing.T) { runControlTest(t, NewControlString("x", false, "")) } +func TestControlProxiedAuthorization(t *testing.T) { + runControlTest(t, NewControlProxiedAuthorization("dn:uid=someone,ou=people,dc=example,dc=net")) +} + func runControlTest(t *testing.T, originalControl Control) { header := "" if callerpc, _, line, ok := runtime.Caller(1); ok { diff --git a/whoami_test.go b/whoami_test.go index ffe2dc26..40902387 100644 --- a/whoami_test.go +++ b/whoami_test.go @@ -42,7 +42,7 @@ func ExampleWhoAmIProxied() { log.Fatalf("Failed to bind: %s\n", err) } - pa := ldap.NewControlProxiedAuthoization("dn:uid=other,ou=people,dc=example,dc=org") + pa := ldap.NewControlProxiedAuthorization("dn:uid=other,ou=people,dc=example,dc=org") res, err := conn.WhoAmI([]ldap.Control{pa}) if err != nil {