/__w/shurbej/shurbej/_build/test/cover/ct/shurbej_admin.html

1 -module(shurbej_admin).
2 -include_lib("shurbej_store/include/shurbej_records.hrl").
3 -include_lib("stdlib/include/ms_transform.hrl").
4
5 -export([
6 %% Users
7 create_user/2, create_user/3, list_users/0, delete_user/1,
8 %% API keys
9 create_api_key/2, create_api_key/3,
10 %% Groups
11 create_group/3, create_group/4, delete_group/1, list_groups/0,
12 add_member/3, remove_member/2, list_members/1, list_user_groups/1
13 ]).
14
15 %% ===================================================================
16 %% Users
17 %% ===================================================================
18
19 create_user(Username, Password) when is_binary(Username), is_binary(Password) ->
20 %% Allocate user_id and insert in one transaction so two concurrent
21 %% admin calls can't race into the same id.
22 38 Result = mnesia:transaction(fun() ->
23 38 case mnesia:read(shurbej_user, Username) of
24
:-(
[_] -> {error, already_exists};
25 [] ->
26 38 UserId = tx_next_user_id(),
27 38 tx_create_user(Username, Password, UserId),
28 38 {ok, UserId}
29 end
30 end),
31 38 case Result of
32 {atomic, {ok, UserId}} ->
33 38 logger:notice("created user ~s (user_id=~p)", [Username, UserId]),
34 38 ok;
35 {atomic, {error, _} = Err} ->
36
:-(
Err
37 end.
38
39 create_user(Username, Password, UserId) when is_binary(Username), is_binary(Password) ->
40 3 Result = mnesia:transaction(fun() ->
41 3 case mnesia:read(shurbej_user, Username) of
42 1 [_] -> {error, already_exists};
43 [] ->
44 2 tx_create_user(Username, Password, UserId),
45 2 ok
46 end
47 end),
48 3 case Result of
49 {atomic, ok} ->
50 2 logger:notice("created user ~s (user_id=~p)", [Username, UserId]),
51 2 ok;
52 {atomic, {error, _} = Err} ->
53 1 Err
54 end.
55
56 %% Mirror shurbej_db:create_user's writes without its own transaction wrapper
57 %% so the caller's transaction stays single-level.
58 tx_create_user(Username, Password, UserId) ->
59 40 Salt = crypto:strong_rand_bytes(16),
60 40 Hash = shurbej_db:hash_password(Password, Salt),
61 40 mnesia:write(#shurbej_user{
62 username = Username,
63 password_hash = Hash,
64 salt = Salt,
65 user_id = UserId
66 }),
67 40 LibRef = {user, UserId},
68 40 case mnesia:read(shurbej_library, LibRef) of
69 40 [] -> mnesia:write(#shurbej_library{ref = LibRef, version = 0});
70
:-(
_ -> ok
71 end.
72
73 tx_next_user_id() ->
74 38 MS = ets:fun2ms(fun(#shurbej_user{user_id = Id}) -> Id end),
75 38 case mnesia:select(shurbej_user, MS) of
76
:-(
[] -> 1;
77 38 Ids -> lists:max(Ids) + 1
78 end.
79
80 list_users() ->
81 38 MS = ets:fun2ms(
82 fun(#shurbej_user{username = U, user_id = Id}) -> {U, Id} end),
83 38 mnesia:dirty_select(shurbej_user, MS).
84
85 delete_user(Username) ->
86 1 mnesia:dirty_delete({shurbej_user, Username}).
87
88
89 %% ===================================================================
90 %% API keys
91 %% ===================================================================
92
93 %% Shortcut: create_api_key(UserId, Name) — full access to the user library
94 %% and every group they belong to.
95 create_api_key(UserId, Name) ->
96 1 create_api_key(UserId, Name, full).
97
98 %% create_api_key(UserId, Name, Access) where Access is one of:
99 %% full — library+write+files+notes on user, library+write on groups.all
100 %% read_only — library only on user, library only on groups.all
101 %% Map — canonical perms map; passes through normalize_perms.
102 create_api_key(UserId, Name, Access) when is_integer(UserId), is_binary(Name) ->
103 37 case shurbej_db:get_user_by_id(UserId) of
104 1 undefined -> {error, user_not_found};
105 {ok, _} ->
106 36 Perms = resolve_access(Access),
107 36 ApiKey = generate_api_key(),
108 36 shurbej_db:create_key(ApiKey, UserId, Perms),
109 36 logger:notice("created API key '~s' for user_id=~p", [Name, UserId]),
110 36 {ok, ApiKey}
111 end.
112
113 resolve_access(full) ->
114 33 shurbej_http_common:normalize_perms(undefined);
115 resolve_access(read_only) ->
116 1 shurbej_http_common:normalize_perms(#{
117 user => #{library => true, write => false, files => false, notes => false},
118 groups => #{all => #{library => true, write => false}}
119 });
120 resolve_access(Map) when is_map(Map) ->
121 2 shurbej_http_common:normalize_perms(Map).
122
123 generate_api_key() ->
124 36 binary:encode_hex(crypto:strong_rand_bytes(32), lowercase).
125
126 %% ===================================================================
127 %% Groups
128 %% ===================================================================
129
130 %% create_group(Name, OwnerUserId, Type) — owner auto-added as member.
131 create_group(Name, OwnerUserId, Type) ->
132 3 create_group(Name, OwnerUserId, Type, #{}).
133
134 %% Opts: description, url, library_editing, library_reading, file_editing.
135 create_group(Name, OwnerUserId, Type, Opts)
136 when is_binary(Name), is_integer(OwnerUserId),
137 (Type =:= private orelse Type =:= public_closed
138 orelse Type =:= public_open) ->
139 16 create_group_1(Name, OwnerUserId, Type, Opts);
140 create_group(_, _, Type, _) ->
141 1 {error, {bad_type, Type}}.
142
143 create_group_1(Name, OwnerUserId, Type, Opts) ->
144 16 case shurbej_db:get_user_by_id(OwnerUserId) of
145
:-(
undefined -> {error, owner_not_found};
146 {ok, _} ->
147 16 LibEd = maps:get(library_editing, Opts, members),
148 16 LibRd = maps:get(library_reading, Opts, members),
149 16 FileEd = maps:get(file_editing, Opts, admins),
150 %% Allocate group_id and write within a single transaction so
151 %% concurrent creates can't collide on the same id.
152 16 {atomic, GroupId} = mnesia:transaction(fun() ->
153 16 Gid = tx_next_group_id(),
154 16 mnesia:write(#shurbej_group{
155 group_id = Gid,
156 name = Name,
157 owner_id = OwnerUserId,
158 type = Type,
159 description = maps:get(description, Opts, <<>>),
160 url = maps:get(url, Opts, <<>>),
161 has_image = false,
162 library_editing = LibEd,
163 library_reading = LibRd,
164 file_editing = FileEd,
165 created = erlang:system_time(second),
166 version = 0
167 }),
168 16 mnesia:write(#shurbej_group_member{
169 id = {Gid, OwnerUserId}, role = owner
170 }),
171 16 mnesia:write(#shurbej_library{
172 ref = {group, Gid}, version = 0
173 }),
174 16 Gid
175 end),
176 16 logger:notice("created group ~s (group_id=~p, owner=~p)",
177 [Name, GroupId, OwnerUserId]),
178 16 {ok, GroupId}
179 end.
180
181 tx_next_group_id() ->
182 16 MS = ets:fun2ms(fun(#shurbej_group{group_id = Id}) -> Id end),
183 16 case mnesia:select(shurbej_group, MS) of
184 1 [] -> 1;
185 15 Ids -> lists:max(Ids) + 1
186 end.
187
188 delete_group(GroupId) when is_integer(GroupId) ->
189 1 shurbej_db:delete_group(GroupId).
190
191 list_groups() ->
192 2 [{G#shurbej_group.group_id, G#shurbej_group.name, G#shurbej_group.owner_id,
193 2 G#shurbej_group.type} || G <- shurbej_db:list_groups()].
194
195 add_member(GroupId, UserId, Role)
196 when is_integer(GroupId), is_integer(UserId),
197 (Role =:= owner orelse Role =:= admin orelse Role =:= member) ->
198 10 case {shurbej_db:get_group(GroupId), shurbej_db:get_user_by_id(UserId)} of
199 1 {undefined, _} -> {error, group_not_found};
200 1 {_, undefined} -> {error, user_not_found};
201 {{ok, _}, {ok, _}} ->
202 8 shurbej_db:add_group_member(GroupId, UserId, Role)
203 end;
204 add_member(_, _, Role) ->
205
:-(
{error, {bad_role, Role}}.
206
207 remove_member(GroupId, UserId) ->
208 1 shurbej_db:remove_group_member(GroupId, UserId).
209
210 list_members(GroupId) ->
211 2 [{U, R} || #shurbej_group_member{id = {_, U}, role = R}
212 2 <- shurbej_db:list_group_members(GroupId)].
213
214 list_user_groups(UserId) ->
215 1 [{G, R} || #shurbej_group_member{id = {G, _}, role = R}
216 1 <- shurbej_db:list_user_groups(UserId)].
217
Line Hits Source