# Copyright 2019 Extreme Networks, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import six
import jsonschema
from jinja2.exceptions import UndefinedError
from oslo_config import cfg

from st2api.controllers.base import BaseRestControllerMixin
from st2common import log as logging
from st2common.exceptions.actionalias import ActionAliasAmbiguityException
from st2common.exceptions.db import StackStormDBObjectNotFoundError
from st2common.models.api.action import ActionAliasAPI
from st2common.models.api.action import AliasMatchAndExecuteInputAPI
from st2common.models.api.auth import get_system_username
from st2common.models.api.execution import ActionExecutionAPI
from st2common.models.db.auth import UserDB
from st2common.models.db.liveaction import LiveActionDB
from st2common.models.db.notification import NotificationSchema, NotificationSubSchema
from st2common.models.utils import action_param_utils
from st2common.models.utils.action_alias_utils import extract_parameters_for_action_alias_db
from st2common.persistence.actionalias import ActionAlias
from st2common.services import action as action_service
from st2common.util import action_db as action_utils
from st2common.util import reference
from st2common.util.actionalias_matching import get_matching_alias
from st2common.util.jinja import render_values as render
from st2common.rbac.types import PermissionType
from st2common.rbac.backends import get_rbac_backend
from st2common.router import abort
from st2common.router import Response

http_client = six.moves.http_client

LOG = logging.getLogger(__name__)

CAST_OVERRIDES = {
    'array': (lambda cs_x: [v.strip() for v in cs_x.split(',')])
}


