Skip to content

Commit e6ee0d3

Browse files
omercierscresto31
andauthored
enh(apps::monitoring::zscaler::zdx::api): adapt to OneAPI authentication (#6094)
Co-authored-by: Sylvain Cresto <scresto@centreon.com> Refs: CTOR-2251
1 parent fc45529 commit e6ee0d3

11 files changed

Lines changed: 196 additions & 200 deletions

File tree

.github/docker/unit-tests/Dockerfile.unit-tests-alma10

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ ARG REGISTRY_URL=docker.io
33
FROM ${REGISTRY_URL}/almalinux:10
44

55
RUN bash -e <<EOF
6-
76
dnf install -y 'dnf-command(config-manager)' epel-release zstd jq
87
dnf config-manager --set-enabled crb
98

@@ -12,25 +11,25 @@ cat > /etc/yum.repos.d/centreon-plugins.repo <<'REPOEOF'
1211
[centreon-plugins-stable]
1312
name=centreon plugins stable x86_64
1413
baseurl=https://packages.centreon.com/rpm-plugins/el10/stable/x86_64
15-
enabled=0
14+
enabled=1
1615
gpgcheck=1
1716
gpgkey=https://yum-gpg.centreon.com/RPM-GPG-KEY-CES
1817
[centreon-plugins-stable-noarch]
1918
name=centreon plugins stable noarch
2019
baseurl=https://packages.centreon.com/rpm-plugins/el10/stable/noarch
21-
enabled=0
20+
enabled=1
2221
gpgcheck=1
2322
gpgkey=https://yum-gpg.centreon.com/RPM-GPG-KEY-CES
2423
[centreon-plugins-testing]
2524
name=centreon plugins testing x86_64
2625
baseurl=https://packages.centreon.com/rpm-plugins/el10/testing/x86_64
27-
enabled=0
26+
enabled=1
2827
gpgcheck=1
2928
gpgkey=https://yum-gpg.centreon.com/RPM-GPG-KEY-CES
3029
[centreon-plugins-testing-noarch]
3130
name=centreon plugins testing noarch
3231
baseurl=https://packages.centreon.com/rpm-plugins/el10/testing/noarch
33-
enabled=0
32+
enabled=1
3433
gpgcheck=1
3534
gpgkey=https://yum-gpg.centreon.com/RPM-GPG-KEY-CES
3635
[centreon-plugins-unstable]

.github/docker/unit-tests/Dockerfile.unit-tests-trixie

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ ENV LANG=en_US.utf8
1818

1919
RUN bash -e <<EOF
2020
# Add Centreon plugins repositories
21-
#echo "deb https://packages.centreon.com/apt-plugins-stable/ trixie main" | tee /etc/apt/sources.list.d/centreon-plugins.list
22-
#echo "deb https://packages.centreon.com/apt-plugins-testing/ trixie main" | tee -a /etc/apt/sources.list.d/centreon-plugins.list
23-
#echo "deb https://packages.centreon.com/apt-plugins-unstable/ trixie main" | tee -a /etc/apt/sources.list.d/centreon-plugins.list
21+
echo "deb https://packages.centreon.com/apt-plugins-stable/ trixie main" | tee /etc/apt/sources.list.d/centreon-plugins.list
22+
echo "deb https://packages.centreon.com/apt-plugins-testing/ trixie main" | tee -a /etc/apt/sources.list.d/centreon-plugins.list
23+
echo "deb https://packages.centreon.com/apt-plugins-unstable/ trixie main" | tee -a /etc/apt/sources.list.d/centreon-plugins.list
2424
echo "deb https://packages.centreon.com/apt-plugins-unstable/ trixie main" | tee a /etc/apt/sources.list.d/centreon-plugins.list
2525
wget -O- https://apt-key.centreon.com | gpg --dearmor | tee /etc/apt/trusted.gpg.d/centreon.gpg > /dev/null 2>&1
2626
apt-get update

.github/packaging/centreon-plugin.yaml.template

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ overrides:
5555
perl(lib),
5656
perl(sort),
5757
perl(String::ShellQuote),
58+
perl(Digest::SHA),
5859
tzdata,
5960
@RPM_DEPENDENCIES@
6061
]
@@ -84,6 +85,7 @@ overrides:
8485
libdatetime-perl,
8586
libxml-libxml-perl,
8687
libstring-shellquote-perl,
88+
libdigest-sha-perl,
8789
tzdata,
8890
@DEB_DEPENDENCIES@
8991
]
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"dependencies": [
3-
"perl(DateTime)"
3+
"perl(DateTime)",
4+
"perl(URI::Escape)"
45
]
56
}

src/apps/monitoring/zscaler/zdx/api/custom/api.pm

