Skip to content

Commit

Permalink
Creates a new plugin for Duo that provides on-demand MFA across Dispa…
Browse files Browse the repository at this point in the history
…tch (Netflix#3035)

* initial scaffolding for duo plugin

* Create example for Slack plugin

* Add duo requirement

* remove index-url

* remove new line

* Apply black formatter

* add docstring

* Update src/dispatch/plugins/dispatch_duo/plugin.py

* Change MFA plugin type to auth-mfa and remove example
  • Loading branch information
wssheldon authored and rutvijmehta-harness committed Mar 17, 2023
1 parent 67ee226 commit 0443be6
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 1 deletion.
1 change: 1 addition & 0 deletions requirements-base.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ bcrypt
cachetools
chardet
click
duo-client
email-validator
emails
fastapi
Expand Down
5 changes: 4 additions & 1 deletion requirements-base.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.9
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile requirements-base.in
Expand Down Expand Up @@ -99,6 +99,8 @@ deprecated==1.2.13
# via atlassian-python-api
dnspython==2.2.1
# via email-validator
duo-client==4.6.1
# via -r requirements-base.in
ecdsa==0.18.0
# via python-jose
email-validator==1.3.1
Expand Down Expand Up @@ -349,6 +351,7 @@ sh==2.0.2
six==1.16.0
# via
# atlassian-python-api
# duo-client
# ecdsa
# google-auth
# google-auth-httplib2
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ def run(self):
"dispatch_pkce_auth = dispatch.plugins.dispatch_core.plugin:PKCEAuthProviderPlugin",
"dispatch_header_auth = dispatch.plugins.dispatch_core.plugin:HeaderAuthProviderPlugin",
"dispatch_ticket = dispatch.plugins.dispatch_core.plugin:DispatchTicketPlugin",
"duo_auth_mfa = dispatch.plugins.dispatch_duo.plugin:DuoMfaPlugin",
"generic_workflow = dispatch.plugins.generic_workflow.plugin:GenericWorkflowPlugin",
"github_monitor = dispatch.plugins.dispatch_github.plugin:GithubMonitorPlugin",
"google_calendar_conference = dispatch.plugins.dispatch_google.calendar.plugin:GoogleCalendarConferencePlugin",
Expand Down
1 change: 1 addition & 0 deletions src/dispatch/plugins/bases/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .document import DocumentPlugin # noqa
from .document_resolver import DocumentResolverPlugin # noqa
from .email import EmailPlugin # noqa
from .auth_mfa import MultiFactorAuthenticationPlugin # noqa
from .oncall import OncallPlugin # noqa
from .participant import ParticipantPlugin # noqa
from .participant_group import ParticipantGroupPlugin # noqa
Expand Down
15 changes: 15 additions & 0 deletions src/dispatch/plugins/bases/auth_mfa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
.. module: dispatch.plugins.bases.mfa
:platform: Unix
:copyright: (c) 2023 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Will Sheldon <[email protected]>
"""
from dispatch.plugins.base import Plugin


class MultiFactorAuthenticationPlugin(Plugin):
type = "auth-mfa"

def send_push_notification(self, items, **kwargs):
raise NotImplementedError
1 change: 1 addition & 0 deletions src/dispatch/plugins/dispatch_duo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ._version import __version__ # noqa
1 change: 1 addition & 0 deletions src/dispatch/plugins/dispatch_duo/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
18 changes: 18 additions & 0 deletions src/dispatch/plugins/dispatch_duo/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pydantic import Field, SecretStr
from dispatch.config import BaseConfigurationModel


class DuoConfiguration(BaseConfigurationModel):
"""Duo configuration description."""

integration_key: SecretStr = Field(
title="Integration Key", description="Admin API integration key ('DI...'):"
)
integration_secret_key: SecretStr = Field(
title="Integration Secret Key",
description="Secret token used in conjunction with integration key.",
)
host: str = Field(
title="API Hostname",
description="API hostname ('api-....duosecurity.com'): ",
)
83 changes: 83 additions & 0 deletions src/dispatch/plugins/dispatch_duo/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
.. module: dispatch.plugins.dispatch_duo.plugin
:platform: Unix
:copyright: (c) 2023 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Will Sheldon <[email protected]>
"""
import logging
from typing import NewType

from dispatch.decorators import apply, counter, timer
from dispatch.plugins.bases import MultiFactorAuthenticationPlugin
from dispatch.plugins.dispatch_duo import service as duo_service
from dispatch.plugins.dispatch_duo.config import DuoConfiguration
from . import __version__

log = logging.getLogger(__name__)


DuoAuthResponse = NewType(
"DuoAuthResponse",
dict[
str,
dict[str, str] | str,
],
)


@apply(timer, exclude=["__init__"])
@apply(counter, exclude=["__init__"])
class DuoMfaPlugin(MultiFactorAuthenticationPlugin):
title = "Duo Plugin - Multi Factor Authentication"
slug = "duo-auth-mfa"
description = "Uses Duo to validate user actions with multi-factor authentication."
version = __version__

author = "Netflix"
author_url = "https://github.com/netflix/dispatch.git"

def __init__(self):
self.configuration_schema = DuoConfiguration

def send_push_notification(
self,
username: str,
type: str,
device: str = "auto",
) -> DuoAuthResponse:
"""Create a new push notification for authentication.
This function sends a push notification to a Duo-enabled device for multi-factor authentication.
Args:
username (str): The unique identifier for the user, commonly specified by your application during user
creation (e.g. [email protected]). This value may also represent a username alias assigned to a user (e.g. wshel).
type (str): A string that is displayed in the Duo Mobile app push notification and UI. The notification text
changes to "Verify request" and shows your customized string followed by a colon and the application's name,
and the request details screen also shows your customized string and the application's name.
device (str, optional): The ID of the device. This device must have the "push" capability. Defaults to "auto"
to use the first of the user's devices with the "push" capability.
Returns:
DuoAuthResponse: The response from the Duo API. A successful response would appear as:
{"response": {"result": "allow", "status": "allow", "status_msg": "Success. Logging you in..."}, "stat": "OK"}
Example:
>>> plugin = DuoMfaPlugin()
>>> result = plugin.send_push_notification(username='[email protected]', type='Login Request')
>>> result
{'response': {'result': 'allow', 'status': 'allow', 'status_msg': 'Success. Logging you in...'}, 'stat': 'OK'}
Notes:
For more information, see https://duo.com/docs/authapi#/auth
"""
duo_client = duo_service.create_duo_auth_client(self.configuration)
return duo_client.auth(
factor="push",
username=username,
device=device,
type=type,
)
13 changes: 13 additions & 0 deletions src/dispatch/plugins/dispatch_duo/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import duo_client
from duo_client.auth import Auth

from dispatch.plugins.dispatch_duo.config import DuoConfiguration


def create_duo_auth_client(config: DuoConfiguration) -> Auth:
"""Creates a Duo Auth API client."""
return duo_client.Auth(
ikey=config.integration_key.get_secret_value(),
skey=config.integration_secret_key.get_secret_value(),
host=config.host,
)

0 comments on commit 0443be6

Please sign in to comment.