class ActionAliasExecutionController(BaseRestControllerMixin):
    def match_and_execute(self, input_api, requester_user, show_secrets=False):
        """
            Try to find a matching alias and if one is found, schedule a new
            execution by parsing parameters from the provided command against
            the matched alias.

            Handles requests:
                POST /aliasexecution/match_and_execute
        """
        command = input_api.command

        try:
            format_ = get_matching_alias(command=command)
        except ActionAliasAmbiguityException as e:
            LOG.exception('Command "%s" matched (%s) patterns.', e.command, len(e.matches))
            return abort(http_client.BAD_REQUEST, six.text_type(e))

        action_alias_db = format_['alias']
        representation = format_['representation']

        params = {
            'name': action_alias_db.name,
            'format': representation,
            'command': command,
            'user': input_api.user,
            'source_channel': input_api.source_channel,
        }

        # Add in any additional parameters provided by the user
        if input_api.notification_channel:
            params['notification_channel'] = input_api.notification_channel

        if input_api.notification_route:
            params['notification_route'] = input_api.notification_route

        alias_execution_api = AliasMatchAndExecuteInputAPI(**params)
        results = self._post(
            payload=alias_execution_api,
            requester_user=requester_user,
            show_secrets=show_secrets,
            match_multiple=format_['match_multiple'])
        return Response(json={'results': results}, status=http_client.CREATED)

    def _post(self, payload, requester_user, show_secrets=False, match_multiple=False):
        action_alias_name = payload.name if payload else None

        if not action_alias_name:
            abort(http_client.BAD_REQUEST, 'Alias execution "name" is required')
            return

        if not requester_user:
            requester_user = UserDB(cfg.CONF.system_user.user)

        format_str = payload.format or ''
        command = payload.command or ''

        try:
            action_alias_db = ActionAlias.get_by_name(action_alias_name)
        except ValueError:
            action_alias_db = None

        if not action_alias_db:
            msg = 'Unable to identify action alias with name "%s".' % (action_alias_name)
            abort(http_client.NOT_FOUND, msg)
            return

        if not action_alias_db.enabled:
            msg = 'Action alias with name "%s" is disabled.' % (action_alias_name)
            abort(http_client.BAD_REQUEST, msg)
            return

        if match_multiple:
            multiple_execution_parameters = extract_parameters_for_action_alias_db(
                action_alias_db=action_alias_db,
                format_str=format_str,
                param_stream=command,
                match_multiple=match_multiple)
        else:
            multiple_execution_parameters = [
                extract_parameters_for_action_alias_db(
                    action_alias_db=action_alias_db,
                    format_str=format_str,
                    param_stream=command,
                    match_multiple=match_multiple)
            ]

        notify = self._get_notify_field(payload)

        context = {
            'action_alias_ref': reference.get_ref_from_model(action_alias_db),
            'api_user': payload.user,
            'user': requester_user.name,
        }

        results = []
        for execution_parameters in multiple_execution_parameters:
            execution = self._schedule_execution(action_alias_db=action_alias_db,
                                                 params=execution_parameters,
                                                 notify=notify,
                                                 context=context,
                                                 show_secrets=show_secrets,
                                                 requester_user=requester_user)

            result = {
                'execution': execution,
                'actionalias': ActionAliasAPI.from_model(action_alias_db)
            }

            if action_alias_db.ack:
                try:
                    if 'format' in action_alias_db.ack:
                        message = render({'alias': action_alias_db.ack['format']}, result)['alias']

                        result.update({
                            'message': message
                        })
                except UndefinedError as e:
                    result.update({
                        'message': ('Cannot render "format" in field "ack" for alias. ' +
                                    six.text_type(e))
                    })

                try:
                    if 'extra' in action_alias_db.ack:
                        result.update({
                            'extra': render(action_alias_db.ack['extra'], result)
                        })
                except UndefinedError as e:
                    result.update({
                        'extra': ('Cannot render "extra" in field "ack" for alias. ' +
                                  six.text_type(e))
                    })

            results.append(result)

        return results

    def post(self, payload, requester_user, show_secrets=False):
        results = self._post(payload, requester_user, show_secrets, match_multiple=False)
        return Response(json=results[0], status=http_client.CREATED)

    def _tokenize_alias_execution(self, alias_execution):
        tokens = alias_execution.strip().split(' ', 1)
        return (tokens[0], tokens[1] if len(tokens) > 1 else None)

    def _get_notify_field(self, payload):
        on_complete = NotificationSubSchema()
        route = (getattr(payload, 'notification_route', None) or
                 getattr(payload, 'notification_channel', None))
        on_complete.routes = [route]
        on_complete.data = {
            'user': payload.user,
            'source_channel': payload.source_channel,
            'source_context': getattr(payload, 'source_context', None),
        }
        notify = NotificationSchema()
        notify.on_complete = on_complete
        return notify

    def _schedule_execution(self, action_alias_db, params, notify, context, requester_user,
                            show_secrets):
        action_ref = action_alias_db.action_ref
        action_db = action_utils.get_action_by_ref(action_ref)

        if not action_db:
            raise StackStormDBObjectNotFoundError('Action with ref "%s" not found ' % (action_ref))

        rbac_utils = get_rbac_backend().get_utils_class()
        permission_type = PermissionType.ACTION_EXECUTE
        rbac_utils.assert_user_has_resource_db_permission(user_db=requester_user,
                                                          resource_db=action_db,
                                                          permission_type=permission_type)

        try:
            # prior to shipping off the params cast them to the right type.
            params = action_param_utils.cast_params(action_ref=action_alias_db.action_ref,
                                                    params=params,
                                                    cast_overrides=CAST_OVERRIDES)
            if not context:
                context = {
                    'action_alias_ref': reference.get_ref_from_model(action_alias_db),
                    'user': get_system_username()
                }
            liveaction = LiveActionDB(action=action_alias_db.action_ref, context=context,
                                      parameters=params, notify=notify)
            _, action_execution_db = action_service.request(liveaction)
            mask_secrets = self._get_mask_secrets(requester_user, show_secrets=show_secrets)
            return ActionExecutionAPI.from_model(action_execution_db, mask_secrets=mask_secrets)
        except ValueError as e:
            LOG.exception('Unable to execute action.')
            abort(http_client.BAD_REQUEST, six.text_type(e))
        except jsonschema.ValidationError as e:
            LOG.exception('Unable to execute action. Parameter validation failed.')
            abort(http_client.BAD_REQUEST, six.text_type(e))
        except Exception as e:
            LOG.exception('Unable to execute action. Unexpected error encountered.')
            abort(http_client.INTERNAL_SERVER_ERROR, six.text_type(e))


