diff --git a/cEP-0015.md b/cEP-0015.md new file mode 100644 index 000000000..c15fe427b --- /dev/null +++ b/cEP-0015.md @@ -0,0 +1,257 @@ +# corobo Enhancement (security, tests and configurability) + +| Metadata | | +| -------- | -------------------------------------------------------- | +| cEP | 15 | +| Version | 1.0 | +| Title | corobo Enhancement (security, tests and configurability) | +| Authors | Nitanshu Vashistha | +| Status | Proposed | +| Type | Process | + +## Abstract + +This cEP describes the details of enhancement of +[corobo](https://github.com/coala/corobo) in terms of security, tests, +configurability and the new plugins that are to be added to corobo as part +of the +[GSoC Project](https://summerofcode.withgoogle.com/projects/#6603667076022272). + +## Security + +Security has been one of the major concerns due to some past experiences. +These are the changes that will be implemented to the existing plugins +in order to harden the security. + +1. Give developers the right to invite newcomers to the organization. + This will involve changing the bot command to `corobo invite <@username>`. + +2. Remove the automatic invite due to `Hello World` message. + +3. Remove `invite me` bot command. + +4. Stop newcomers from getting assigned to more than one newcomer issue. + +5. Make all LabHub plugins require being a member of the organization. This + will be done by creating a `members_only` decorator to check if the user is + a member of team or not. The decorator can be used on any bot command which + needs to be under the accessibility of an organization member only. + + ```py + def members_only(func): + @wraps(func) + def wrapper(*args, **kwargs): + # plugin instance + instance = args[0] + msg = args[1] + user = msg.frm.nick + for team in instance.team_mapping().values(): + if team.is_member(user): + yield from func(*args, **kwargs) + return + yield ('You need to be a member of this organization' + ' to use this command.') + return wrapper + ``` + +6. Add callback message to warn users who are spamming the room. + +7. Add bot command to ban a user from all rooms at once. The goal of + this feature is to prevent spamming as someone with many accounts + can't be stopped to join a room. + +## Tests + +The default TestBase provided by Errbot is not enough for testing plugins like +LabHub, which required intensive mocking. + +This will involve making changes upstream in +[Errbot](https://github.com/errbotio/errbot/) and extend the existing testing +infrastructure to implement better testing for plugins like LabHub. + +Errbot provides a pytest fixture `testbot` for testing purposes and to interact +with the message queue. Extending the testbot and adding `inject_mock` method +so that mocked objects can be injected into the plugin by passing a dictionary +containing the `PluginName`, `field_name` and `Mocked Object`. The +`inject_mock` method will inject a property on the plugin object and discard +the assignments from the plugin itself. + +```py +def test_command(testbot): + mocks = {'PluginName': {'field_name1': fakeObj1, + 'field_name2': fakeObj2}} + testbot.inject_mock(mocks) + assert 'something' in testbot.exec_command('!my_command') +``` + +## Configurability + +corobo has a potential to be used by other organizations for similar tasks like +onboarding and automation. Currently, it is not configurable and many plugins +are still very coala specific. Making it more configurable will allow other +organizations to adapt corobo to cater their needs. + +Making existing coala specific plugins generic for other organizations and +letting them configure the plugins as per their needs will ensure +configurability. + +Errobot provides [plugin configuration](http://errbot.io/en/latest/user_guide/plugin_development/configuration.html) +through the built-in `!config` command which can be used by other organizations +to configure the plugins as per their needs. + +List of Plugins which are coala specific and can be generalized: + +1. `LabHub` plugin commands are meant to work for a coala specific team names + maintainers, developers, newcomers and coala repositories. + + Issue Link: + +2. `explain` currently uses hardcoded coala explanations. Configuring + sub-directories `explain/genric` and `explain/` so that + other organizations can include their culture-specific explanations. + +3. `searchdocs` uses API_DOCS and USER_DOCS links which are constant for coala. + Other organizations will be able to initialize their own documentation + links as per their requirement. + + Issue Link: + +4. `community_stats` will be moved out from LabHub as a separate plugin since + it can also be used by other organizations for analytics overview. + + Issue Link: + +5. `wolfram_alpha` plugin currently uses an environment variable, it will be + modified to use the configuration template. + + Issue Link: + +6. `answer` plugin use `ANSWER_END` environment variable as an endpoint of the + answers microservice. It'll be configured so that other organizations can + also adapt to it. + + Issue Link: + +### Configuration Sample + +```py +class LabHub(BotPlugin): + def get_configuration_template(self): + return {'GH_TEAM_NAMES': ['newcomers', 'developers', 'maintainers']} + + @botcmd + def mycommand(self, mess, args): + # oh I need my TEAM ! + gh_teams = self.config['GH_TEAM_NAMES'] +``` + +``` + plugin config LabHub {'GH_TEAM_NAMES': ['newbies', 'devs', 'admins']} +``` + +Using the configuration templates means that the bot admin will have to +configure the plugin every time the bot starts and since corobo is continuously +deployed on each commit, reconfiguring again and again using the chat via a +admin is not preferable. + +This issue can be handled by using a Mixin to pre-configure the plugins through +`config.py` file. By using `DEFAULT_CONFIGS` in `config.py` file to hold the +configuration dictionaries for the plugins. Eg: + +```py +DEFAULT_CONFIG = { + 'LabHub': { + 'GH_TOKEN': os.getenv('GITHUB_TOKEN'), + 'GL_TOKEN': os.getenv('GITLAB_TOKEN'), + 'GH_ORG_NAME': os.getenv('GH_ORG_NAME'), + }, + 'WolframAlpha': { + 'WA_APP_ID': os.getenv('WA_TOKEN'), + }, +} +``` + +Configuration of the plugins will be done before their activation and if a +plugin is already configured, the `DefaultConfigMixin` will autoconfigure the +plugin from `DEFAULT_CONFIGS`. + +```py +class DefaultConfigMixin: + @property + def _default_config(self) -> Optional[Mapping]: + if (self.bot_config.DEFAULT_CONFIG and self.name + in self.bot_config.DEFAULT_CONFIG): + return self.bot_config.DEFAULT_CONFIG[self.name] + + def __init__(self, bot, name=None) -> None: + super().__init__(bot, name = name) + default_config = self._default_config + if default_config and not self.config: + super().configure(default_config) + + def get_configuration_template(self) -> Mapping: + default_config = self._default_config + if default_config: + return default_config +``` + +```py +class MyPlugin(BotPlugin, DefaultConfigMixin): + ... +``` + +Other possible ways to configure corobo were discussed [here](https://github.com/coala/corobo/issues/574). + +### Use of `activate` instead of `__init__` to configure plugins: + +Currently, most of the plugins make use of the `__init__` method for instead of +`activate` for configuration. Since `__init__` is executed at load time its +failure will cause the plugin to not show up. Configuring the plugins through +the `activate` method will remove this possibility. + +Eg: labhub.py + +```py + def __init__(self, bot, name=None): + super().__init__(bot, name) + + teams = dict() + try: + gh = github3.login(token=os.environ.get('GH_TOKEN')) + assert gh is not None + except AssertionError: + self.log.error('Cannot create github object, please check GH_TOKEN') + else: + self.GH3_ORG = gh.organization(self.GH_ORG_NAME) + for team in self.GH3_ORG.teams(): + teams[team.name] = team + + self._teams = teams + + self.IGH = GitHub(GitHubToken(os.environ.get('GH_TOKEN'))) + self.IGL = GitLab(GitLabPrivateToken(os.environ.get('GL_TOKEN'))) + + self.REPOS = dict() +``` + +```py + def activate(self): + + teams = dict() + try: + gh = github3.login(token=os.environ.get('GH_TOKEN')) + assert gh is not None + except AssertionError: + self.log.error('Cannot create github object, please check GH_TOKEN') + else: + self.GH3_ORG = gh.organization(self.GH_ORG_NAME) + for team in self.GH3_ORG.teams(): + teams[team.name] = team + + self._teams = teams + + self.IGH = GitHub(GitHubToken(os.environ.get('GH_TOKEN'))) + self.IGL = GitLab(GitLabPrivateToken(os.environ.get('GL_TOKEN'))) + + super(LabHub, self).activate() +```