File Coverage

blib/lib/WWW/Zitadel/Management.pm
Criterion Covered Total %
statement 177 180 98.3
branch 112 182 61.5
condition 70 146 47.9
subroutine 59 61 96.7
pod 45 46 97.8
total 463 615 75.2


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