action_alias_execution_controller = ActionAliasExecutionController()

st2api/st2api/controllers/v1/aliasexecution.py
# Copyright 2019 Extreme Networks, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import six
import jsonschema
from jinja2.exceptions import UndefinedError
from oslo_config import cfg

from st2api.controllers.base import BaseRestControllerMixin
from st2common import log as logging
from st2common.exceptions.actionalias import ActionAliasAmbiguityException
from st2common.exceptions.db import StackStormDBObjectNotFoundError
from st2common.models.api.action import ActionAliasAPI
from st2common.models.api.action import AliasMatchAndExecuteInputAPI
from st2common.models.api.auth import get_system_username
from st2common.models.api.execution import ActionExecutionAPI
from st2common.models.db.auth import UserDB
from st2common.models.db.liveaction import LiveActionDB
from st2common.models.db.notification import NotificationSchema, NotificationSubSchema
from st2common.models.utils import action_param_utils
from st2common.models.utils.action_alias_utils import extract_parameters_for_action_alias_db
from st2common.persistence.actionalias import ActionAlias
from st2common.services import action as action_service
from st2common.util import action_db as action_utils
from st2common.util import reference
from st2common.util.actionalias_matching import get_matching_alias
from st2common.util.jinja import render_values as render
from st2common.rbac.types import PermissionType
from st2common.rbac.backends import get_rbac_backend
from st2common.router import abort
from st2common.router import Response

http_client = six.moves.http_client

LOG = logging.getLogger(__name__)

CAST_OVERRIDES = {
    'array': (lambda cs_x: [v.strip() for v in cs_x.split(',')])
}


