diff --git a/client.go b/client.go index c7f41f6f..c00cb106 100644 --- a/client.go +++ b/client.go @@ -22,6 +22,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 b7f181f2..37a04530 100644 --- a/control.go +++ b/control.go @@ -18,6 +18,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 @@ -25,6 +27,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 @@ -238,6 +241,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) +} + +// NewControlProxiedAuthorization returns a ProxiedAuthorization control +func NewControlProxiedAuthorization(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 { @@ -373,6 +414,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/error.go b/error.go index 6e1277fd..cdcb3f3c 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 @@ -97,6 +102,7 @@ var LDAPResultCodeMap = map[uint8]string{ LDAPResultEntryAlreadyExists: "Entry Already Exists", LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", + LDAPResultAuthorizationDenied: "Authorization Denied", LDAPResultOther: "Other", ErrorNetwork: "Network Error", 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..40902387 --- /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.NewControlProxiedAuthorization("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) +}