Lines changed: 85 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ use warnings;
2525
use centreon::plugins::http;
2626
use centreon::plugins::statefile;
2727
use DateTime;
28-
use Digest::MD5 qw(md5_hex);
28+
use URI::Escape;
29+
use Digest::SHA qw(sha256_hex);
2930
use centreon::plugins::misc qw(json_decode is_empty value_of is_excluded);
3031

3132
sub new {
3233
my ($class, %options) = @_;
33-
my $self = {};
34+
my $self = {};
3435
bless $self, $class;
3536

3637
if (!defined($options{output})) {
@@ -41,23 +42,24 @@ sub new {
4142
$options{output}->add_option_msg(short_msg => "Class Custom: Need to specify 'options' argument.");
4243
$options{output}->option_exit();
4344
}
44-
45+
4546
if (!defined($options{noptions})) {
4647
$options{options}->add_options(arguments => {
47-
'key-id:s' => { name => 'key_id', default => '' },
48-
'key-secret:s' => { name => 'key_secret', default => '' },
49-
'api-path:s' => { name => 'api_path', default => '/v1' },
50-
'hostname:s' => { name => 'hostname', default => 'api.zdxcloud.net' },
51-
'port:s' => { name => 'port', default => 443 },
52-
'proto:s' => { name => 'proto', default => 'https' },
53-
'timeout:s' => { name => 'timeout', default => 10 }
48+
'auth-url:s' => { name => 'auth_url', default => '' },
49+
'client-id:s' => { name => 'client_id', default => '' },
50+
'client-secret:s' => { name => 'client_secret', default => '' },
51+
'api-path:s' => { name => 'api_path', default => '/zdx/v1' },
52+
'hostname:s' => { name => 'hostname', default => 'api.zsapi.net' },
53+
'port:s' => { name => 'port', default => 443 },
54+
'proto:s' => { name => 'proto', default => 'https' },
55+
'timeout:s' => { name => 'timeout', default => 10 }
5456
});
5557
}
5658
$options{options}->add_help(package => __PACKAGE__, sections => 'REST API OPTIONS', once => 1);
5759

5860
$self->{output} = $options{output};
59-
$self->{http} = centreon::plugins::http->new(%options, default_backend => 'curl');
60-
$self->{cache} = centreon::plugins::statefile->new(%options);
61+
$self->{http} = centreon::plugins::http->new(%options, default_backend => 'curl');
62+
$self->{cache} = centreon::plugins::statefile->new(%options);
6163

6264
return $self;
6365
}
@@ -70,14 +72,13 @@ sub get_token {
7072
$self->settings();
7173

7274
my $has_cache_file = $self->{cache}->read(
73-
statefile => 'zdx_token_' . md5_hex(
74-
$self->{hostname}
75-
. ':' . $self->{port}
76-
. '_' . $self->{key_id})
75+
statefile => 'zdx_token_' . sha256_hex(
76+
$self->{auth_url}
77+
. '_' . $self->{client_id})
7778
);
7879
# return token stored in cache if exists after checking it is still valid
7980
if ($has_cache_file) {
80-
my $expiration = $self->{cache}->get(name => 'expiration');
81+
my $expiration = $self->{cache}->get(name => 'expiration');
8182
if (defined($expiration) && $expiration > time() + 60) {
8283
$self->{token} = $self->{cache}->get(name => 'token');
8384
$self->{token_type} = $self->{cache}->get(name => 'token_type');
@@ -89,17 +90,22 @@ sub get_token {
8990
# if we do not have a token or if it has to be renewed
9091
my $content = $self->{http}->request(
9192
method => 'POST',
92-
url_path => '/v1/oauth/token',
93-
query_form_post => '{"key_id": "' . $self->{key_id} . '", "key_secret": "' . $self->{key_secret} . '"}',
94-
93+
full_url => $self->{auth_url},
94+
header => [ 'Content-Type: application/x-www-form-urlencoded' ],
95+
query_form_post => "grant_type=client_credentials&client_id=" . uri_escape($self->{client_id})
96+
. "&client_secret=" . uri_escape($self->{client_secret}),
9597
);
9698
my $decoded_content = json_decode($content, output => $self->{output});
9799

98-
$self->{output}->option_exit(short_msg => "No token found in '$content'") unless ($decoded_content->{token});
99-
$self->{token} = $decoded_content->{token};
100-
$self->{token_type} = $decoded_content->{token_type};
100+
$self->{output}->option_exit(short_msg => "No token found in '$content'")
101+
unless ($decoded_content->{access_token} || $decoded_content->{token});
102+
# store in $self for further requests
103+
$self->{token} = $decoded_content->{access_token} // $decoded_content->{token};
104+
$self->{token_type} = $decoded_content->{token_type} // 'Bearer';
105+
# store in cache for further executions
101106
$self->{http}->add_header(key => 'Authorization', value => $self->{token_type} . ' ' . $self->{token});
102-
$self->{cache}->write(data => { token => $decoded_content->{token}, token_type => $decoded_content->{token_type}, expiration => ($decoded_content->{expires_in} + time()) });
107+
$self->{cache}->write(data => { token => $self->{token}, token_type => $self->{token_type}, expiration => ($decoded_content->{expires_in} + time()) });
108+
103109
return $self->{token};
104110
}
105111

@@ -120,7 +126,7 @@ sub build_location_filters {
120126
# if location filters are provided, get the list of location ids
121127
my $locations = $self->get_locations(%options);
122128
# $locations points to an array of {id, name}
123-
return { loc => [map { $_->{id}} @$locations] };
129+
return { loc => [ map {$_->{id}} @$locations ] };
124130
}
125131

126132
sub get_unique_app {
@@ -129,24 +135,26 @@ sub get_unique_app {
129135
my $content = $self->{http}->request(
130136
method => 'GET',
131137
get_params => $self->{get_params},
138+
unknown_status => "%{http_code} < 200 or %{http_code} > 404",
132139
url_path => $self->{option_results}->{api_path} . '/apps/' . $options{application_id}
133140
);
134141
my $app = json_decode($content, output => $self->{output});
142+
135143
return {
136-
id => $app->{id},
137-
name => $app->{name},
138-
score => $app->{score},
139-
total_users => $app->{stats}->{active_users}
144+
id => $app->{id} // $options{application_id},
145+
name => $app->{name} // "not found",
146+
score => $app->{score} // -1,
147+
total_users => $app->{stats}->{active_users} // 0
140148
}
141149
}
142150

143151
sub get_unique_app_metrics {
144152
my ($self, %options) = @_;
145153

146-
my $to = time();
147-
my $get_params = { %{ $self->{get_params}} };
154+
my $to = time();
155+
my $get_params = { %{$self->{get_params}} };
148156
$get_params->{from} = $to - 60 * ($options{max_metrics_age} // 20);
149-
$get_params->{to} = $to;
157+
$get_params->{to} = $to;
150158

151159
my $content = $self->{http}->request(
152160
method => 'GET',
@@ -183,6 +191,7 @@ sub get_unique_app_metrics {
183191
}
184192
}
185193

194+
186195
return $data;
187196
}
188197

@@ -221,6 +230,12 @@ sub get_apps {
221230
total_users => $app->{total_users}
222231
};
223232
}
233+
push @stats, {
234+
id => $options{application_id} // -1,
235+
name => $options{include_application_name} //"not found",
236+
score => -1,
237+
total_users => 0
238+
} unless @stats;
224239

225240
return \@stats;
226241
}
@@ -232,8 +247,8 @@ sub get_locations {
232247

233248
# get all locations and check which ones match the filters
234249
my $locations_json = $self->{http}->request(
235-
method => 'GET',
236-
url_path => $self->{option_results}->{api_path} . '/administration/locations/'
250+
method => 'GET',
251+
url_path => $self->{option_results}->{api_path} . '/administration/locations/'
237252
);
238253
my $locations = json_decode($locations_json, output => $self->{output});
239254

@@ -246,8 +261,8 @@ sub get_locations {
246261
$options{exclude_location_name});
247262

248263
push @result, {
249-
id => $loc->{id},
250-
name => $loc->{name},
264+
id => $loc->{id},
265+
name => $loc->{name},
251266
};
252267
}
253268

@@ -256,13 +271,28 @@ sub get_locations {
256271

257272
sub set_defaults {}
258273

274+
sub from_fqdn_to_url {
275+
my $input = shift;
276+
# remove trailing /
277+
$input =~ s/\/$//;
278+
# if url is already complete, return
279+
return $input if $input =~ /^https?:\/\/[\w\.:]+\/.+$/;
280+
# add default auth path if nothing after fqdn
281+
$input .= "/oauth2/v1/token" if $input =~ /^(https?:\/\/)?[\w\.:]+$/;
282+
# add default auth proto if no proto given
283+
$input = "https://" . $input if $input !~ /^https?:\/\//;
284+
return $input;
285+
}
286+
259287
sub check_options {
260288
my ($self, %options) = @_;
261289

262-
$self->{$_} = $self->{option_results}->{$_} foreach qw(hostname port proto api_path timeout key_id key_secret );
290+
$self->{$_} = $self->{option_results}->{$_} foreach qw(hostname port proto timeout auth_url api_path client_id client_secret);
291+
$self->{output}->option_exit(short_msg => "Mandatory option 'auth-url' is missing for OneAPI authentication.") if ($self->{auth_url} eq '');
263292

264-
foreach (qw(key_id key_secret)) {
265-
$self->{output}->option_exit(short_msg => "Mandatory option '$_' is missing.") if ($self->{$_} eq '');
293+
$self->{auth_url} = from_fqdn_to_url($self->{auth_url});
294+
foreach (qw(client_id client_secret)) {
295+
$self->{output}->option_exit(short_msg => "Mandatory option '$_' is missing for OneAPI authentication.") if ($self->{$_} eq '');
266296
$self->{output}->option_exit(short_msg => "Option '$_' contains illegal characters.") if ($self->{$_} =~ /([\b\f\n\r\t\"\\]+)/);
267297
}
268298

@@ -274,9 +304,7 @@ sub check_options {
274304
sub settings {
275305
my ($self, %options) = @_;
276306

277-
#$self->build_options_for_httplib();
278307
$self->{http}->add_header(key => 'Accept', value => 'application/json');
279-
$self->{http}->add_header(key => 'Content-Type', value => 'application/json');
280308
$self->{http}->set_options(%{$self->{option_results}});
281309
}
282310

@@ -298,29 +326,37 @@ Zscaler Digital Experience (ZDX) Rest API
298326
299327
=over 8
300328
329+
=item B<--auth-url>
330+
331+
Authentication URL (or FQDN) to get a token from (mandatory).
332+
333+
Depends on your Zscaler customer name.
334+
335+
Example: C<https://company-name.zslogin.net/oauth2/v1/token> or C<company-name.zslogin.net>.
336+
301337
=item B<--hostname>
302338
303-
ZDX API hostname (default: C<api.zdxcloud.net>)
339+
API URL (default: C<api.zsapi.net>). You should not need to change it.
304340
305341
=item B<--port>
306342
307-
API port (default: 443)
343+
API port (default: 443).
308344
309345
=item B<--proto>
310346
311-
Specify http if needed (default: 'https')
347+
Specify http if needed (default: 'https').
312348
313349
=item B<--api-path>
314350
315-
API URL path (default: '/api')
351+
API URL path (default: '/zdx/v1')
316352
317-
=item B<--key-id>
353+
=item B<--client-id>
318354
319-
Key ID (see L<here|https://help.zscaler.com/zdx/managing-zdx-api-keys> for more details).
355+
Client ID for OneAPI authentication (see https://help.zscaler.com/zidentity/understanding-oneapi-authentication for more details).
320356
321-
=item B<--key-secret>
357+
=item B<--client-secret>
322358
323-
Key secret (see L<here|https://help.zscaler.com/zdx/managing-zdx-api-keys> for more details).
359+
Client secret for OneAPI authentication (see https://help.zscaler.com/zidentity/understanding-oneapi-authentication for more details).
324360
325361
=item B<--timeout>
326362

src/apps/monitoring/zscaler/zdx/api/plugin.pm

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ sub new {
3131

3232
$self->{version} = '0.1';
3333
$self->{modes} = {
34-
'application' => 'apps::monitoring::zscaler::zdx::api::mode::application',
35-
'discovery' => 'apps::monitoring::zscaler::zdx::api::mode::discovery',
36-
'list-locations' => 'apps::monitoring::zscaler::zdx::api::mode::listlocations',
34+
'application' => 'apps::monitoring::zscaler::zdx::api::mode::application',
35+
'discovery' => 'apps::monitoring::zscaler::zdx::api::mode::discovery',
36+
'list-locations' => 'apps::monitoring::zscaler::zdx::api::mode::listlocations',
3737
};
3838
$self->{custom_modes}->{api} = 'apps::monitoring::zscaler::zdx::api::custom::api';
3939
return $self;

tests/apps/monitoring/zscaler/zdx/api/application.robot

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ Test Timeout 120s
1111
*** Variables ***
1212
${MOCKOON_JSON} ${CURDIR}${/}mockoon.json
1313
${CMD} ${CENTREON_PLUGINS}
14-
... --plugin=apps::monitoring::zscaler::zdx::api::plugin
15-
... --mode=application
16-
... --hostname=${HOSTNAME}
17-
... --port=${APIPORT}
18-
... --proto=http
19-
... --key-id=1
20-
... --key-secret=1
14+
... --plugin=apps::monitoring::zscaler::zdx::api::plugin
15+
... --mode=application
16+
... --hostname=${HOSTNAME}
17+
... --port=${APIPORT}
18+
... --proto=http
19+
... --client-id=1
20+
... --client-secret=1
21+
... --auth-url=http://${HOSTNAME}:${APIPORT}/oauth2/v1/token
2122

2223

2324
*** Test Cases ***

0 commit comments

Comments
 (0)