class ActionAliasExecutionController(BaseRestControllerMixin):
    def match_and_execute(self, input_api, requester_user, show_secrets=False):
        """
            Try to find a matching alias and if one is found, schedule a new
            execution by parsing parameters from the provided command against
            the matched alias.

            Handles requests:
                POST /aliasexecution/match_and_execute
        """
        command = input_api.command

        try:
            format_ = get_matching_alias(command=command)
        except ActionAliasAmbiguityException as e:
            LOG.exception('Command "%s" matched (%s) patterns.', e.command, len(e.matches))
            return abort(http_client.BAD_REQUEST, six.text_type(e))

        action_alias_db = format_['alias']
        representation = format_['representation']

        params = {
            'name': action_alias_db.name,
            'format': representation,
            'command': command,
            'user': input_api.user,
            'source_channel': input_api.source_channel,
        }

        # Add in any additional parameters provided by the user
        if input_api.notification_channel:
            params['notification_channel'] = input_api.notification_channel

        if input_api.notification_route:
            params['notification_route'] = input_api.notification_route

        alias_execution_api = AliasMatchAndExecuteInputAPI(**params)
        results = self._post(
            payload=alias_execution_api,
            requester_user=requester_user,
            show_secrets=show_secrets,
            match_multiple=format_['match_multiple'])
        return Response(json={'results': results}, status=http_client.CREATED)

    def _post(self, payload, requester_user, show_secrets=False, match_multiple=False):
        action_alias_name = payload.name if payload else None

        if not action_alias_name:
            abort(http_client.BAD_REQUEST, 'Alias execution "name" is required')
            return

        if not requester_user:
            requester_user = UserDB(cfg.CONF.system_user.user)

        format_str = payload.format or ''
        command = payload.command or ''

        try:
            action_alias_db = ActionAlias.get_by_name(action_alias_name)
        except ValueError:
            action_alias_db = None

        if not action_alias_db:
            msg = 'Unable to identify action alias with name "%s".' % (action_alias_name)
            abort(http_client.NOT_FOUND, msg)
            return

        if not action_alias_db.enabled:
            msg = 'Action alias with name "%s" is disabled.' % (action_alias_name)
            abort(http_client.BAD_REQUEST, msg)
            return

        if match_multiple:
            multiple_execution_parameters = extract_parameters_for_action_alias_db(
                action_alias_db=action_alias_db,
                format_str=format_str,
                param_stream=command,
                match_multiple=match_multiple)
        else:
            multiple_execution_parameters = [
                extract_parameters_for_action_alias_db(
                    action_alias_db=action_alias_db,
                    format_str=format_str,
                    param_stream=command,
                    match_multiple=match_multiple)
            ]

        notify = self._get_notify_field(payload)

        context = {
            'action_alias_ref': reference.get_ref_from_model(action_alias_db),
            'api_user': payload.user,
            'user': requester_user.name,
            'source_channel': payload.source_channel,
        }

        results = []
        for execution_parameters in multiple_execution_parameters:
            execution = self._schedule_execution(action_alias_db=action_alias_db,
                                                 params=execution_parameters,
                                                 notify=notify,
                                                 context=context,
                                                 show_secrets=show_secrets,
                                                 requester_user=requester_user)

            result = {
                'execution': execution,
                'actionalias': ActionAliasAPI.from_model(action_alias_db)
            }

            if action_alias_db.ack:
                try:
                    if 'format' in action_alias_db.ack:
                        message = render({'alias': action_alias_db.ack['format']}, result)['alias']

                        result.update({
                            'message': message
                        })
                except UndefinedError as e:
                    result.update({
                        'message': ('Cannot render "format" in field "ack" for alias. ' +
                                    six.text_type(e))
                    })

                try:
                    if 'extra' in action_alias_db.ack:
                        result.update({
                            'extra': render(action_alias_db.ack['extra'], result)
                        })
                except UndefinedError as e:
                    result.update({
                        'extra': ('Cannot render "extra" in field "ack" for alias. ' +
                                  six.text_type(e))
                    })

            results.append(result)

        return results

    def post(self, payload, requester_user, show_secrets=False):
        results = self._post(payload, requester_user, show_secrets, match_multiple=False)
        return Response(json=results[0], status=http_client.CREATED)

    def _tokenize_alias_execution(self, alias_execution):
        tokens = alias_execution.strip().split(' ', 1)
        return (tokens[0], tokens[1] if len(tokens) > 1 else None)

    def _get_notify_field(self, payload):
        on_complete = NotificationSubSchema()
        route = (getattr(payload, 'notification_route', None) or
                 getattr(payload, 'notification_channel', None))
        on_complete.routes = [route]
        on_complete.data = {
            'user': payload.user,
            'source_channel': payload.source_channel,
            'source_context': getattr(payload, 'source_context', None),
        }
        notify = NotificationSchema()
        notify.on_complete = on_complete
        return notify

    def _schedule_execution(self, action_alias_db, params, notify, context, requester_user,
                            show_secrets):
        action_ref = action_alias_db.action_ref
        action_db = action_utils.get_action_by_ref(action_ref)

        if not action_db:
            raise StackStormDBObjectNotFoundError('Action with ref "%s" not found ' % (action_ref))

        rbac_utils = get_rbac_backend().get_utils_class()
        permission_type = PermissionType.ACTION_EXECUTE
        rbac_utils.assert_user_has_resource_db_permission(user_db=requester_user,
                                                          resource_db=action_db,
                                                          permission_type=permission_type)

        try:
            # prior to shipping off the params cast them to the right type.
            params = action_param_utils.cast_params(action_ref=action_alias_db.action_ref,
                                                    params=params,
                                                    cast_overrides=CAST_OVERRIDES)
            if not context:
                context = {
                    'action_alias_ref': reference.get_ref_from_model(action_alias_db),
                    'user': get_system_username()
                }
            liveaction = LiveActionDB(action=action_alias_db.action_ref, context=context,
                                      parameters=params, notify=notify)
            _, action_execution_db = action_service.request(liveaction)
            mask_secrets = self._get_mask_secrets(requester_user, show_secrets=show_secrets)
            return ActionExecutionAPI.from_model(action_execution_db, mask_secrets=mask_secrets)
        except ValueError as e:
            LOG.exception('Unable to execute action.')
            abort(http_client.BAD_REQUEST, six.text_type(e))
        except jsonschema.ValidationError as e:
            LOG.exception('Unable to execute action. Parameter validation failed.')
            abort(http_client.BAD_REQUEST, six.text_type(e))
        except Exception as e:
            LOG.exception('Unable to execute action. Unexpected error encountered.')
            abort(http_client.INTERNAL_SERVER_ERROR, six.text_type(e))


