]> gitweb.factorcode.org Git - factor.git/blob - basis/furnace/auth/providers/couchdb/couchdb.factor
basis: removing unnecessary method stack effects.
[factor.git] / basis / furnace / auth / providers / couchdb / couchdb.factor
1 USING: accessors assocs base64 byte-arrays combinators.short-circuit
2 continuations couchdb fry furnace.auth.providers json.writer kernel
3 locals make mirrors namespaces sequences strings urls urls.encoding ;
4 IN: furnace.auth.providers.couchdb
5
6 ! !!! Implement the authentication protocol for CouchDB.
7 ! !!!
8 ! !!! 'user' tuples are copied verbatim into the DB as objects.
9 ! !!! Special 'reservation' records are inserted into the DB to
10 ! !!! reserve usernames and email addresses. These reservation records
11 ! !!! all have ids with the prefix given to couchdb-auth-provider.
12 ! !!! A reservation in the email domain for the email address "foo@bar.com"
13 ! !!! would have id "PREFIXemail!foo%40bar.com". Both the domain name
14 ! !!! and the value are url-encoded, to ensure that the use of '!' as
15 ! !!! a separator guarantees a unique ID for any given (domain,value)
16 ! !!! pairing.
17 ! !!!
18 ! !!! It would be nice to use CouchDB attachments to avoid junking the
19 ! !!! global namespace like this. However, attachments in CouchDB
20 ! !!! inherit their revision ids from their parent document, which would
21 ! !!! make various operations on users unnecessairly non-independent
22 ! !!! of each other.
23 ! !!!
24 ! !!! On the basic technique used here, see:
25 ! !!!
26 ! !!! http://kfalck.net/2009/06/29/enforcing-unique-usernames-on-couchdb
27 ! !!!
28
29 ! Many of the words below assume that this symbol is bound to an
30 ! appropriate instance.
31 TUPLE: couchdb-auth-provider
32     base-url
33     { username-view string }
34     { prefix string initial: "user_reservation_" }
35     { field-map assoc initial: { } } ;
36
37 <PRIVATE
38
39 ! >json does weird things for mirrors, so we copy the mirror into
40 ! a real hashtable before serializing it.
41 : hash-mirror ( obj -- hash )
42     make-mirror H{ } assoc-like ;
43
44 : is-couchdb-conflict-error? ( error -- ? )
45     { [ couchdb-error? ] [ data>> "error" of "conflict" = ] } 1&& ;
46 : is-couchdb-not-found-error? ( error -- ? )
47     { [ couchdb-error? ] [ data>> "error" of "not_found" = ] } 1&& ;
48
49 : get-url ( url -- url' )
50     couchdb-auth-provider get
51     base-url>> >url swap >url derive-url ;
52
53 : reservation-id ( value name -- id )
54     couchdb-auth-provider get
55     prefix>> [ % url-encode-full % "!" % url-encode-full % ] "" make ;
56
57 : (reserve) ( value name -- id/f )
58     '[
59         _ _ reservation-id get-url
60         H{ } clone swap couch-put
61     ] [ is-couchdb-conflict-error? ] ignore-error/f ;
62
63 ! Don't reserve false values (e.g. if the email field is f, don't reserve f,
64 ! or the first user who registers without an email address will block all
65 ! others who wish to do so).
66 : reserve ( value name -- id/f )
67     over [ (reserve) ] [ 2drop t ] if ;
68
69 : unreserve ( couch-rval -- )
70     [ "id" of get-url ]
71     [ "rev" of "rev" set-query-param ]
72     bi
73     couch-delete drop ;
74
75 : unreserve-from-id ( id -- )
76     '[
77         _ get-url dup couch-get
78         "_rev" of "rev" set-query-param
79         couch-delete drop
80     ] [ is-couchdb-not-found-error? ] ignore-error ;
81
82 :: (reserve-multiple) ( hash keys made -- ? )
83     keys empty? [ t ] [
84         keys first hash at keys first reserve [
85             made push
86             hash keys rest-slice made (reserve-multiple)
87         ] [
88             ! Delete reservations that were already successfully made.
89             made [ unreserve ] each
90             f
91         ] if*
92     ] if ;
93
94 ! Try to reserve all of the given name/value pairs; if not all reservations
95 ! can be made, delete those that were made.
96 : reserve-multiple ( hash -- ? )
97     dup keys V{ } clone (reserve-multiple) ;
98
99 : change-at* ( key assoc quot -- assoc )
100     over [ change-at ] dip ; inline
101
102 ! Should be given a view URL.
103 : url>user ( couchdb-url -- user/f )
104     couch-get
105     "rows" of dup empty? [ drop f ] [ first "value" of ] if ;
106
107 : (get-user) ( username -- user/f )
108     couchdb-auth-provider get
109     username-view>> get-url
110     swap >json "key" set-query-param
111     url>user ;
112
113 : strip-hash ( hash1 -- hash2 )
114     [ drop first CHAR: _ = ] assoc-reject ;
115
116 : at-or-k ( key hash -- newkey )
117     dupd at [ nip ] when* ;
118 : value-at-or-k ( key hash -- newkey )
119     dupd value-at [ nip ] when* ;
120
121 : map-fields-forward ( assoc field-map -- assoc )
122     [ swapd at-or-k swap ] curry assoc-map ;
123
124 : map-fields-backward ( assoc field-map -- assoc )
125     [ swapd value-at-or-k swap ] curry assoc-map ;
126
127 : user-hash>user ( hash -- user )
128     couchdb-auth-provider get field-map>> map-fields-backward
129     [ "password" swap [ base64> >byte-array ] change-at ]
130     [
131         strip-hash
132         user new dup [ make-mirror swap assoc-union! drop ] dip
133         f >>changed?
134     ]
135     bi ;
136
137 : user>user-hash ( user -- hash )
138      hash-mirror
139      [ [ "password" ] dip [ >base64 >string ] change-at ] keep
140      couchdb-auth-provider get field-map>> map-fields-forward ;
141
142 ! Used when the user is guaranteed to exist if the logic of the Factor
143 ! code is correct (e.g. when update-user is called).
144 ! In the unlikely event that the user does not exist, an error is thrown.
145 : (get-user)/throw-on-no-user ( username -- user/f )
146     (get-user) [ ] [ "User not found" throw ] if* ;
147
148 : (new-user) ( user -- user/f )
149     dup
150     [
151         [ username>> "username" ,, ]
152         [ email>> "email" ,, ]
153         bi
154     ] H{ } make
155     reserve-multiple
156     [
157         user>user-hash
158         "" get-url
159         couch-post
160     ] [
161         drop f
162     ] if ;
163
164 : unify-users ( old new -- new )
165     swap
166     [ "_rev" of "_rev" rot set-at ]
167     [ "_id" of "_id" rot set-at ]
168     [ swap assoc-union ]
169     2tri ;
170
171 ! If the user has changed username or email address,
172 ! we should let other registrants use the old ones,
173 ! and make sure that the new ones are reserved.
174 ! (This word is called by the 'update-user' method.)
175 : check-update ( old new -- ? )
176     [
177         2dup [ "email" of ] same? not [
178             [ "email" of ] bi@
179             [ drop "email" reservation-id unreserve-from-id ]
180             [ nip "email" reserve ]
181             2bi
182         ] [ 2drop t ] if
183     ] [
184         2dup [ "username" of ] same? not [
185             [ "username" of ] bi@
186             [ drop "username" reservation-id unreserve-from-id ]
187             [ nip "username" reserve ]
188             2bi
189         ] [ 2drop t ] if
190     ] 2bi and ;
191
192 PRIVATE>
193
194 : <couchdb-auth-provider> ( base-url username-view -- couchdb-auth-provider )
195     couchdb-auth-provider new swap >>username-view swap >>base-url ;
196
197 M: couchdb-auth-provider get-user
198     couchdb-auth-provider [
199         (get-user) [ user-hash>user ] [ f ] if*
200     ] with-variable ;
201
202 M: couchdb-auth-provider new-user
203     couchdb-auth-provider [
204         dup (new-user) [
205             username>> couchdb-auth-provider get get-user
206         ] [ drop f ] if
207     ] with-variable ;
208
209 M: couchdb-auth-provider update-user
210     couchdb-auth-provider [
211         [ username>> (get-user)/throw-on-no-user dup ]
212         [ drop "_id" of get-url ]
213         [ user>user-hash swapd
214           2dup check-update drop
215           unify-users swap couch-put drop
216         ]
217         tri
218     ] with-variable ;