diff --git a/.circleci/config.yml b/.circleci/config.yml index 2dbafee..6ddec1e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -108,7 +108,7 @@ jobs: command: sudo apt -y install gmic optipng - run: name: Update exchange.stackstorm.org - command: ~/ci/.circle/deployment + command: ~/ci/.circle/deployment workflows: version: 2 diff --git a/CHANGES.md b/CHANGES.md index 30d97f7..735babc 100755 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Change Log +## 0.14.0 + +- Add `add_watcher` and `update_reporter` action +- Add `client_cert_file` and `client_key_file` for certificate based authentication with JIRA server + ## 0.13.0 - Add ``validate`` option to pack config to enable validating credentials diff --git a/README.md b/README.md index 41c4fd0..f504a17 100755 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ to `/opt/stackstorm/configs/jira.yaml` and edit as required. * ``verify`` - Verify SSL certificates. Default True. Set to False to disable verification * ``auth_method`` - Specify either `basic` or `oauth` authentication +Include following settings for certificate based authentication with JIRA server + +* ``client_cert_file`` Path to a client cert file, e.g. /home/jiracerts/username.cer +* ``client_key_file`` Path to a client key file, e.g. /home/jiracerts/username.key + Include the following settings when using the `oauth` auth_method: * ``rsa_cert_file`` - Path to the file with a private key * ``oauth_token`` - OAuth token @@ -90,3 +95,14 @@ The sensor monitors for new tickets and sends a trigger into the system whenever * ``create_issue`` - Action which creates a new JIRA issue. * ``get_issue`` - Action which retrieves details for a particular issue. +* ``add_field_value`` - Add a field to a particular JIRA issue. +* ``add_watcher`` - Add a watcher on a JIRA issue. +* ``attach_file_to_issue`` - Attach a file to JIRA issue / ticket. +* ``attach_files_to_issue`` - Attach multiple files to JIRA issue / ticket. +* ``comment_issue`` - Comment on a JIRA issue / ticket. +* ``create_issue`` - Create a new JIRA issue / ticket. +* ``get_issue_attachments`` - Retrieve attachments for a particular JIRA issue. +* ``search_issues`` - Search JIRA issues with a JQL query +* ``transition_issue`` - Do a transition on a JIRA issue / ticket. +* ``update_field_value`` - Update a field in a particular JIRA issue. +* ``update_reporter`` - Update the reporter of a particular JIRA issue. diff --git a/actions/add_watcher.py b/actions/add_watcher.py new file mode 100755 index 0000000..3abb29f --- /dev/null +++ b/actions/add_watcher.py @@ -0,0 +1,8 @@ +from lib.base import BaseJiraAction + +__all__ = ["JiraIssueAddWatcherAction"] + + +class JiraIssueAddWatcherAction(BaseJiraAction): + def run(self, issue_key, username): + return self._client.add_watcher(issue_key, username) diff --git a/actions/add_watcher.yaml b/actions/add_watcher.yaml new file mode 100755 index 0000000..74a0ff8 --- /dev/null +++ b/actions/add_watcher.yaml @@ -0,0 +1,15 @@ +--- +name: add_watcher +runner_type: python-script +description: Add a watcher on a JIRA issue / ticket. +enabled: true +entry_point: add_watcher.py +parameters: + username: + type: string + description: User to add as a watcher. + required: true + issue_key: + type: string + description: Issue key (e.g. PROJECT-1000). + required: true diff --git a/actions/lib/base.py b/actions/lib/base.py index 9ca50e3..15dbe8c 100755 --- a/actions/lib/base.py +++ b/actions/lib/base.py @@ -1,9 +1,7 @@ from jira import JIRA # from st2common.runners.base_action import Action -__all__ = [ - 'BaseJiraAction' -] +__all__ = ['BaseJiraAction'] class Action(object): @@ -21,6 +19,12 @@ def _get_client(self): options = {'server': config['url'], 'verify': config['verify']} + # Getting client cert configuration + cert_file_path = config['client_cert_file'] + key_file_path = config['client_key_file'] + if cert_file_path and key_file_path: + options['client_cert'] = (cert_file_path, key_file_path) + auth_method = config['auth_method'] if auth_method == 'oauth': @@ -31,19 +35,24 @@ def _get_client(self): 'access_token': config['oauth_token'], 'access_token_secret': config['oauth_secret'], 'consumer_key': config['consumer_key'], - 'key_cert': rsa_key_content + 'key_cert': rsa_key_content, } client = JIRA(options=options, oauth=oauth_creds) elif auth_method == 'basic': basic_creds = (config['username'], config['password']) - client = JIRA(options=options, basic_auth=basic_creds, - validate=config.get('validate', False)) + client = JIRA( + options=options, + basic_auth=basic_creds, + validate=config.get('validate', False), + ) else: - msg = ('You must set auth_method to either "oauth"', - 'or "basic" your jira.yaml config file.') + msg = ( + "You must set auth_method to either 'oauth'", + "or 'basic' your jira.yaml config file.", + ) raise Exception(msg) return client diff --git a/actions/update_reporter.py b/actions/update_reporter.py new file mode 100644 index 0000000..5f23b10 --- /dev/null +++ b/actions/update_reporter.py @@ -0,0 +1,13 @@ +from lib.base import BaseJiraAction + +__all__ = [ + 'UpdateReporterValue' +] + + +class UpdateReporterValue(BaseJiraAction): + def run(self, issue_key, username, notify): + issue = self._client.issue(issue_key) + issue.update(reporter={'name': username}, notify=notify) + result = issue.fields.labels + return result diff --git a/actions/update_reporter.yaml b/actions/update_reporter.yaml new file mode 100644 index 0000000..cd0f668 --- /dev/null +++ b/actions/update_reporter.yaml @@ -0,0 +1,20 @@ +--- +name: update_reporter +runner_type: python-script +description: Update the reporter of a particular JIRA issue. +enabled: true +entry_point: update_reporter.py +parameters: + issue_key: + type: string + description: Issue key (e.g. PROJECT-1000). + required: true + username: + type: string + description: the reporter name. + required: true + notify: + type: boolean + description: jira will send notifications (default is true) + default: true + required: false diff --git a/config.schema.yaml b/config.schema.yaml index 4ad712f..b50baa2 100755 --- a/config.schema.yaml +++ b/config.schema.yaml @@ -63,3 +63,13 @@ type: boolean default: false required: false + client_cert_file: + description: "Path to a client cert file, e.g. /home/jiracerts/username.cer" + type: "string" + secret: false + required: false + client_key_file: + description: "Path to a client key file, e.g. /home/jiracerts/username.key" + type: "string" + secret: false + required: false diff --git a/pack.yaml b/pack.yaml index 9c36d76..f157124 100755 --- a/pack.yaml +++ b/pack.yaml @@ -6,7 +6,7 @@ keywords: - issues - ticket management - project management -version: 0.13.0 +version: 0.14.0 python_versions: - "2" - "3" diff --git a/sensors/jira_sensor.py b/sensors/jira_sensor.py index c168d40..f72cf0c 100755 --- a/sensors/jira_sensor.py +++ b/sensors/jira_sensor.py @@ -11,13 +11,14 @@ class JIRASensor(PollingSensor): Sensor will monitor for any new projects created in JIRA and emit trigger instance when one is created. ''' + def __init__(self, sensor_service, config=None, poll_interval=5): - super(JIRASensor, self).__init__(sensor_service=sensor_service, - config=config, - poll_interval=poll_interval) + super(JIRASensor, self).__init__(sensor_service=sensor_service, + config=config, + poll_interval=poll_interval) self._jira_url = None - # The Consumer Key created while setting up the "Incoming Authentication" in + # The Consumer Key created while setting up the 'Incoming Authentication' in # JIRA for the Application Link. self._consumer_key = u'' self._rsa_key = None @@ -41,29 +42,39 @@ def setup(self): self._jira_url = self._config['url'] auth_method = self._config['auth_method'] + options = {'server': self._config['url'], + 'verify': self._config['verify']} + # Getting client cert configuration + cert_file_path = self._config['client_cert_file'] + key_file_path = self._config['client_key_file'] + if cert_file_path and key_file_path: + options['client_cert'] = (cert_file_path, key_file_path) + if auth_method == 'oauth': rsa_cert_file = self._config['rsa_cert_file'] if not os.path.exists(rsa_cert_file): - raise Exception('Cert file for JIRA OAuth not found at %s.' % rsa_cert_file) + raise Exception( + 'Cert file for JIRA OAuth not found at %s.' % rsa_cert_file + ) self._rsa_key = self._read_cert(rsa_cert_file) - self._poll_interval = self._config.get('poll_interval', self._poll_interval) + self._poll_interval = self._config.get( + 'poll_interval', self._poll_interval) oauth_creds = { 'access_token': self._config['oauth_token'], 'access_token_secret': self._config['oauth_secret'], 'consumer_key': self._config['consumer_key'], - 'key_cert': self._rsa_key + 'key_cert': self._rsa_key, } - self._jira_client = JIRA(options={'server': self._jira_url}, - oauth=oauth_creds) + self._jira_client = JIRA(options=options, oauth=oauth_creds) elif auth_method == 'basic': basic_creds = (self._config['username'], self._config['password']) - self._jira_client = JIRA(options={'server': self._jira_url}, - basic_auth=basic_creds) + self._jira_client = JIRA(options=options, basic_auth=basic_creds) else: msg = ('You must set auth_method to either "oauth"', - 'or "basic" your jira.yaml config file.') + 'or "basic" your jira.yaml config file.', + ) raise Exception(msg) if self._projects_available is None: @@ -74,7 +85,8 @@ def setup(self): if not self._project or self._project not in self._projects_available: raise Exception('Invalid project (%s) to track.' % self._project) self._jql_query = 'project=%s' % self._project - all_issues = self._jira_client.search_issues(self._jql_query, maxResults=None) + all_issues = self._jira_client.search_issues( + self._jql_query, maxResults=None) self._issues_in_project = {issue.key: issue for issue in all_issues} def poll(self): @@ -93,7 +105,9 @@ def remove_trigger(self, trigger): pass def _detect_new_issues(self): - new_issues = self._jira_client.search_issues(self._jql_query, maxResults=50, startAt=0) + new_issues = self._jira_client.search_issues( + self._jql_query, maxResults=50, startAt=0 + ) for issue in new_issues: if issue.key not in self._issues_in_project: diff --git a/sensors/jira_sensor_for_apiv2.py b/sensors/jira_sensor_for_apiv2.py index c676225..cca38e7 100755 --- a/sensors/jira_sensor_for_apiv2.py +++ b/sensors/jira_sensor_for_apiv2.py @@ -11,13 +11,14 @@ class JIRASensorForAPIv2(PollingSensor): Sensor will monitor for any new projects created in JIRA and emit trigger instance when one is created. ''' + def __init__(self, sensor_service, config=None, poll_interval=5): - super(JIRASensorForAPIv2, self).__init__(sensor_service=sensor_service, - config=config, - poll_interval=poll_interval) + super(JIRASensorForAPIv2, self).__init__( + sensor_service=sensor_service, config=config, poll_interval=poll_interval + ) self._jira_url = None - # The Consumer Key created while setting up the "Incoming Authentication" in + # The Consumer Key created while setting up the 'Incoming Authentication' in # JIRA for the Application Link. self._consumer_key = u'' self._rsa_key = None @@ -41,29 +42,40 @@ def setup(self): self._jira_url = self._config['url'] auth_method = self._config['auth_method'] + options = {'server': self._config['url'], + 'verify': self._config['verify']} + # Getting client cert configuration + cert_file_path = self._config['client_cert_file'] + key_file_path = self._config['client_key_file'] + if cert_file_path and key_file_path: + options['client_cert'] = (cert_file_path, key_file_path) + if auth_method == 'oauth': rsa_cert_file = self._config['rsa_cert_file'] if not os.path.exists(rsa_cert_file): - raise Exception('Cert file for JIRA OAuth not found at %s.' % rsa_cert_file) + raise Exception( + 'Cert file for JIRA OAuth not found at %s.' % rsa_cert_file + ) self._rsa_key = self._read_cert(rsa_cert_file) - self._poll_interval = self._config.get('poll_interval', self._poll_interval) + self._poll_interval = self._config.get( + 'poll_interval', self._poll_interval) oauth_creds = { 'access_token': self._config['oauth_token'], 'access_token_secret': self._config['oauth_secret'], 'consumer_key': self._config['consumer_key'], - 'key_cert': self._rsa_key + 'key_cert': self._rsa_key, } - self._jira_client = JIRA(options={'server': self._jira_url}, - oauth=oauth_creds) + self._jira_client = JIRA(options=options, oauth=oauth_creds) elif auth_method == 'basic': basic_creds = (self._config['username'], self._config['password']) - self._jira_client = JIRA(options={'server': self._jira_url}, - basic_auth=basic_creds) + self._jira_client = JIRA(options=options, basic_auth=basic_creds) else: - msg = ('You must set auth_method to either "oauth"', - 'or "basic" your jira.yaml config file.') + msg = ( + 'You must set auth_method to either "oauth"', + 'or "basic" your jira.yaml config file.', + ) raise Exception(msg) if self._projects_available is None: @@ -74,7 +86,8 @@ def setup(self): if not self._project or self._project not in self._projects_available: raise Exception('Invalid project (%s) to track.' % self._project) self._jql_query = 'project=%s' % self._project - all_issues = self._jira_client.search_issues(self._jql_query, maxResults=None) + all_issues = self._jira_client.search_issues( + self._jql_query, maxResults=None) self._issues_in_project = {issue.key: issue for issue in all_issues} def poll(self): @@ -93,7 +106,9 @@ def remove_trigger(self, trigger): pass def _detect_new_issues(self): - new_issues = self._jira_client.search_issues(self._jql_query, maxResults=50, startAt=0) + new_issues = self._jira_client.search_issues( + self._jql_query, maxResults=50, startAt=0 + ) for issue in new_issues: if issue.key not in self._issues_in_project: diff --git a/tests/fixtures/full_auth_passwd.yaml b/tests/fixtures/full_auth_passwd.yaml index d387c7a..f875019 100644 --- a/tests/fixtures/full_auth_passwd.yaml +++ b/tests/fixtures/full_auth_passwd.yaml @@ -6,4 +6,5 @@ poll_interval: 30 project: "MY_PROJECT" verify: True - + client_cert_file: "" + client_key_file: ""