You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2014/08/01 11:04:57 UTC
[4/9] git commit: Add top level API and security logic
Add top level API and security logic
BugzId: 29571
Project: http://git-wip-us.apache.org/repos/asf/couchdb-cassim/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-cassim/commit/c1bb498b
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-cassim/tree/c1bb498b
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-cassim/diff/c1bb498b
Branch: refs/heads/windsor-merge
Commit: c1bb498b37d9055822fcd10d66adc6e305502ff7
Parents: a6b6545
Author: Russell Branca <ch...@gmail.com>
Authored: Wed Apr 30 14:12:03 2014 -0700
Committer: Robert Newson <rn...@apache.org>
Committed: Wed Jul 30 18:16:17 2014 +0100
----------------------------------------------------------------------
src/cassim.erl | 81 +++++++++++++++++
src/cassim_security.erl | 204 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 285 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/couchdb-cassim/blob/c1bb498b/src/cassim.erl
----------------------------------------------------------------------
diff --git a/src/cassim.erl b/src/cassim.erl
new file mode 100644
index 0000000..b3debea
--- /dev/null
+++ b/src/cassim.erl
@@ -0,0 +1,81 @@
+% Copyright 2014 Cloudant
+%
+% 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(cassim).
+
+
+-export([
+ is_server_admin/1,
+ verify_is_admin/1,
+ verify_is_server_admin/1
+]).
+
+-export([
+ has_admin_role/1,
+ verify_admin_role/1
+]).
+
+-export([
+ get_security/1,
+ get_security/2,
+ set_security/2,
+ set_security/3
+]).
+
+
+-include_lib("couch/include/couch_db.hrl").
+
+
+verify_is_admin(#httpd{}=Req) ->
+ chttpd:verify_is_server_admin(Req).
+
+
+is_server_admin(#httpd{user_ctx=#user_ctx{roles=Roles}}) ->
+ lists:member(server_admin, Roles).
+
+
+verify_is_server_admin(#httpd{}=Req) ->
+ case is_server_admin(Req) of
+ true -> ok;
+ false -> throw({unauthorized, <<"You are not a server admin.">>})
+ end.
+
+
+has_admin_role(#user_ctx{roles=Roles}) ->
+ has_admin_role(Roles);
+has_admin_role(Roles) when is_list(Roles) ->
+ lists:member(<<"_admin">>, Roles) orelse lists:member(server_admin, Roles).
+
+
+verify_admin_role(Obj) ->
+ case has_admin_role(Obj) of
+ true -> ok;
+ false -> throw({unauthorized, <<"You are not a db or server admin.">>})
+ end.
+
+
+get_security(DbName) ->
+ cassim_security:get_security(DbName).
+
+
+get_security(DbName, Options) ->
+ cassim_security:get_security(DbName, Options).
+
+
+set_security(DbName, SecObj) ->
+ cassim_security:set_security(DbName, SecObj).
+
+
+set_security(DbName, SecObj, Options) ->
+ cassim_security:set_security(DbName, SecObj, Options).
http://git-wip-us.apache.org/repos/asf/couchdb-cassim/blob/c1bb498b/src/cassim_security.erl
----------------------------------------------------------------------
diff --git a/src/cassim_security.erl b/src/cassim_security.erl
new file mode 100644
index 0000000..52fa56a
--- /dev/null
+++ b/src/cassim_security.erl
@@ -0,0 +1,204 @@
+% Copyright 2014 Cloudant
+%
+% 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(cassim_security).
+
+
+-export([
+ get_security/1,
+ get_security/2,
+ get_security_doc/1
+]).
+
+-export([
+ set_security/2,
+ set_security/3
+]).
+
+-export([
+ security_meta_id/1,
+ validate_security_doc/1
+]).
+
+
+-include_lib("couch/include/couch_db.hrl").
+
+
+-define(ADMIN_USER, #user_ctx{roles = [<<"_admin">>]}).
+-define(ADMIN_CTX, {user_ctx, ?ADMIN_USER}).
+
+
+security_meta_id(DbName) ->
+ <<DbName/binary, "/_security">>.
+
+
+get_security(DbName) ->
+ get_security(DbName, [?ADMIN_CTX]).
+
+
+get_security(#db{name=DbName}, Options) ->
+ get_security(DbName, Options);
+get_security(DbName, Options) ->
+ case cassim_metadata_cache:metadata_db_exists() of
+ true ->
+ UserCtx = couch_util:get_value(user_ctx, Options, #user_ctx{}),
+ Doc = get_security_doc(DbName),
+ {SecProps} = couch_doc:to_json_obj(Doc, []),
+ check_is_member(UserCtx, SecProps),
+ {proplists:delete(<<"_id">>, SecProps)};
+ false ->
+ fabric:get_security(DbName, Options)
+ end.
+
+
+get_security_doc(DbName0) when is_binary(DbName0) ->
+ DbName = mem3:dbname(DbName0),
+ MetaId = security_meta_id(DbName),
+ case cassim_metadata_cache:load_meta(MetaId) of
+ undefined ->
+ SecProps = fabric:get_security(DbName),
+ {ok, SecDoc} = migrate_security_props(DbName, SecProps),
+ SecDoc;
+ SecProps ->
+ couch_doc:from_json_obj(SecProps)
+ end.
+
+
+set_security(DbName, SecProps) ->
+ set_security(DbName, SecProps, [?ADMIN_CTX]).
+
+
+set_security(#db{name=DbName0}=Db, #doc{body=SecProps}=SecDoc0, Options) ->
+ case cassim_metadata_cache:metadata_db_exists() of
+ true ->
+ DbName = mem3:dbname(DbName0),
+ MetaId = security_meta_id(DbName),
+ SecDoc = SecDoc0#doc{id=MetaId},
+ UserCtx = couch_util:get_value(user_ctx, Options, #user_ctx{}),
+ MetaDbName = cassim_metadata_cache:metadata_db(),
+ MetaDb = #db{name=MetaDbName, user_ctx=?ADMIN_USER},
+ cassim:verify_admin_role(UserCtx),
+ ok = validate_security_doc(SecDoc),
+ {Status, Etag, {Body0}} =
+ chttpd_db:update_doc(MetaDb, MetaId, SecDoc, Options),
+ Body = {proplists:delete(<<"_id">>, Body0)},
+ {Status, Etag, Body};
+ false ->
+ fabric:set_security(Db, SecProps, Options)
+ end.
+
+
+migrate_security_props(DbName0, {SecProps}) ->
+ DbName = mem3:dbname(DbName0),
+ MetaId = security_meta_id(DbName),
+ SecDoc = #doc{id=MetaId, body={SecProps}},
+ MetaDbName = cassim_metadata_cache:metadata_db(),
+ MetaDb = #db{name=MetaDbName, user_ctx=?ADMIN_USER},
+ %% Better way to construct a new #doc{} with the rev?
+ {_, _, {Body}} = chttpd_db:update_doc(MetaDb, MetaId, SecDoc, [?ADMIN_CTX]),
+ Rev = proplists:get_value(rev, Body),
+ SecProps1 = lists:keystore(<<"_rev">>, 1, SecProps, {<<"_rev">>, Rev}),
+ SecDoc1 = couch_doc:from_json_obj({SecProps1}),
+ {ok, SecDoc1}.
+
+
+validate_security_doc(#doc{body={SecProps}}) ->
+ Admins = couch_util:get_value(<<"admins">>, SecProps, {[]}),
+ % we fallback to readers here for backwards compatibility
+ Members = couch_util:get_value(<<"members">>, SecProps,
+ couch_util:get_value(<<"readers">>, SecProps, {[]})),
+ ok = validate_names_and_roles(Admins),
+ ok = validate_names_and_roles(Members),
+ Users = couch_util:get_value(<<"cloudant">>, SecProps, {[]}),
+ ok = validate_cloudant_roles(Users),
+ ok.
+
+
+validate_names_and_roles({Props}) when is_list(Props) ->
+ lists:foreach(
+ fun(Name) ->
+ validate_roles_list(Name, couch_util:get_value(Name, Props, []))
+ end,
+ [<<"names">>, <<"roles">>]
+ ).
+
+
+validate_cloudant_roles({Props}) when is_list(Props) ->
+ lists:foreach(fun({U, R}) -> validate_roles_list(U, R) end, Props);
+validate_cloudant_roles(_) ->
+ throw("Cloudant field must be a set of name roles list pairs").
+
+
+validate_roles_list(Field, Roles) when is_list(Roles) ->
+ case lists:all(fun(X) -> is_binary(X) end, Roles) of
+ true -> ok;
+ false -> throw(binary_to_list(Field) ++ " must be a JSON list of strings")
+ end;
+validate_roles_list(Field, _Roles) ->
+ throw(binary_to_list(Field) ++ " must be a JSON list of strings").
+
+
+check_is_admin(#user_ctx{name=Name,roles=Roles}, SecProps) ->
+ {Admins} = get_admins(SecProps),
+ AdminRoles = [<<"_admin">> | couch_util:get_value(<<"roles">>, Admins, [])],
+ AdminNames = couch_util:get_value(<<"names">>, Admins,[]),
+ case AdminRoles -- Roles of
+ AdminRoles -> % same list, not an admin role
+ case AdminNames -- [Name] of
+ AdminNames -> % same names, not an admin
+ throw({unauthorized, <<"You are not a db or server admin.">>});
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end.
+
+
+check_is_member(#user_ctx{name=Name,roles=Roles}=UserCtx, SecProps) ->
+ case (catch check_is_admin(UserCtx, SecProps)) of
+ ok -> ok;
+ _ ->
+ {Members} = get_members(SecProps),
+ ReaderRoles = couch_util:get_value(<<"roles">>, Members,[]),
+ WithAdminRoles = [<<"_admin">> | ReaderRoles],
+ ReaderNames = couch_util:get_value(<<"names">>, Members,[]),
+ case ReaderRoles ++ ReaderNames of
+ [] -> ok; % no readers == public access
+ _Else ->
+ case WithAdminRoles -- Roles of
+ WithAdminRoles -> % same list, not an reader role
+ case ReaderNames -- [Name] of
+ ReaderNames -> % same names, not a reader
+ ?LOG_DEBUG("Not a reader: UserCtx ~p vs Names ~p Roles ~p",[UserCtx, ReaderNames, WithAdminRoles]),
+ throw({unauthorized, <<"You are not authorized to access this db.">>});
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end
+ end
+ end.
+
+
+get_admins(SecProps) ->
+ couch_util:get_value(<<"admins">>, SecProps, {[]}).
+
+
+get_members(SecProps) ->
+ % we fallback to readers here for backwards compatibility
+ couch_util:get_value(<<"members">>, SecProps,
+ couch_util:get_value(<<"readers">>, SecProps, {[]})).