Skip to content

Instantly share code, notes, and snippets.

@fdmanana
Created August 19, 2010 20:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fdmanana/538847 to your computer and use it in GitHub Desktop.
Save fdmanana/538847 to your computer and use it in GitHub Desktop.
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index 6b70777..6aedace 100644
--- a/etc/couchdb/default.ini.tpl.in
+++ b/etc/couchdb/default.ini.tpl.in
@@ -17,7 +17,10 @@ uri_file = %localstatelibdir%/couch.uri
port = 5984
bind_address = 127.0.0.1
max_connections = 2048
-authentication_handlers = {couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}
+; authentication handler which uses Apple Directory Services
+authentication_handlers = {couch_apple_ds_auth, apple_ds_authentication_handler}
+; standard CouchDB authentication handlers
+; authentication_handlers = {couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}
default_handler = {couch_httpd_db, handle_request}
secure_rewrites = true
vhost_global_handlers = _utils, _uuids, _session, _oauth, _users
@@ -121,4 +124,12 @@ compressible_types = text/*, application/javascript, application/json, applicat
[replicator]
max_http_sessions = 10
-max_http_pipeline_size = 10
\ No newline at end of file
+max_http_pipeline_size = 10
+
+[apple_application]
+app_id_key = 1234
+app_admin_password = foobar
+func = employee,john,doe
+auth_cookie_name = myacinfo
+login_url = https://dsauthweb-at.corp.apple.com/cgi-bin/WebObjects/DSAuthWeb.woa/wa/login
+validate_url = https://dsauthweb-at.corp.apple.com/cgi-bin/WebObjects/DSAuthWeb.woa/wa/validate
diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am
index 308a383..250ab39 100644
--- a/src/couchdb/Makefile.am
+++ b/src/couchdb/Makefile.am
@@ -27,6 +27,8 @@ CLEANFILES = $(compiled_files) $(doc_base)
# CLEANFILES = $(doc_modules) edoc-info
source_files = \
+ couch_apple_ds_auth.erl \
+ couch_apple_ds_cache.erl \
couch.erl \
couch_app.erl \
couch_auth_cache.erl \
@@ -85,6 +87,8 @@ EXTRA_DIST = $(source_files) couch_db.hrl couch_js_functions.hrl
compiled_files = \
couch.app \
+ couch_apple_ds_auth.beam \
+ couch_apple_ds_cache.beam \
couch.beam \
couch_app.beam \
couch_auth_cache.beam \
diff --git a/src/couchdb/couch_apple_ds_auth.erl b/src/couchdb/couch_apple_ds_auth.erl
new file mode 100644
index 0000000..556b926
--- /dev/null
+++ b/src/couchdb/couch_apple_ds_auth.erl
@@ -0,0 +1,107 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_apple_ds_auth).
+-include("couch_db.hrl").
+
+-export([apple_ds_authentication_handler/1]).
+
+-define(REQ_TIMEOUT, 30000).
+
+
+apple_ds_authentication_handler(#httpd{mochi_req = MochiReq} = Req) ->
+ CookieName = couch_config:get("apple_application", "auth_cookie_name"),
+ case MochiReq:get_cookie_value(CookieName) of
+ undefined ->
+ redirect_for_login(Req);
+ CookieValue ->
+ ?LOG_DEBUG("Found cookie ~p for Apple DS authentication with value: ~p",
+ [CookieName, CookieValue]),
+ validate_cookie(Req, CookieValue)
+ end.
+
+
+redirect_for_login(Req) ->
+ AppIdKey = couch_config:get("apple_application", "app_id_key"),
+ LoginUrl = couch_config:get("apple_application", "login_url") ++
+ "?appIdKey=" ++ couch_util:url_encode(AppIdKey),
+ couch_httpd:send_response(Req, 301, [{"Location", LoginUrl}], <<>>),
+ Req.
+
+
+validate_cookie(#httpd{peer = Peer} = Req, CookieValue) ->
+ Params = [
+ {"cookie", CookieValue},
+ {"ip", Peer},
+ {"appId", couch_config:get("apple_application", "app_id_key")},
+ {"func", couch_config:get("apple_application", "func")},
+ {"appAdminPassword",
+ couch_config:get("apple_application", "app_admin_password")}
+ ],
+ {ok, ValidateUrl} = validate_url(Params),
+ case send_validate_req(ValidateUrl) of
+ {error, Reason} ->
+ ?LOG_ERROR("Error during authentication through Apple DS: ~s",[Reason]),
+ redirect_for_login(Req);
+ {ok, _Headers, Body} ->
+ ?LOG_DEBUG("Received body from request to validate URL ~p is: ~p",
+ [ValidateUrl, Body]),
+ {ok, Reply} = parse_validation_reply(Body),
+ case couch_util:get_value("status", Reply) of
+ "0" ->
+ Req#httpd{user_ctx = #user_ctx{roles = [<<"_admin">>]}};
+ "STATUS_SUCCESS" ->
+ Req#httpd{user_ctx = #user_ctx{roles = [<<"_admin">>]}};
+ _ ->
+ redirect_for_login(Req)
+ end
+ end.
+
+
+parse_validation_reply(Body) ->
+ Lines = re:split(Body, "\r\n|\n", [{return, list}]),
+ KVs = lists:foldl(
+ fun(Line, Acc) ->
+ [K, V] = re:split(Line, "=", [{return, list}]),
+ [{couch_util:trim(K), couch_util:trim(V)} | Acc]
+ end,
+ [], Lines
+ ),
+ {ok, KVs}.
+
+
+send_validate_req(Url) ->
+ case ibrowse:send_req(Url, [], get, [],
+ [{response_format, list}, {inactivity_timeout, ?REQ_TIMEOUT}]) of
+ {ok, "200", Headers, Body} ->
+ {ok, Headers, Body};
+ {ok, Code, _Headers, _Body} ->
+ Reason = io_lib:format("received HTTP code ~s from the validation URL",
+ [Code]),
+ {error, Reason};
+ {error, Reason} ->
+ {error, couch_util:to_list(Reason)}
+ end.
+
+
+validate_url(Params) ->
+ BaseUrl = couch_config:get("apple_application", "validate_url"),
+ QS = lists:foldl(
+ fun({_K, undefined}, Acc) ->
+ Acc;
+ ({K, V}, Acc) ->
+ [K ++ "=" ++ couch_util:url_encode(V) | Acc]
+ end,
+ [], Params
+ ),
+ {ok, BaseUrl ++ "?" ++ string:join(QS, "&")}.
+
diff --git a/src/couchdb/couch_apple_ds_cache.erl b/src/couchdb/couch_apple_ds_cache.erl
new file mode 100644
index 0000000..90cc7e2
--- /dev/null
+++ b/src/couchdb/couch_apple_ds_cache.erl
@@ -0,0 +1,159 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_apple_ds_cache).
+-include("couch_db.hrl").
+
+% public API
+-export([start_link/1]).
+-export([get/1, put/2]).
+
+% gen_server callbacks
+-export([init/1, handle_call/3, handle_info/2, handle_cast/2]).
+-export([code_change/3, terminate/2]).
+
+-import(couch_util, [get_value/2, get_value/3]).
+
+-define(ITEMS, apple_ds_items_ets).
+-define(ATIMES, apple_ds_atimes_ets).
+
+-record(state, {
+ max_cache_size,
+ cache_size = 0,
+ get_purge_item,
+ timeout
+}).
+
+
+get(Key) ->
+ case ets:lookup(?ITEMS, Key) of
+ [] ->
+ not_found;
+ [{Key, {Item, _, _}}] ->
+ ok = gen_server:cast(?MODULE, {cache_hit, Key}),
+ {ok, Item}
+ end.
+
+
+put(Key, Item) ->
+ ok = gen_server:cast(?MODULE, {put, Key, Item}).
+
+
+%% @spec start_link(Options()) -> {ok, pid()}
+%% @type Options() -> [ Option() ]
+%% @type Option() -> {policy, Policy()} | {size, int()} | {ttl, int()}
+%% @type Policy() -> lru | mru
+start_link(Options) ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, Options, []).
+
+
+init(Options) ->
+ ?ITEMS = ets:new(?ITEMS, [protected, set, named_table]),
+ ?ATIMES = ets:new(?ATIMES, [private, ordered_set, named_table]),
+ GetPurgeItem = case get_value(policy, Options, lru) of
+ lru ->
+ fun() -> ets:first(?ATIMES) end;
+ mru ->
+ fun() -> ets:last(?ATIMES) end
+ end,
+ State = #state{
+ max_cache_size = get_value(size, Options, 100),
+ timeout = get_value(ttl, Options, 0),
+ get_purge_item = GetPurgeItem
+ },
+ {ok, State}.
+
+
+handle_cast({put, Key, Item}, #state{timeout = Timeout} = State) ->
+ #state{
+ cache_size = CacheSize,
+ max_cache_size = MaxSize,
+ get_purge_item = GetPurgeItem
+ } = State,
+ case CacheSize >= MaxSize of
+ true ->
+ free_cache_entry(GetPurgeItem);
+ false ->
+ ok
+ end,
+ ATime = erlang:now(),
+ Timer = set_timer(Key, Timeout),
+ true = ets:insert(?ATIMES, {ATime, Key}),
+ true = ets:insert(?ITEMS, {Key, {Item, ATime, Timer}}),
+ {noreply, State#state{cache_size = get_value(size, ets:info(?ITEMS))}};
+
+handle_cast({cache_hit, Key}, #state{timeout = Timeout} = State) ->
+ case ets:lookup(?ITEMS, Key) of
+ [{Key, {Item, ATime, Timer}}] ->
+ cancel_timer(Key, Timer),
+ true = ets:delete(?ITEMS, Key),
+ true = ets:delete(?ATIMES, ATime),
+ NewATime = erlang:now(),
+ NewTimer = set_timer(Key, Timeout),
+ true = ets:insert(?ATIMES, {NewATime, Key}),
+ true = ets:insert(?ITEMS, {Key, {Item, NewATime, NewTimer}});
+ [] ->
+ ok
+ end,
+ {noreply, State}.
+
+
+handle_info({expired, Key}, #state{cache_size = CacheSize} = State) ->
+ [{Key, {_Item, ATime, _Timer}}] = ets:lookup(?ITEMS, Key),
+ true = ets:delete(?ITEMS, Key),
+ true = ets:delete(?ATIMES, ATime),
+ {noreply, State#state{cache_size = erlang:max(CacheSize - 1, 0)}}.
+
+
+handle_call(Msg, From, State) ->
+ ?LOG_ERROR("Apple DS cache server received unexpected message ~p from ~p",
+ [Msg, From]),
+ {stop, unexpected_sync_call, State}.
+
+
+terminate(_Reason, _State) ->
+ true = ets:delete(?ITEMS),
+ true = ets:delete(?ATIMES).
+
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+
+free_cache_entry(GetPurgeItem) ->
+ case GetPurgeItem() of
+ '$end_of_table' ->
+ ok; % empty cache
+ ATime ->
+ [{ATime, Key}] = ets:lookup(?ATIMES, ATime),
+ [{Key, {_Item, ATime, Timer}}] = ets:lookup(?ITEMS, Key),
+ cancel_timer(Key, Timer),
+ true = ets:delete(?ATIMES, ATime),
+ true = ets:delete(?ITEMS, Key)
+ end.
+
+
+set_timer(_Key, 0) ->
+ undefined;
+set_timer(Key, Interval) when Interval > 0 ->
+ erlang:send_after(Interval, self(), {expired, Key}).
+
+
+cancel_timer(_Key, undefined) ->
+ ok;
+cancel_timer(Key, Timer) ->
+ case erlang:cancel_timer(Timer) of
+ false ->
+ ok;
+ _TimeLeft ->
+ receive {expired, Key} -> ok after 0 -> ok end
+ end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment