File Coverage

blib/lib/Net/Async/Zitadel/Management.pm
Criterion Covered Total %
statement 175 183 95.6
branch 100 182 54.9
condition 68 146 46.5
subroutine 60 62 96.7
pod 0 46 0.0
total 403 619 65.1


line stmt bran cond sub pod time code
1             package Net::Async::Zitadel::Management;
2              
3             # ABSTRACT: Async client for Zitadel Management API v1
4              
5 3     3   190520 use Moo;
  3         6610  
  3         24  
6 3     3   2341 use JSON::MaybeXS qw(encode_json decode_json);
  3         8  
  3         253  
7 3     3   500 use HTTP::Request;
  3         19196  
  3         154  
8 3     3   1307 use MIME::Base64 qw(encode_base64);
  3         1865  
  3         237  
9 3     3   26 use URI;
  3         6  
  3         74  
10 3     3   35 use Future;
  3         7  
  3         75  
11 3     3   488 use Net::Async::Zitadel::Error;
  3         8  
  3         86  
12 3     3   40 use namespace::clean;
  3         15  
  3         25  
13              
14             our $VERSION = '0.001';
15              
16             has base_url => (
17             is => 'ro',
18             required => 1,
19             );
20              
21             has token => (
22             is => 'ro',
23             required => 1,
24             );
25              
26             has http => (
27             is => 'ro',
28             required => 1,
29             doc => 'Net::Async::HTTP instance (shared from parent)',
30             );
31              
32             has _api_base => (
33             is => 'lazy',
34             builder => sub {
35 3     3   67 my $base = $_[0]->base_url;
36 3         16 $base =~ s{/+$}{};
37 3         22 "$base/management/v1";
38             },
39             );
40              
41             sub BUILD {
42 14     14 0 323598 my $self = shift;
43 14 100       235 die Net::Async::Zitadel::Error::Validation->new(
44             message => 'base_url must not be empty',
45             ) unless length $self->base_url;
46             }
47              
48 21     21   671 sub _require { die Net::Async::Zitadel::Error::Validation->new(message => $_[0]) }
49              
50             # --- Generic async request methods ---
51              
52             sub _request_f {
53 3     3   14 my ($self, $method, $path, $body) = @_;
54              
55 3         83 my $url = $self->_api_base . $path;
56 3         30 my $req = HTTP::Request->new($method => $url);
57 3         10262 $req->header(Authorization => 'Bearer ' . $self->token);
58 3         246 $req->header(Accept => 'application/json');
59              
60 3 100       153 if ($body) {
61 1         3 $req->header('Content-Type' => 'application/json');
62 1         47 $req->content(encode_json($body));
63             }
64              
65             return $self->http->do_request(request => $req)->then(sub {
66 3     3   400 my ($response) = @_;
67 3         6 my $data;
68 3 100 66     12 if ($response->decoded_content && length $response->decoded_content) {
69 2         25 eval { $data = decode_json($response->decoded_content) };
  2         8  
70             }
71              
72 3 100       54 unless ($response->is_success) {
73 1 50 33     9 my $api_msg = $data && $data->{message} ? $data->{message} : undef;
74 1         2 my $msg = 'API error: ' . $response->status_line;
75 1 50       5 $msg .= " - $api_msg" if $api_msg;
76 1         3 return Future->fail(Net::Async::Zitadel::Error::API->new(
77             message => $msg,
78             http_status => $response->status_line,
79             api_message => $api_msg,
80             ));
81             }
82              
83 2   100     25 return Future->done($data // {});
84 3         46 });
85             }
86              
87 7     7   32 sub _get_f { $_[0]->_request_f('GET', $_[1]) }
88 27     27   139 sub _post_f { $_[0]->_request_f('POST', $_[1], $_[2]) }
89 5     5   22 sub _put_f { $_[0]->_request_f('PUT', $_[1], $_[2]) }
90 6     6   44 sub _delete_f { $_[0]->_request_f('DELETE', $_[1]) }
91              
92             # --- Users ---
93              
94             sub list_users_f {
95 1     1 0 11 my ($self, %args) = @_;
96             $self->_post_f('/users/_search', {
97             query => {
98             offset => $args{offset} // 0,
99             limit => $args{limit} // 100,
100             asc => $args{asc} // JSON::MaybeXS::true,
101             },
102 1 50 50     51 $args{queries} ? (queries => $args{queries}) : (),
      50        
      33        
103             });
104             }
105              
106             sub get_user_f {
107 2     2 0 95 my ($self, $user_id) = @_;
108 2 100       12 $user_id or _require('user_id required');
109 1         6 $self->_get_f("/users/$user_id");
110             }
111              
112             sub create_human_user_f {
113 2     2 0 7878 my ($self, %args) = @_;
114             $self->_post_f('/users/human', {
115             userName => $args{user_name} // _require('user_name required'),
116             profile => {
117             firstName => $args{first_name} // _require('first_name required'),
118             lastName => $args{last_name} // _require('last_name required'),
119             displayName => $args{display_name} // "$args{first_name} $args{last_name}",
120             $args{nick_name} ? (nickName => $args{nick_name}) : (),
121             $args{preferred_language} ? (preferredLanguage => $args{preferred_language}) : (),
122             },
123             email => {
124             email => $args{email} // _require('email required'),
125             isEmailVerified => $args{email_verified} // JSON::MaybeXS::false,
126             },
127             $args{phone} ? (phone => {
128             phone => $args{phone},
129             isPhoneVerified => $args{phone_verified} // JSON::MaybeXS::false,
130             }) : (),
131 2 50 33     34 $args{password} ? (password => $args{password}) : (),
    50 33        
    50 66        
    50 33        
      33        
      33        
      0        
132             });
133             }
134              
135             sub update_user_f {
136 1     1 0 23 my ($self, $user_id, %args) = @_;
137 1 50       3 $user_id or _require('user_id required');
138             $self->_put_f("/users/$user_id/profile", {
139             $args{first_name} ? (firstName => $args{first_name}) : (),
140             $args{last_name} ? (lastName => $args{last_name}) : (),
141             $args{display_name} ? (displayName => $args{display_name}) : (),
142 1 50       23 $args{nick_name} ? (nickName => $args{nick_name}) : (),
    50          
    50          
    50          
143             });
144             }
145              
146             sub deactivate_user_f {
147 1     1 0 19 my ($self, $user_id) = @_;
148 1 50       3 $user_id or _require('user_id required');
149 1         4 $self->_post_f("/users/$user_id/_deactivate", {});
150             }
151              
152             sub reactivate_user_f {
153 1     1 0 25 my ($self, $user_id) = @_;
154 1 50       3 $user_id or _require('user_id required');
155 1         2 $self->_post_f("/users/$user_id/_reactivate", {});
156             }
157              
158             sub delete_user_f {
159 1     1 0 18 my ($self, $user_id) = @_;
160 1 50       4 $user_id or _require('user_id required');
161 1         6 $self->_delete_f("/users/$user_id");
162             }
163              
164             # --- Service / machine users ---
165              
166             sub create_service_user_f {
167 2     2 0 7984 my ($self, %args) = @_;
168             $self->_post_f('/users/machine', {
169             userName => $args{user_name} // _require('user_name required'),
170             name => $args{name} // _require('name required'),
171 2 50 66     22 $args{description} ? (description => $args{description}) : (),
      33        
172             });
173             }
174              
175             sub list_service_users_f {
176 1     1 0 51 my ($self, %args) = @_;
177             $self->_post_f('/users/_search', {
178             query => {
179             offset => $args{offset} // 0,
180             limit => $args{limit} // 100,
181             asc => $args{asc} // JSON::MaybeXS::true,
182             },
183             queries => [
184             { typeQuery => { type => 'TYPE_MACHINE' } },
185 1   50     26 @{ $args{queries} // [] },
  1   50     19  
      33        
      50        
186             ],
187             });
188             }
189              
190             sub get_service_user_f {
191 1     1 0 37 my ($self, $user_id) = @_;
192 1 50       4 $user_id or _require('user_id required');
193 1         5 $self->_get_f("/users/$user_id");
194             }
195              
196             sub delete_service_user_f {
197 1     1 0 32 my ($self, $user_id) = @_;
198 1 50       3 $user_id or _require('user_id required');
199 1         8 $self->_delete_f("/users/$user_id");
200             }
201              
202             # --- Machine keys (JWT auth for service users) ---
203              
204             sub add_machine_key_f {
205 1     1 0 49 my ($self, $user_id, %args) = @_;
206 1 50       31 $user_id or _require('user_id required');
207             $self->_post_f("/users/$user_id/keys", {
208             type => $args{type} // 'KEY_TYPE_JSON',
209 1 50 50     18 $args{expiration_date} ? (expirationDate => $args{expiration_date}) : (),
210             });
211             }
212              
213             sub list_machine_keys_f {
214 1     1 0 48 my ($self, $user_id, %args) = @_;
215 1 50       4 $user_id or _require('user_id required');
216             $self->_post_f("/users/$user_id/keys/_search", {
217             query => {
218             offset => $args{offset} // 0,
219 1   50     15 limit => $args{limit} // 100,
      50        
220             },
221             });
222             }
223              
224             sub remove_machine_key_f {
225 2     2 0 294 my ($self, $user_id, $key_id) = @_;
226 2 50       23 $user_id or _require('user_id required');
227 2 100       7 $key_id or _require('key_id required');
228 1         40 $self->_delete_f("/users/$user_id/keys/$key_id");
229             }
230              
231             # --- Password management ---
232              
233             sub set_password_f {
234 3     3 0 7791 my ($self, $user_id, %args) = @_;
235 3 100       11 $user_id or _require('user_id required');
236             $self->_post_f("/users/$user_id/password", {
237             password => $args{password} // _require('password required'),
238 2 50 66     44 $args{change_required} ? (changeRequired => $args{change_required}) : (),
239             });
240             }
241              
242             sub request_password_reset_f {
243 1     1 0 44 my ($self, $user_id) = @_;
244 1 50       3 $user_id or _require('user_id required');
245 1         4 $self->_post_f("/users/$user_id/_reset_password", {});
246             }
247              
248             # --- User metadata ---
249              
250             sub set_user_metadata_f {
251 3     3 0 524 my ($self, $user_id, $key, $value) = @_;
252 3 50       9 $user_id or _require('user_id required');
253 3 100       9 $key or _require('key required');
254 2 100       13 defined $value or _require('value required');
255 1         9 $self->_post_f("/users/$user_id/metadata/$key", {
256             value => encode_base64($value, ''),
257             });
258             }
259              
260             sub get_user_metadata_f {
261 1     1 0 66 my ($self, $user_id, $key) = @_;
262 1 50       3 $user_id or _require('user_id required');
263 1 50       3 $key or _require('key required');
264 1         4 $self->_get_f("/users/$user_id/metadata/$key");
265             }
266              
267             sub list_user_metadata_f {
268 1     1 0 22 my ($self, $user_id, %args) = @_;
269 1 50       3 $user_id or _require('user_id required');
270             $self->_post_f("/users/$user_id/metadata/_search", {
271             query => {
272             offset => $args{offset} // 0,
273 1   50     21 limit => $args{limit} // 100,
      50        
274             },
275             });
276             }
277              
278             # --- Projects ---
279              
280             sub list_projects_f {
281 1     1 0 11 my ($self, %args) = @_;
282             $self->_post_f('/projects/_search', {
283             query => {
284             offset => $args{offset} // 0,
285             limit => $args{limit} // 100,
286             },
287 1 50 50     38 $args{queries} ? (queries => $args{queries}) : (),
      50        
288             });
289             }
290              
291             sub get_project_f {
292 2     2 0 483 my ($self, $project_id) = @_;
293 2 100       17 $project_id or _require('project_id required');
294 1         9 $self->_get_f("/projects/$project_id");
295             }
296              
297             sub create_project_f {
298 1     1 0 39 my ($self, %args) = @_;
299             $self->_post_f('/projects', {
300             name => $args{name} // _require('name required'),
301             $args{project_role_assertion} ? (projectRoleAssertion => $args{project_role_assertion}) : (),
302             $args{project_role_check} ? (projectRoleCheck => $args{project_role_check}) : (),
303             $args{has_project_check} ? (hasProjectCheck => $args{has_project_check}) : (),
304 1 50 33     17 $args{private_labeling_setting} ? (privateLabelingSetting => $args{private_labeling_setting}) : (),
    50          
    50          
    50          
305             });
306             }
307              
308             sub update_project_f {
309 1     1 0 39 my ($self, $project_id, %args) = @_;
310 1 50       4 $project_id or _require('project_id required');
311             $self->_put_f("/projects/$project_id", {
312             name => $args{name} // _require('name required'),
313             $args{project_role_assertion} ? (projectRoleAssertion => $args{project_role_assertion}) : (),
314             $args{project_role_check} ? (projectRoleCheck => $args{project_role_check}) : (),
315             $args{has_project_check} ? (hasProjectCheck => $args{has_project_check}) : (),
316 1 50 33     40 $args{private_labeling_setting} ? (privateLabelingSetting => $args{private_labeling_setting}) : (),
    50          
    50          
    50          
317             });
318             }
319              
320             sub delete_project_f {
321 1     1 0 36 my ($self, $project_id) = @_;
322 1 50       8 $project_id or _require('project_id required');
323 1         7 $self->_delete_f("/projects/$project_id");
324             }
325              
326             # --- Applications (OIDC) ---
327              
328             sub list_apps_f {
329 0     0 0 0 my ($self, $project_id, %args) = @_;
330 0 0       0 $project_id or _require('project_id required');
331             $self->_post_f("/projects/$project_id/apps/_search", {
332             query => {
333             offset => $args{offset} // 0,
334             limit => $args{limit} // 100,
335             },
336 0 0 0     0 $args{queries} ? (queries => $args{queries}) : (),
      0        
337             });
338             }
339              
340             sub get_app_f {
341 2     2 0 770 my ($self, $project_id, $app_id) = @_;
342 2 100       13 $project_id or _require('project_id required');
343 1 50       7 $app_id or _require('app_id required');
344 0         0 $self->_get_f("/projects/$project_id/apps/$app_id");
345             }
346              
347             sub create_oidc_app_f {
348 2     2 0 11740 my ($self, $project_id, %args) = @_;
349 2 50       17 $project_id or _require('project_id required');
350             $self->_post_f("/projects/$project_id/apps/oidc", {
351             name => $args{name} // _require('name required'),
352             redirectUris => $args{redirect_uris} // _require('redirect_uris required'),
353             responseTypes => $args{response_types} // ['OIDC_RESPONSE_TYPE_CODE'],
354             grantTypes => $args{grant_types} // ['OIDC_GRANT_TYPE_AUTHORIZATION_CODE'],
355             appType => $args{app_type} // 'OIDC_APP_TYPE_WEB',
356             authMethodType => $args{auth_method} // 'OIDC_AUTH_METHOD_TYPE_BASIC',
357             $args{post_logout_uris} ? (postLogoutRedirectUris => $args{post_logout_uris}) : (),
358             $args{dev_mode} ? (devMode => $args{dev_mode}) : (),
359             $args{access_token_type} ? (accessTokenType => $args{access_token_type}) : (),
360             $args{id_token_role_assertion} ? (idTokenRoleAssertion => $args{id_token_role_assertion}) : (),
361 2 50 33     117 $args{additional_origins} ? (additionalOrigins => $args{additional_origins}) : (),
    50 66        
    50 50        
    50 50        
    50 50        
      50        
362             });
363             }
364              
365             sub update_oidc_app_f {
366 1     1 0 79 my ($self, $project_id, $app_id, %args) = @_;
367 1 50       4 $project_id or _require('project_id required');
368 1 50       5 $app_id or _require('app_id required');
369             $self->_put_f("/projects/$project_id/apps/$app_id/oidc_config", {
370             $args{redirect_uris} ? (redirectUris => $args{redirect_uris}) : (),
371             $args{response_types} ? (responseTypes => $args{response_types}) : (),
372             $args{grant_types} ? (grantTypes => $args{grant_types}) : (),
373             $args{app_type} ? (appType => $args{app_type}) : (),
374             $args{auth_method} ? (authMethodType => $args{auth_method}) : (),
375             $args{post_logout_uris} ? (postLogoutRedirectUris => $args{post_logout_uris}) : (),
376             $args{dev_mode} ? (devMode => $args{dev_mode}) : (),
377             $args{access_token_type} ? (accessTokenType => $args{access_token_type}) : (),
378             $args{id_token_role_assertion} ? (idTokenRoleAssertion => $args{id_token_role_assertion}) : (),
379 1 50       29 $args{additional_origins} ? (additionalOrigins => $args{additional_origins}) : (),
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
380             });
381             }
382              
383             sub delete_app_f {
384 0     0 0 0 my ($self, $project_id, $app_id) = @_;
385 0 0       0 $project_id or _require('project_id required');
386 0 0       0 $app_id or _require('app_id required');
387 0         0 $self->_delete_f("/projects/$project_id/apps/$app_id");
388             }
389              
390             # --- Organizations ---
391              
392             sub get_org_f {
393 1     1 0 15 my ($self) = @_;
394 1         7 $self->_get_f('/orgs/me');
395             }
396              
397             sub create_org_f {
398 2     2 0 9492 my ($self, %args) = @_;
399             $self->_post_f('/orgs', {
400 2   66     27 name => $args{name} // _require('name required'),
401             });
402             }
403              
404             sub list_orgs_f {
405 1     1 0 68 my ($self, %args) = @_;
406             $self->_post_f('/orgs/_search', {
407             query => {
408             offset => $args{offset} // 0,
409             limit => $args{limit} // 100,
410             },
411 1 50 50     18 $args{queries} ? (queries => $args{queries}) : (),
      50        
412             });
413             }
414              
415             sub update_org_f {
416 2     2 0 533 my ($self, %args) = @_;
417             $self->_put_f('/orgs/me', {
418 2   66     38 name => $args{name} // _require('name required'),
419             });
420             }
421              
422             sub deactivate_org_f {
423 1     1 0 33 my ($self) = @_;
424 1         5 $self->_post_f('/orgs/me/_deactivate', {});
425             }
426              
427             # --- Roles ---
428              
429             sub add_project_role_f {
430 2     2 0 8584 my ($self, $project_id, %args) = @_;
431 2 50       12 $project_id or _require('project_id required');
432             $self->_post_f("/projects/$project_id/roles", {
433             roleKey => $args{role_key} // _require('role_key required'),
434             displayName => $args{display_name} // $args{role_key},
435 2 50 66     38 $args{group} ? (group => $args{group}) : (),
      33        
436             });
437             }
438              
439             sub list_project_roles_f {
440 1     1 0 77 my ($self, $project_id, %args) = @_;
441 1 50       6 $project_id or _require('project_id required');
442             $self->_post_f("/projects/$project_id/roles/_search", {
443             query => {
444             offset => $args{offset} // 0,
445             limit => $args{limit} // 100,
446             },
447 1 50 50     21 $args{queries} ? (queries => $args{queries}) : (),
      50        
448             });
449             }
450              
451             # --- User Grants (role assignments) ---
452              
453             sub create_user_grant_f {
454 2     2 0 537 my ($self, %args) = @_;
455 2   66     17 my $user_id = $args{user_id} // _require('user_id required');
456             $self->_post_f("/users/$user_id/grants", {
457             projectId => $args{project_id} // _require('project_id required'),
458 1   33     13 roleKeys => $args{role_keys} // _require('role_keys required'),
      33        
459             });
460             }
461              
462             sub list_user_grants_f {
463 1     1 0 54 my ($self, %args) = @_;
464             $self->_post_f('/users/grants/_search', {
465             query => {
466             offset => $args{offset} // 0,
467             limit => $args{limit} // 100,
468             },
469 1 50 50     39 $args{queries} ? (queries => $args{queries}) : (),
      50        
470             });
471             }
472              
473             # --- Identity Providers (IDPs) ---
474              
475             sub list_idps_f {
476 1     1 0 74 my ($self, %args) = @_;
477             $self->_post_f('/idps/_search', {
478             query => {
479             offset => $args{offset} // 0,
480             limit => $args{limit} // 100,
481             },
482 1 50 50     16 $args{queries} ? (queries => $args{queries}) : (),
      50        
483             });
484             }
485              
486             sub get_idp_f {
487 2     2 0 296 my ($self, $idp_id) = @_;
488 2 100       22 $idp_id or _require('idp_id required');
489 1         9 $self->_get_f("/idps/$idp_id");
490             }
491              
492             sub create_oidc_idp_f {
493 3     3 0 12190 my ($self, %args) = @_;
494             $self->_post_f('/idps/oidc', {
495             name => $args{name} // _require('name required'),
496             clientId => $args{client_id} // _require('client_id required'),
497             clientSecret => $args{client_secret} // _require('client_secret required'),
498             issuer => $args{issuer} // _require('issuer required'),
499             scopes => $args{scopes} // ['openid', 'profile', 'email'],
500             $args{display_name_mapping} ? (displayNameMapping => $args{display_name_mapping}) : (),
501             $args{username_mapping} ? (usernameMapping => $args{username_mapping}) : (),
502 3 50 66     61 $args{auto_register} ? (autoRegister => $args{auto_register}) : (),
    50 66        
    50 33        
      33        
      50        
503             });
504             }
505              
506             sub update_idp_f {
507 2     2 0 298 my ($self, $idp_id, %args) = @_;
508 2 50       8 $idp_id or _require('idp_id required');
509             $self->_put_f("/idps/$idp_id", {
510             name => $args{name} // _require('name required'),
511             $args{display_name_mapping} ? (displayNameMapping => $args{display_name_mapping}) : (),
512             $args{username_mapping} ? (usernameMapping => $args{username_mapping}) : (),
513 2 50 66     25 $args{auto_register} ? (autoRegister => $args{auto_register}) : (),
    50          
    50          
514             });
515             }
516              
517             sub delete_idp_f {
518 2     2 0 347 my ($self, $idp_id) = @_;
519 2 100       10 $idp_id or _require('idp_id required');
520 1         28 $self->_delete_f("/idps/$idp_id");
521             }
522              
523             sub activate_idp_f {
524 1     1 0 64 my ($self, $idp_id) = @_;
525 1 50       4 $idp_id or _require('idp_id required');
526 1         6 $self->_post_f("/idps/$idp_id/_activate", {});
527             }
528              
529             sub deactivate_idp_f {
530 1     1 0 38 my ($self, $idp_id) = @_;
531 1 50       5 $idp_id or _require('idp_id required');
532 1         5 $self->_post_f("/idps/$idp_id/_deactivate", {});
533             }
534              
535             1;
536              
537             __END__