Password reset

This commit is contained in:
Correl Roush 2017-09-12 17:35:48 -04:00
parent 664190cdae
commit 765299ac2f
5 changed files with 259 additions and 1 deletions

View file

@ -5,3 +5,5 @@
{["users", uid, "password"], 'elmdap-password', []}.
{["users"], 'elmdap-users', []}.
{["groups"], 'elmdap-groups', []}.
{["reset"], 'elmdap-reset-generate', []}.
{["reset", token], 'elmdap-reset', []}.

View file

@ -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, [

View file

@ -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]))))

109
src/elmdap-reset.lfe Normal file
View file

@ -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)))))

View file

@ -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")))]))