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, {[]})).