add Okta SSO integration (#17351)
* add Okta SSO integration * add Okta SSO integration * add new authentication integration logic to the generation script * apply Ilya's edits * Apply suggestions from code review * Update integrations/cloud-authentication/metadata.yaml * Update okta_sso.md --------- Co-authored-by: Fotis Voutsas <fotis@netdata.cloud> Co-authored-by: Ilya Mashchenko <ilya@netdata.cloud>
Hugo Valente committed
Apr 10, 2024 at 10:00 UTC
d9c5f204588e196b90046ecd39cbd1e4cb35b69b
8 files changed
+356
-3
integrations/categories.yaml
+7
-1
@@ -418,7 +418,7 @@
418
name: exporters
419
description: "Exporter Integrations"
420
most_popular: true
421
- priority: 5
421
+ priority: 6
422
children: []
423
- id: notify
424
name: notifications
@@ -438,3 +438,9 @@
438
most_popular: true
439
priority: 1
440
children: []
441
+- id: auth
442
+ name: authentication
443
+ description: "Authentication & Authorization"
444
+ most_popular: true
445
+ priority: 5
446
+ children: []
integrations/cloud-authentication/integrations/okta_sso.md
new
+49
@@ -0,0 +1,49 @@
1
+<!--startmeta
2
+custom_edit_url: "https://github.com/netdata/netdata/edit/master/integrations/cloud-authentication/integrations/okta_sso.md"
3
+meta_yaml: "https://github.com/netdata/netdata/edit/master/integrations/cloud-authentication/metadata.yaml"
4
+sidebar_label: "Okta SSO"
5
+learn_status: "Published"
6
+learn_rel_path: "Authentication"
7
+message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE AUTHENTICATION'S metadata.yaml FILE"
8
+endmeta-->
9
+
10
+# Okta SSO
11
+
12
+
13
+<img src="https://netdata.cloud/img/okta.png" width="150"/>
14
+
15
+
16
+Integrate your organization's Okta account with Netdata to better manage your team's access controls to Netdata Cloud.
17
+
18
+
19
+<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" />
20
+
21
+## Setup
22
+
23
+### Prerequisites
24
+- An Okta account
25
+- A Netdata Cloud account
26
+- Access to the Space as an administrator
27
+- Space needs to be on Business plan or higher
28
+
29
+### Setting up Okta
30
+Steps needed to be done on Okta Admin Portal:
31
+1. Click on **Applications** tab and choose to **Browse App Catalogue**
32
+2. Find Netdata's preconfigured app for easy setup and click **Add Integration**
33
+3. Give the app the preferred **Application label**, for when it is displayed in your apps dashboard,
34
+and click Next to move to the Sign-On options tab
35
+5. In the **Sign-On Options** all the values we expect are already filled and no additional data is required
36
+6. Click **Done**. You are be able to go back and edit any fields later if need be
37
+7. Go to the **Assignments** tab and enter the People or Group assignments as per your organization’s policies
38
+
39
+### Netdata Configuration Steps
40
+1. Click on the Space settings cog (located above your profile icon)
41
+2. Click on the **Authentication** tab
42
+3. On the Okta SSO card, click on **Configure**
43
+4. Fill in the required credentials, you get them from **Okta Admin Portal**:
44
+ - **Issuer URL** you can get it from your profile icon on top, e.g. `https://company-name.okta.com`
45
+ - **Client ID** you can get it from **General** tab on application you configured on Okta
46
+ - **Client Secret** you can get it from **General** tab on application you configured on Okta
47
+ Note: [Okta - Find your application credentials](https://developer.okta.com/docs/guides/find-your-app-credentials/main/)
48
+
49
+
integrations/cloud-authentication/metadata.yaml
new
+41
@@ -0,0 +1,41 @@
1
+# yamllint disable rule:line-length
2
+---
3
+- id: 'okta-authentication'
4
+ meta:
5
+ name: 'Okta SSO'
6
+ link: 'https://netdata.cloud'
7
+ categories:
8
+ - auth
9
+ icon_filename: 'okta.png'
10
+ keywords:
11
+ - sso
12
+ - okta
13
+ - okta-sso
14
+ overview:
15
+ authentication_description: "Integrate your organization's Okta account with Netdata to better manage your team's access controls to Netdata Cloud."
16
+ authentication_limitations: ''
17
+ setup:
18
+ description: |
19
+ ### Prerequisites
20
+ - An Okta account
21
+ - A Netdata Cloud account
22
+ - Access to the Space as an administrator
23
+ - Space needs to be on the Business plan or higher
24
+
25
+ ### Setting up Okta
26
+ Steps needed to be done on Okta Admin Portal:
27
+ 1. Click on **Applications** tab and choose to **Browse App Catalogue**
28
+ 2. Find Netdata's preconfigured app for easy setup and click **Add Integration**
29
+ 3. Give the app, that will be in your apps dashboard, the preferred **Application label** and click **Next** to move to the Sign-On options tab
30
+ 4. In the **Sign-On Options** all the values we expect are already filled and no additional data is required
31
+ 5. Click **Done**. You are able to go back and edit any fields later if need be
32
+ 6. Go to the **Assignments** tab and enter the People or Group assignments as per your organization’s policies
33
+
34
+ ### Netdata Configuration Steps
35
+ 1. Click on the Space settings cog (located above your profile icon)
36
+ 2. Click on the **Authentication** tab
37
+ 3. On the Okta SSO card, click on **Configure**
38
+ 4. Fill in the [required credentials](https://developer.okta.com/docs/guides/find-your-app-credentials/main/), you get them from **Okta Admin Portal**:
39
+ - **Issuer URL** you can get it from your profile icon on top, e.g. `https://company-name.okta.com`
40
+ - **Client ID** you can get it from **General** tab on application you configured on Okta
41
+ - **Client Secret** you can get it from **General** tab on application you configured on Okta
integrations/gen_docs_integrations.py
+69
@@ -25,6 +25,9 @@ def cleanup():
25
for element in Path("integrations/cloud-notifications").glob('**/*/'):
26
if "integrations" in str(element) and not "metadata.yaml" in str(element):
27
shutil.rmtree(element)
28
+ for element in Path("integrations/cloud-authentication").glob('**/*/'):
29
+ if "integrations" in str(element) and not "metadata.yaml" in str(element):
30
+ shutil.rmtree(element)
31
32
def generate_category_from_name(category_fragment, category_array):
33
"""
@@ -80,6 +83,9 @@ def add_custom_edit_url(markdown_string, meta_yaml_link, sidebar_label_string, m
83
elif mode == 'agent-notifications':
84
path_to_md_file = meta_yaml_link.replace("metadata.yaml", "README")
85
86
+ elif mode == 'cloud-authentication':
87
+ path_to_md_file = meta_yaml_link.replace("metadata.yaml", f'integrations/{clean_string(sidebar_label_string)}')
88
+
89
output = markdown_string.replace(
90
"<!--startmeta",
91
f'<!--startmeta\ncustom_edit_url: \"{path_to_md_file}.md\"')
@@ -244,6 +250,39 @@ endmeta-->
250
251
except Exception as e:
252
print("Exception in notification md construction", e, integration['id'])
253
+
254
+ # AUTHENTICATIONS
255
+ elif mode == 'authentication':
256
+ if True:
257
+ # initiate the variables for the authentication method
258
+ meta_yaml = integration['edit_link'].replace("blob", "edit")
259
+ sidebar_label = integration['meta']['name']
260
+ learn_rel_path = generate_category_from_name(integration['meta']['categories'][0].split("."), categories)
261
+
262
+ # build the markdown string
263
+ md = \
264
+ f"""<!--startmeta
265
+meta_yaml: "{meta_yaml}"
266
+sidebar_label: "{sidebar_label}"
267
+learn_status: "Published"
268
+learn_rel_path: "{learn_rel_path.replace("authentication", "Authentication")}"
269
+message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE AUTHENTICATION'S metadata.yaml FILE"
270
+endmeta-->
271
+
272
+{create_overview(integration, integration['meta']['icon_filename'])}"""
273
+
274
+ if integration['setup']:
275
+ md += f"""
276
+{integration['setup']}
277
+"""
278
+
279
+ if integration['troubleshooting']:
280
+ md += f"""
281
+{integration['troubleshooting']}
282
+"""
283
+
284
+ # except Exception as e:
285
+ # print("Exception in authentication md construction", e, integration['id'])
286
287
if "community" in integration['meta'].keys():
288
community = "<img src=\"https://img.shields.io/badge/maintained%20by-Community-blue\" />"
@@ -329,6 +368,29 @@ def write_to_file(path, md, meta_yaml, sidebar_label, community, mode='default')
368
except FileNotFoundError as e:
369
print("Exception in writing to file", e)
370
371
+ elif mode == 'authentication':
372
+
373
+ name = clean_string(integration['meta']['name'])
374
+
375
+ if not Path(f'{path}/integrations').exists():
376
+ Path(f'{path}/integrations').mkdir()
377
+
378
+ # proper_edit_name = meta_yaml.replace(
379
+ # "metadata.yaml", f'integrations/{clean_string(sidebar_label)}.md\"')
380
+
381
+ md = add_custom_edit_url(md, meta_yaml, sidebar_label, mode='cloud-authentication')
382
+
383
+ finalpath = f'{path}/integrations/{name}.md'
384
+
385
+ try:
386
+ clean_and_write(
387
+ md,
388
+ Path(finalpath)
389
+ )
390
+
391
+ except FileNotFoundError as e:
392
+ print("Exception in writing to file", e)
393
+
394
395
def make_symlinks(symlink_dict):
396
"""
@@ -386,5 +448,12 @@ for integration in integrations:
448
path = build_path(meta_yaml)
449
write_to_file(path, md, meta_yaml, sidebar_label, community, mode='notification')
450
451
+ elif integration['integration_type'] == "authentication":
452
+
453
+ meta_yaml, sidebar_label, learn_rel_path, md, community = build_readme_from_integration(
454
+ integration, mode='authentication')
455
+ path = build_path(meta_yaml)
456
+ write_to_file(path, md, meta_yaml, sidebar_label, community, mode='authentication')
457
+
458
459
make_symlinks(symlink_dict)
integrations/gen_integrations.py
+108
-2
@@ -45,6 +45,10 @@ NOTIFICATION_SOURCES = [
45
(AGENT_REPO, INTEGRATIONS_PATH / 'cloud-notifications' / 'metadata.yaml', False),
46
]
47
48
+AUTHENTICATION_SOURCES = [
49
+ (AGENT_REPO, INTEGRATIONS_PATH / 'cloud-authentication' / 'metadata.yaml', False),
50
+]
51
+
52
COLLECTOR_RENDER_KEYS = [
53
'alerts',
54
'metrics',
@@ -66,6 +70,12 @@ NOTIFICATION_RENDER_KEYS = [
70
'troubleshooting',
71
]
72
73
+AUTHENTICATION_RENDER_KEYS = [
74
+ 'overview',
75
+ 'setup',
76
+ 'troubleshooting',
77
+]
78
+
79
CUSTOM_TAG_PATTERN = re.compile('\\{% if .*?%\\}.*?\\{% /if %\\}|\\{%.*?%\\}', flags=re.DOTALL)
80
FIXUP_BLANK_PATTERN = re.compile('\\\\\\n *\\n')
81
@@ -117,6 +127,11 @@ NOTIFICATION_VALIDATOR = Draft7Validator(
127
registry=registry,
128
)
129
130
+AUTHENTICATION_VALIDATOR = Draft7Validator(
131
+ {'$ref': './authentication.json#'},
132
+ registry=registry,
133
+)
134
+
135
COLLECTOR_VALIDATOR = Draft7Validator(
136
{'$ref': './collector.json#'},
137
registry=registry,
@@ -384,6 +399,51 @@ def load_notifications():
399
400
return ret
401
402
+def _load_authentication_file(file, repo):
403
+ debug(f'Loading { file }.')
404
+ data = load_yaml(file)
405
+
406
+ if not data:
407
+ return []
408
+
409
+ try:
410
+ AUTHENTICATION_VALIDATOR.validate(data)
411
+ except ValidationError:
412
+ warn(f'Failed to validate { file } against the schema.', file)
413
+ return []
414
+
415
+ if 'id' in data:
416
+ data['integration_type'] = 'authentication'
417
+ data['_src_path'] = file
418
+ data['_repo'] = repo
419
+ data['_index'] = 0
420
+
421
+ return [data]
422
+ else:
423
+ ret = []
424
+
425
+ for idx, item in enumerate(data):
426
+ item['integration_type'] = 'authentication'
427
+ item['_src_path'] = file
428
+ item['_repo'] = repo
429
+ item['_index'] = idx
430
+ ret.append(item)
431
+
432
+ return ret
433
+
434
+
435
+def load_authentications():
436
+ ret = []
437
+
438
+ for repo, path, match in AUTHENTICATION_SOURCES:
439
+ if match and path.exists() and path.is_dir():
440
+ for file in path.glob(METADATA_PATTERN):
441
+ ret.extend(_load_authentication_file(file, repo))
442
+ elif not match and path.exists() and path.is_file():
443
+ ret.extend(_load_authentication_file(path, repo))
444
+
445
+ return ret
446
+
447
448
def make_id(meta):
449
if 'monitored_instance' in meta:
@@ -652,6 +712,49 @@ def render_notifications(categories, notifications, ids):
712
return notifications, clean_notifications, ids
713
714
715
+def render_authentications(categories, authentications, ids):
716
+ debug('Sorting authentications.')
717
+
718
+ sort_integrations(authentications)
719
+
720
+ debug('Checking authentication ids.')
721
+
722
+ authentications, ids = dedupe_integrations(authentications, ids)
723
+
724
+ clean_authentications = []
725
+
726
+ for item in authentications:
727
+ item['edit_link'] = make_edit_link(item)
728
+
729
+ clean_item = deepcopy(item)
730
+
731
+ for key in AUTHENTICATION_RENDER_KEYS:
732
+
733
+ if key in item.keys():
734
+ template = get_jinja_env().get_template(f'{ key }.md')
735
+ data = template.render(entry=item, clean=False)
736
+ clean_data = template.render(entry=item, clean=True)
737
+
738
+ if 'variables' in item['meta']:
739
+ template = get_jinja_env().from_string(data)
740
+ data = template.render(variables=item['meta']['variables'], clean=False)
741
+ template = get_jinja_env().from_string(clean_data)
742
+ clean_data = template.render(variables=item['meta']['variables'], clean=True)
743
+ else:
744
+ data = ''
745
+ clean_data = ''
746
+
747
+ item[key] = data
748
+ clean_item[key] = clean_data
749
+
750
+ for k in ['_src_path', '_repo', '_index']:
751
+ del item[k], clean_item[k]
752
+
753
+ clean_authentications.append(clean_item)
754
+
755
+ return authentications, clean_authentications, ids
756
+
757
+
758
def render_integrations(categories, integrations):
759
template = get_jinja_env().get_template('integrations.js')
760
data = template.render(
@@ -675,16 +778,19 @@ def main():
778
deploy = load_deploy()
779
exporters = load_exporters()
780
notifications = load_notifications()
781
+ authentications = load_authentications()
782
783
collectors, clean_collectors, ids = render_collectors(categories, collectors, dict())
784
deploy, clean_deploy, ids = render_deploy(distros, categories, deploy, ids)
785
exporters, clean_exporters, ids = render_exporters(categories, exporters, ids)
786
notifications, clean_notifications, ids = render_notifications(categories, notifications, ids)
787
+ authentications, clean_authentications, ids = render_authentications(categories, authentications, ids)
788
+
789
684
- integrations = collectors + deploy + exporters + notifications
790
+ integrations = collectors + deploy + exporters + notifications + authentications
791
render_integrations(categories, integrations)
792
687
- clean_integrations = clean_collectors + clean_deploy + clean_exporters + clean_notifications
793
+ clean_integrations = clean_collectors + clean_deploy + clean_exporters + clean_notifications + clean_authentications
794
render_json(categories, clean_integrations)
795
796
integrations/schemas/authentication.json
new
+71
@@ -0,0 +1,71 @@
1
+{
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Netdata authentication mechanism metadata.",
4
+ "oneOf": [
5
+ {
6
+ "$ref": "#/$defs/entry"
7
+ },
8
+ {
9
+ "type": "array",
10
+ "minLength": 1,
11
+ "items": {
12
+ "$ref": "#/$defs/entry"
13
+ }
14
+ }
15
+ ],
16
+ "$defs": {
17
+ "entry": {
18
+ "type": "object",
19
+ "description": "Data for a single authentication method.",
20
+ "properties": {
21
+ "id": {
22
+ "$ref": "./shared.json#/$defs/id"
23
+ },
24
+ "meta": {
25
+ "$ref": "./shared.json#/$defs/instance"
26
+ },
27
+ "keywords": {
28
+ "$ref": "./shared.json#/$defs/keywords"
29
+ },
30
+ "overview": {
31
+ "type": "object",
32
+ "description": "General information about the authentication method.",
33
+ "properties": {
34
+ "authentication_description": {
35
+ "type": "string",
36
+ "description": "General description of what the authentication method does."
37
+ },
38
+ "authentication_limitations": {
39
+ "type": "string",
40
+ "description": "Explanation of any limitations of the authentication method."
41
+ }
42
+ },
43
+ "required": [
44
+ "authentication_description",
45
+ "authentication_limitations"
46
+ ]
47
+ },
48
+ "setup": {
49
+ "oneOf": [
50
+ {
51
+ "$ref": "./shared.json#/$defs/short_setup"
52
+ },
53
+ {
54
+ "$ref": "./shared.json#/$defs/full_setup"
55
+ }
56
+ ]
57
+ },
58
+ "troubleshooting": {
59
+ "$ref": "./shared.json#/$defs/troubleshooting"
60
+ }
61
+ },
62
+ "required": [
63
+ "id",
64
+ "meta",
65
+ "keywords",
66
+ "overview",
67
+ "setup"
68
+ ]
69
+ }
70
+ }
71
+}
integrations/templates/overview.md
+2
@@ -4,4 +4,6 @@
4
[% include 'overview/exporter.md' %]
5
[% elif entry.integration_type == 'notification' %]
6
[% include 'overview/notification.md' %]
7
+[% elif entry.integration_type == 'authentication' %]
8
+[% include 'overview/authentication.md' %]
9
[% endif %]
integrations/templates/overview/authentication.md
new
+9
@@ -0,0 +1,9 @@
1
+# [[ entry.meta.name ]]
2
+
3
+[[ entry.overview.authentication_description ]]
4
+[% if entry.overview.authentication_limitations %]
5
+
6
+## Limitations
7
+
8
+[[ entry.overview.authentication_limitations ]]
9
+[% endif %]