action_alias_execution_controller = ActionAliasExecutionController()

st2api/st2api/controllers/v1/aliasexecution.py
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 80de6042fb..8fc416f4c7 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -13,6 +13,11 @@ Fixed
   (``ssh_runner.ssh_config_file_path``). (bug fix) #4660 #4661
 * Update pack install action so it works correctly when ``python_versions`` ``pack.yaml`` metadata
   attribute is used in combination with ``--python3`` pack install flag. (bug fix) #4654 #4662
+* Add ``source_channel`` back to the context used by Mistral workflows for executions which are
+  triggered via ChatOps (using action alias).
+
+  In StackStorm v3.0.0, this variable was inadvertently removed from the context used by Mistral
+  workflows. (bug fix) #4650 #4656
 
 3.0.0 - April 18, 2019
 ----------------------
diff --git a/st2api/st2api/controllers/v1/aliasexecution.py b/st2api/st2api/controllers/v1/aliasexecution.py
index 8abb69b3e8..b14750630f 100644
--- a/st2api/st2api/controllers/v1/aliasexecution.py
+++ b/st2api/st2api/controllers/v1/aliasexecution.py
@@ -143,6 +143,7 @@ def _post(self, payload, requester_user, show_secrets=False, match_multiple=Fals
             'action_alias_ref': reference.get_ref_from_model(action_alias_db),
             'api_user': payload.user,
             'user': requester_user.name,
+            'source_channel': payload.source_channel,
         }
 
         results = []
diff --git a/st2api/tests/unit/controllers/v1/test_alias_execution.py b/st2api/tests/unit/controllers/v1/test_alias_execution.py
index fab96debd5..f89f998d50 100644
--- a/st2api/tests/unit/controllers/v1/test_alias_execution.py
+++ b/st2api/tests/unit/controllers/v1/test_alias_execution.py
@@ -187,9 +187,9 @@ def test_match_and_execute_matches_many(self, mock_request):
                        return_value=(None, EXECUTION))
     def test_match_and_execute_matches_one(self, mock_request):
         base_data = {
-            'source_channel': 'chat',
+            'source_channel': 'chat-channel',
             'notification_route': 'hubot',
-            'user': 'chat-user'
+            'user': 'chat-user',
         }
 
         # Command matches - should result in action execution
@@ -204,6 +204,15 @@ def test_match_and_execute_matches_one(self, mock_request):
         expected_parameters = {'cmd': 'date', 'hosts': 'localhost'}
         self.assertEquals(mock_request.call_args[0][0].parameters, expected_parameters)
 
+        # Also check for source_channel - see
+        # https://github.com/StackStorm/st2/issues/4650
+        actual_context = mock_request.call_args[0][0].context
+
+        self.assertTrue('source_channel' in mock_request.call_args[0][0].context.keys())
+        self.assertEquals(actual_context['source_channel'], 'chat-channel')
+        self.assertEquals(actual_context['api_user'], 'chat-user')
+        self.assertEquals(actual_context['user'], 'stanley')
+
     @mock.patch.object(action_service, 'request',
                        return_value=(None, EXECUTION))
     def test_match_and_execute_matches_one_multiple_match(self, mock_request):

