@cryptotaxi247 / netdata-1 / commits / d9c5f2045

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 %]