diff --git a/priv/dispatch.conf b/priv/dispatch.conf index 33314e3..56ffab5 100644 --- a/priv/dispatch.conf +++ b/priv/dispatch.conf @@ -5,3 +5,5 @@ {["users", uid, "password"], 'elmdap-password', []}. {["users"], 'elmdap-users', []}. {["groups"], 'elmdap-groups', []}. +{["reset"], 'elmdap-reset-generate', []}. +{["reset", token], 'elmdap-reset', []}. diff --git a/rebar.config b/rebar.config index bb3a519..76a233d 100644 --- a/rebar.config +++ b/rebar.config @@ -4,7 +4,9 @@ {deps, [ {lfe, {git, "git://github.com/rvirding/lfe", {tag, "1.0"}}}, {calrissian, {git, "git://github.com/correl/calrissian", {branch, "rebar3"}}}, - {webmachine, ".*", {git, "git://github.com/basho/webmachine.git", {branch, "master"}}} + {webmachine, ".*", {git, "git://github.com/basho/webmachine.git", {branch, "master"}}}, + jwt, + gen_smtp ]}. {plugins, [ diff --git a/src/elmdap-reset-generate.lfe b/src/elmdap-reset-generate.lfe new file mode 100644 index 0000000..4e5df94 --- /dev/null +++ b/src/elmdap-reset-generate.lfe @@ -0,0 +1,131 @@ +(defmodule elmdap-reset-generate + (export (init 1) + (service_available 2) + (allowed_methods 2) + (content_types_provided 2) + (resource_exists 2) + (malformed_request 2) + (allow_missing_post 2) + (process_post 2) + (to_json 2))) + +(include-lib "webmachine/include/webmachine.hrl") +(include-lib "calrissian/include/monads.lfe") + +(defrecord state + email + entry + connection + hostname + port + secret + expires + smtp) + +(defrecord smtp + relay + from + username + password + ssl) + +(defun init (args) + `#(ok ,(make-state hostname (os:getenv "ELMDAP_HOSTNAME" (smtp_util:guess_FQDN)) + port (os:getenv "WEBMACHINE_PORT" "8080") + secret (os:getenv "ELMDAP_SECRET" "secret") + expires (erlang:list_to_integer (os:getenv "ELMDAP_EXPIRES" "3600")) + smtp (make-smtp + from (os:getenv "SMTP_FROM_ADDRESS") + relay (os:getenv "SMTP_RELAY") + username (os:getenv "SMTP_USERNAME") + password (os:getenv "SMTP_PASSWORD") + ssl (== "TRUE" (string:to_upper (os:getenv "SMTP_SSL" "FALSE"))))))) + +(defun service_available (req-data state) + (case (elmdap:open) + ((tuple 'ok connection) + `#(true ,req-data ,(set-state state connection connection))) + (error + (error_logger:warning_report `[service-unavailable + ,error]) + `#(false ,req-data ,state)))) + +(defun allowed_methods (req-data state) + `#([POST] ,req-data ,state)) + +(defun content_types_provided (req-data state) + `#((#("application/json" to_json)) ,req-data ,state)) + +(defun resource_exists (req-data state) + (case (elmdap:from-email (state-connection state) (state-email state)) + (`#(ok ,entry) + `#(true ,req-data ,(set-state state entry entry))) + (_ `#(false ,req-data ,state)))) + +(defun to_json (req-data state) + (tuple + (mochijson:encode (state-email state)) + req-data + state)) + +(defun malformed_request (req-data state) + (case (do-m (monad 'error) + (data <- (try (case (mochijson:decode (wrq:req_body req-data)) + (`#(struct ,plist) `#(ok ,plist)) + (_ #(error malformed))) + (catch (e `#(error ,e))))) + (email <- (case (proplists:get_value "email" data) + ('undefined #(error missing-email)) + (email `#(ok ,email)))) + (return (monad 'error) (set-state state email email))) + (`#(ok ,newstate) `#(false ,req-data ,newstate)) + (_ `#(true ,req-data ,state)))) + +(defun allow_missing_post (req-data state) + `#(true ,req-data ,state)) + +(defun process_post (req-data state) + (error_logger:info_report `[password-reset-requested + #(state ,state)]) + (if (/= 'undefined (state-entry state)) + (send-email state)) + `#(true ,req-data ,state)) + +(defun send-email (state) + (let* ((entry (state-entry state)) + (attrs (elmdap:entry-attributes entry)) + (email (state-email state)) + (name (proplists:get_value "displayName" attrs + (proplists:get_value "cn" attrs))) + (claims `[#(dn ,(erlang:list_to_binary (elmdap-entry:dn entry))) + #(email ,(erlang:list_to_binary email))]) + (expires (state-expires state)) + (secret (erlang:list_to_binary (state-secret state))) + (`#(ok ,token) (jwt:encode #"HS256" claims expires secret)) + (smtp (state-smtp state)) + (email `#(,(smtp-from smtp) + [,email] + ,(erlang:iolist_to_binary + `["Subject: LDAP Password Reset\r\n" + "From: CoreDial LDAP <" ,(smtp-from smtp) ">\r\n" + "\r\n" + ,name ",\r\n" + "Please visit the following URL to reset the LDAP password:\r\n" + "http://" ,(http-host (state-hostname state) (state-port state)) "/reset/" ,token]))) + (options `[#(relay ,(smtp-relay smtp)) + #(username ,(smtp-username smtp)) + #(password ,(smtp-password smtp)) + #(ssl ,(smtp-ssl smtp))])) + (let ((result (gen_smtp_client:send + email + options))) + (error_logger:info_report `[sending-email + #(email ,email) + #(options ,options) + #(result ,result)]) + result))) + +(defun http-host (hostname port) + (case port + ("80" hostname) + (_ (lists:flatten `[,hostname ":" ,port])))) diff --git a/src/elmdap-reset.lfe b/src/elmdap-reset.lfe new file mode 100644 index 0000000..8b1edcc --- /dev/null +++ b/src/elmdap-reset.lfe @@ -0,0 +1,109 @@ +(defmodule elmdap-reset + (export (init 1) + (service_available 2) + (is_authorized 2) + (allowed_methods 2) + (content_types_provided 2) + (resource_exists 2) + (malformed_request 2) + (allow_missing_post 2) + (process_post 2) + (to_json 2))) + +(include-lib "webmachine/include/webmachine.hrl") +(include-lib "calrissian/include/monads.lfe") + +(defrecord state + connection + dn + email + password) + +(defun init (args) + `#(ok ,(make-state))) + +(defun service_available (req-data state) + (case (do-m (monad 'error) + (connection <- (elmdap:open)) + (eldap:simple_bind connection + (os:getenv "LDAP_BIND_DN") + (os:getenv "LDAP_BIND_PASSWORD")) + (return (monad 'error) connection)) + ((tuple 'ok connection) + `#(true ,req-data ,(set-state state connection connection))) + (error + (error_logger:warning_report `[service-unavailable + ,error]) + `#(false ,req-data ,state)))) + +(defun is_authorized (req-data state) + (error_logger:info_report 'is_authorized) + (let ((secret (os:getenv "ELMDAP_SECRET" "secret")) + (`[#(token ,token)] (wrq:path_info req-data))) + (case (do-m (monad 'error) + (claims <- (jwt:decode (erlang:list_to_binary token) (erlang:list_to_binary secret))) + (dn <- (maps:find #"dn" claims)) + (email <- (maps:find #"email" claims)) + (return (monad 'error) (set-state state + dn (erlang:binary_to_list dn) + email (erlang:binary_to_list email)))) + (`#(ok ,newstate) `#(true ,req-data ,newstate)) + (`#(error ,e) + `#("Bearer realm=Elmdap" ,req-data ,state))))) + +(defun allowed_methods (req-data state) + `#([POST] ,req-data ,state)) + +(defun content_types_provided (req-data state) + `#((#("application/json" to_json)) ,req-data ,state)) + +(defun resource_exists (req-data state) + (case (get-entry (state-connection state) (state-dn state) (state-email state)) + (`#(ok ,_) `#(true ,req-data ,state)) + (`#(error, _) `#(false ,req-data ,state)))) + +(defun to_json (req-data state) + (tuple + (mochijson:encode (state-dn state)) + req-data + state)) + +(defun malformed_request (req-data state) + (case (do-m (monad 'error) + (data <- (try (case (mochijson:decode (wrq:req_body req-data)) + (`#(struct ,plist) `#(ok ,plist)) + (_ #(error malformed))) + (catch (e `#(error ,e))))) + (password <- (case (proplists:get_value "password" data) + ('undefined #(error missing-password)) + (email `#(ok ,email)))) + (return (monad 'error) (set-state state password password))) + (`#(ok ,newstate) `#(false ,req-data ,newstate)) + (_ `#(true ,req-data ,state)))) + +(defun allow_missing_post (req-data state) + `#(false ,req-data ,state)) + +(defun process_post (req-data state) + (case (eldap:modify_password + (state-connection state) + (state-dn state) + (state-password state)) + ('ok `#(true ,req-data ,state)) + (e + (error_logger:error_report `[resetting-password ,e]) + `#(false ,req-data ,state)))) + +(defun get-entry (connection dn email) + (error_logger:info_report `[#(connection ,connection) #(dn ,dn) #(email ,email)]) + (let* ((base dn) + (filter (eldap:equalityMatch "mail" email)) + (`#(ok #(eldap_search_result ,results ,_)) + (eldap:search connection + `[#(base ,base) + #(filter ,filter) + #(attributes ["cn" "userPassword"])])) + ((cons entry _) results)) + (case results + (`[,entry] `#(ok ,entry)) + (_ #(error not_found))))) \ No newline at end of file diff --git a/src/elmdap.lfe b/src/elmdap.lfe index 9cef84f..e04391a 100644 --- a/src/elmdap.lfe +++ b/src/elmdap.lfe @@ -32,6 +32,20 @@ ((cons (= entry `#(eldap_entry ,dn ,attributes)) _) `#(ok ,entry)) (_ #(error not_found))))) +(defun from-email (connection email) + (let* ((`#(ok ,connection) (open)) + (base "ou=people,dc=coredial,dc=com") + (filter (eldap:equalityMatch "mail" email)) + (`#(ok #(eldap_search_result ,results ,_)) + (eldap:search connection + `[#(base ,base) + #(filter ,filter)])) + ) + (eldap:close connection) + (case results + ((cons (= entry `#(eldap_entry ,dn ,attributes)) _) `#(ok ,entry)) + (_ #(error not_found))))) + (defun open () (let ((hosts `[,(os:getenv "LDAP_HOST" "localhost")]) (options `[#(port ,(list_to_integer (os:getenv "LDAP_PORT" "389")))]))