3.0 - `source_channel` no longer working for Mistral workflows
##### SUMMARY

In ST2 3.0, Mistral workflows no longer able to identify `source_channel` that alias was called from. 

##### ISSUE TYPE
 - Bug Report

##### STACKSTORM VERSION

`st2 3.0.0, on Python 2.7.12`

##### OS / ENVIRONMENT / INSTALL METHOD

one-liner, on Ubuntu 16.04. 

System upgraded from 2.10.4 to 3.0.0.

##### STEPS TO REPRODUCE

Create a Mistral workflow that uses `source_channel`, and call it via a Chat alias.

Mistral workflow [here](https://github.com/StackStorm/st2_demos/blob/master/actions/workflows/multicap.yaml#L20).

Alias [here](https://github.com/StackStorm/st2_demos/blob/master/aliases/multicap.yaml).

Key lines:
```yaml
input:
    channel: "<% env().get('__actions').get('st2.action').st2_context.parent.source_channel %>"
    message: "Setting up environment and starting captures now...{~}"
on-success:
```

##### EXPECTED RESULTS

Workflow should post back to the original channel with my message 

> Setting up environment and starting captures now...

##### ACTUAL RESULTS

Workflow fails, because it can't get the value of source_channel. Error similar to

```
"Failed to run task [error=Can not evaluate YAQL expression [expression=env().get('__actions').get('st2.action').st2_context.parent.source_channel, error=u'source_channel', data={}], wf=st2_demos.multicap.main, task=notify_slack]:
```

This stems from https://github.com/StackStorm/st2/pull/4609/files#diff-f2911a69b88d56311f8945335e9457b7L148, which is part of PR #4609. If I add that removed line back to `aliasexecution.py`, it works as expected.

Looks like this data may be available in the `notify` dict.

/cc @blag @m4dcoder @emptywee 
3.0 - `source_channel` no longer working for Mistral workflows
##### SUMMARY

In ST2 3.0, Mistral workflows no longer able to identify `source_channel` that alias was called from. 

##### ISSUE TYPE
 - Bug Report

##### STACKSTORM VERSION

`st2 3.0.0, on Python 2.7.12`

##### OS / ENVIRONMENT / INSTALL METHOD

one-liner, on Ubuntu 16.04. 

System upgraded from 2.10.4 to 3.0.0.

##### STEPS TO REPRODUCE

Create a Mistral workflow that uses `source_channel`, and call it via a Chat alias.

Mistral workflow [here](https://github.com/StackStorm/st2_demos/blob/master/actions/workflows/multicap.yaml#L20).

Alias [here](https://github.com/StackStorm/st2_demos/blob/master/aliases/multicap.yaml).

Key lines:
```yaml
input:
    channel: "<% env().get('__actions').get('st2.action').st2_context.parent.source_channel %>"
    message: "Setting up environment and starting captures now...{~}"
on-success:
```

##### EXPECTED RESULTS

Workflow should post back to the original channel with my message 

> Setting up environment and starting captures now...

##### ACTUAL RESULTS

Workflow fails, because it can't get the value of source_channel. Error similar to

```
"Failed to run task [error=Can not evaluate YAQL expression [expression=env().get('__actions').get('st2.action').st2_context.parent.source_channel, error=u'source_channel', data={}], wf=st2_demos.multicap.main, task=notify_slack]:
```

This stems from https://github.com/StackStorm/st2/pull/4609/files#diff-f2911a69b88d56311f8945335e9457b7L148, which is part of PR #4609. If I add that removed line back to `aliasexecution.py`, it works as expected.

Looks like this data may be available in the `notify` dict.

/cc @blag @m4dcoder @emptywee 
