diff --git a/conf/conf.yaml b/conf/conf.yaml index 69db05e..5b51b82 100644 --- a/conf/conf.yaml +++ b/conf/conf.yaml @@ -8,7 +8,7 @@ ALLOWED_HOSTS: ADDITIONAL_APPS: - stacktask.api.v1 - - stacktask.actions.tenant_setup + - stacktask.actions.v1 - stacktask.notifications.request_tracker DATABASES: diff --git a/stacktask/actions/models.py b/stacktask/actions/models.py index da53dea..2bc97e6 100644 --- a/stacktask/actions/models.py +++ b/stacktask/actions/models.py @@ -13,16 +13,11 @@ # under the License. from jsonfield import JSONField -from logging import getLogger -from uuid import uuid4 from django.conf import settings from django.db import models from django.utils import timezone -from stacktask.actions import serializers -from stacktask.actions import user_store - class Action(models.Model): """ @@ -45,1057 +40,3 @@ class Action(models.Model): data = self.action_data return settings.ACTION_CLASSES[self.action_name][0]( data=data, action_model=self) - - -class BaseAction(object): - """ - Base class for the object wrapping around the database model. - Setup to allow multiple action types and different internal logic - per type but built from a single database type. - - 'required' defines what fields to setup from the data. - - If need_token MAY be true, you must implement '_token_email', - which should return the email the action wants the token sent to. - While there are checks to prevent duplicates or different emails, - try and only have one action in your chain provide the email. - - The Action can do anything it needs at one of the three functions - called by the views: - - 'pre_approve' - - 'post_approve' - - 'submit' - - All logic and validation should be handled within the action itself, - and any other actions it is linked to. The way in which pre_approve, - post_approve, and submit are called should rarely change. Actions - should be built with those steps in mind, thinking about what they mean, - and when they execute. - - By using 'get_cache' and 'set_cache' they can pass data along which - may be needed by the action later. This cache is backed to the database. - - Passing data along to other actions is done via the task and - it's cache, but this is in memory only, so it is only useful during the - same action stage ('post_approve', etc.). - - Other than the task cache, actions should not be altering database - models other than themselves. This is not enforced, just a guideline. - """ - - required = [] - - def __init__(self, data, action_model=None, task=None, - order=None): - """ - Build itself around an existing database model, - or build itself and creates a new database model. - Sets up required data as fields. - """ - - self.logger = getLogger('stacktask') - - for field in self.required: - field_data = data[field] - setattr(self, field, field_data) - - if action_model: - self.action = action_model - else: - # make new model and save in db - action = Action.objects.create( - action_name=self.__class__.__name__, - action_data=data, - task=task, - order=order - ) - action.save() - self.action = action - - @property - def valid(self): - return self.action.valid - - @property - def need_token(self): - return self.action.need_token - - def get_email(self): - return self._get_email() - - def _get_email(self): - return None - - def get_cache(self, key): - return self.action.cache.get(key, None) - - def set_cache(self, key, value): - self.action.cache[key] = value - self.action.save() - - @property - def token_fields(self): - return self.action.cache.get("token_fields", []) - - def set_token_fields(self, token_fields): - self.action.cache["token_fields"] = token_fields - self.action.save() - - def add_note(self, note): - """ - Logs the note, and also adds it to the task action notes. - """ - self.logger.info("(%s) - %s" % (timezone.now(), note)) - note = "%s - (%s)" % (note, timezone.now()) - self.action.task.add_action_note( - str(self), note) - - def pre_approve(self): - return self._pre_approve() - - def post_approve(self): - return self._post_approve() - - def submit(self, token_data): - return self._submit(token_data) - - def _pre_approve(self): - raise NotImplementedError - - def _post_approve(self): - raise NotImplementedError - - def _submit(self, token_data): - raise NotImplementedError - - def __str__(self): - return self.__class__.__name__ - - -class ResourceMixin(object): - """Base Mixin class for dealing with Openstack resources.""" - - def _validate_keystone_user(self): - keystone_user = self.action.task.keystone_user - - if keystone_user['project_domain_id'] != self.domain_id: - self.add_note('Domain id does not match keystone user domain.') - return False - - if keystone_user['project_id'] != self.project_id: - self.add_note('Project id does not match keystone user project.') - return False - return True - - def _validate_domain_id(self): - id_manager = user_store.IdentityManager() - domain = id_manager.get_domain(self.domain_id) - if not domain: - self.add_note('Domain does not exist.') - return False - - return True - - def _validate_project_id(self): - # Handle an edge_case where some actions set their - # own project_id value. - if not self.project_id: - self.add_note('No project_id given.') - return False - - # Now actually check the project exists. - id_manager = user_store.IdentityManager() - project = id_manager.get_project(self.project_id) - if not project: - self.add_note('Project with id %s does not exist.' % - self.project_id) - return False - self.add_note('Project with id %s exists.' % self.project_id) - return True - - def _validate_domain_name(self): - id_manager = user_store.IdentityManager() - self.domain = id_manager.find_domain(self.domain_name) - if not self.domain: - self.add_note('Domain does not exist.') - return False - # also store the domain_id separately for later use - self.domain_id = self.domain.id - return True - - -class UserMixin(ResourceMixin): - """Mixin with functions for users.""" - - # Accessors - def _validate_username_exists(self): - id_manager = user_store.IdentityManager() - - self.user = id_manager.find_user(self.username, self.domain.id) - if not self.user: - self.add_note('No user present with username') - return False - return True - - def _validate_role_permissions(self): - keystone_user = self.action.task.keystone_user - # Role permissions check - if not self.are_roles_managable(user_roles=keystone_user['roles'], - requested_roles=self.roles): - self.add_note('User does not have permission to edit role(s).') - return False - return True - - def are_roles_managable(self, user_roles=[], requested_roles=[]): - requested_roles = set(requested_roles) - # blacklist checks - blacklist_roles = set(['admin']) - if len(blacklist_roles & requested_roles) > 0: - return False - - # user managable role - managable_roles = user_store.get_managable_roles(user_roles) - intersection = set(managable_roles) & requested_roles - # if all requested roles match, we can proceed - return intersection == requested_roles - - def find_user(self): - id_manager = user_store.IdentityManager() - return id_manager.find_user(self.username, self.domain_id) - - # Mutators - def grant_roles(self, user, roles, project_id): - return self._user_roles_edit(user, roles, project_id, remove=False) - - def remove_roles(self, user, roles, project_id): - return self._user_roles_edit(user, roles, project_id, remove=True) - - # Helper function to add or remove roles - def _user_roles_edit(self, user, roles, project_id, remove=False): - id_manager = user_store.IdentityManager() - if not remove: - action_fn = id_manager.add_user_role - action_string = "granting" - else: - action_fn = id_manager.remove_user_role - action_string = "removing" - ks_roles = [] - try: - for role in roles: - ks_role = id_manager.find_role(role) - if ks_role: - ks_roles.append(ks_role) - else: - raise TypeError("Keystone missing role: %s" % role) - - for role in ks_roles: - action_fn(user, role, project_id) - except Exception as e: - self.add_note( - "Error: '%s' while %s the roles: %s on user: %s " % - (e, action_string, roles, user)) - raise - - def enable_user(self, user=None): - id_manager = user_store.IdentityManager() - try: - if not user: - user = self.find_user() - id_manager.enable_user(user) - except Exception as e: - self.add_note( - "Error: '%s' while re-enabling user: %s" % - (e, self.username)) - raise - - def create_user(self, password): - id_manager = user_store.IdentityManager() - try: - user = id_manager.create_user( - name=self.username, password=password, - email=self.email, domain=self.domain_id, - created_on=str(timezone.now())) - except Exception as e: - # TODO: Narrow the Exceptions caught to a relevant set. - self.add_note( - "Error: '%s' while creating user: %s with roles: %s" % - (e, self.username, self.roles)) - raise - return user - - def update_password(self, password, user=None): - id_manager = user_store.IdentityManager() - try: - if not user: - user = self.find_user() - id_manager.update_user_password(user, password) - except Exception as e: - self.add_note( - "Error: '%s' while changing password for user: %s" % - (e, self.username)) - raise - - -class ProjectMixin(ResourceMixin): - """Mixin with functions for projects.""" - - def _validate_parent_project(self): - id_manager = user_store.IdentityManager() - # NOTE(adriant): If parent id is None, Keystone defaults to the domain. - # So we only care to validate if parent_id is not None. - if self.parent_id: - parent = id_manager.get_project(self.parent_id) - if not parent: - self.add_note("Parent id: '%s' does not exist." % - self.project_name) - return False - return True - - def _validate_project_absent(self): - id_manager = user_store.IdentityManager() - project = id_manager.find_project( - self.project_name, self.domain_id) - if project: - self.add_note("Existing project with name '%s'." % - self.project_name) - return False - - self.add_note("No existing project with name '%s'." % - self.project_name) - return True - - def _create_project(self): - id_manager = user_store.IdentityManager() - try: - project = id_manager.create_project( - self.project_name, created_on=str(timezone.now()), - parent=self.parent_id, domain=self.domain_id) - except Exception as e: - self.add_note( - "Error: '%s' while creating project: %s" % - (e, self.project_name)) - raise - # put project_id into action cache: - self.action.task.cache['project_id'] = project.id - self.set_cache('project_id', project.id) - self.add_note("New project '%s' created." % project.name) - - -class UserIdAction(BaseAction): - - def _get_target_user(self): - """ - Gets the target user by id - """ - id_manager = user_store.IdentityManager() - user = id_manager.get_user(self.user_id) - - return user - - -class UserNameAction(BaseAction): - """ - Base action for dealing with users. Removes username if - USERNAME_IS_EMAIL and sets email to be username. - """ - - def __init__(self, *args, **kwargs): - if settings.USERNAME_IS_EMAIL: - try: - self.required.remove('username') - except ValueError: - pass - # nothing to remove - super(UserNameAction, self).__init__(*args, **kwargs) - self.username = self.email - else: - super(UserNameAction, self).__init__(*args, **kwargs) - - def _get_email(self): - return self.email - - def _get_target_user(self): - """ - Gets the target user by their username - """ - id_manager = user_store.IdentityManager() - user = id_manager.find_user(self.username, self.domain_id) - - return user - - -class NewUserAction(UserNameAction, ProjectMixin, UserMixin): - """ - Setup a new user with a role on the given project. - Creates the user if they don't exist, otherwise - if the username and email for the request match the - existing one, will simply add the project role. - """ - - required = [ - 'username', - 'email', - 'project_id', - 'roles', - 'domain_id', - ] - - def _validate_target_user(self): - id_manager = user_store.IdentityManager() - - # check if user exists and is valid - # this may mean we need a token. - user = self._get_target_user() - if not user: - self.action.need_token = True - # add to cache to use in template - self.action.task.cache['user_state'] = "default" - self.set_token_fields(["password"]) - self.add_note( - 'No user present with username. Need to create new user.') - return True - if user.email != self.email: - self.add_note( - 'Found matching username, but email did not match.' + - 'Reporting as invalid.') - return False - - if not user.enabled: - self.action.need_token = True - self.action.state = "disabled" - # add to cache to use in template - self.action.task.cache['user_state'] = "disabled" - # as they are disabled we'll reset their password - self.set_token_fields(["password"]) - self.add_note( - 'Existing disabled user with matching email.') - return True - - # role_validation - roles = id_manager.get_roles(user, self.project_id) - role_names = {role.name for role in roles} - missing = set(self.roles) - role_names - if not missing: - self.action.need_token = False - self.action.state = "complete" - self.add_note( - 'Existing user already has roles.' - ) - else: - self.roles = list(missing) - self.action.need_token = True - self.set_token_fields(["confirm"]) - self.action.state = "existing" - # add to cache to use in template - self.action.task.cache['user_state'] = "existing" - self.add_note( - 'Existing user with matching email missing roles.') - - return True - - def _validate(self): - self.action.valid = ( - self._validate_role_permissions() and - self._validate_keystone_user() and - self._validate_domain_id() and - self._validate_project_id() and - self._validate_target_user() - ) - self.action.save() - - def _pre_approve(self): - self._validate() - - def _post_approve(self): - self._validate() - - def _submit(self, token_data): - self._validate() - - if not self.valid: - return - - # add to cache to use in template - self.action.task.cache['user_state'] = self.action.state - - if self.action.state == "default": - # default action: Create a new user in the tenant and add roles - user = self.create_user(token_data['password']) - self.grant_roles(user, self.roles, self.project_id) - - self.add_note( - 'User %s has been created, with roles %s in project %s.' - % (self.username, self.roles, self.project_id)) - - elif self.action.state == "disabled": - # first re-enable user - user = self.find_user() - self.enable_user(user) - self.grant_roles(user, self.roles, self.project_id) - self.update_password(token_data['password']) - - self.add_note('User %s password has been changed.' % self.username) - - self.add_note( - 'Existing user %s has been re-enabled and given roles %s' - ' in project %s.' - % (self.username, self.roles, self.project_id)) - - elif self.action.state == "existing": - # Existing action: only add roles. - user = self.find_user() - self.grant_roles(user, self.roles, self.project_id) - - self.add_note( - 'Existing user %s has been given roles %s in project %s.' - % (self.username, self.roles, self.project_id)) - elif self.action.state == "complete": - # complete action: nothing to do. - self.add_note( - 'Existing user %s already had roles %s in project %s.' - % (self.username, self.roles, self.project_id)) - - -# TODO(adriant): Write tests for this action. -class NewProjectAction(BaseAction, ProjectMixin, UserMixin): - """ - Creates a new project for the current keystone_user. - - This action can only be used for an autheticated taskview. - """ - - required = [ - 'domain_id', - 'parent_id', - 'project_name', - ] - - def __init__(self, *args, **kwargs): - super(NewProjectAction, self).__init__(*args, **kwargs) - - def _validate(self): - self.action.valid = ( - self._validate_domain_id() and - self._validate_parent_project() and - self._validate_project_absent()) - self.action.save() - - def _validate_domain_id(self): - keystone_user = self.action.task.keystone_user - - if keystone_user['project_domain_id'] != self.domain_id: - self.add_note('Domain id does not match keystone user domain.') - return False - - return super(NewProjectAction, self)._validate_domain_id() - - def _validate_parent_project(self): - if self.parent_id: - keystone_user = self.action.task.keystone_user - - if self.parent_id != keystone_user['project_id']: - self.add_note( - 'Parent id does not match keystone user project.') - return False - return super(NewProjectAction, self)._validate_parent_project() - return True - - def _pre_approve(self): - self._validate() - - def _post_approve(self): - project_id = self.get_cache('project_id') - if project_id: - self.action.task.cache['project_id'] = project_id - self.add_note("Project already created.") - else: - self._validate() - - if not self.valid: - return - - self._create_project() - - user_id = self.get_cache('user_id') - if user_id: - self.action.task.cache['user_id'] = user_id - self.add_note("User already given roles.") - else: - default_roles = settings.ACTION_SETTINGS.get( - 'NewProjectAction', {}).get("default_roles", {}) - - project_id = self.get_cache('project_id') - keystone_user = self.action.task.keystone_user - - try: - id_manager = user_store.IdentityManager() - user = id_manager.get_user(keystone_user['user_id']) - - self.grant_roles(user, default_roles, project_id) - except Exception as e: - self.add_note( - ("Error: '%s' while adding roles %s " - "to user '%s' on project '%s'") % - (e, self.username, default_roles, project_id)) - raise - - # put user_id into action cache: - self.action.task.cache['user_id'] = user.id - self.set_cache('user_id', user.id) - self.add_note(("Existing user '%s' attached to project %s" + - " with roles: %s") - % (self.username, project_id, - default_roles)) - - def _submit(self, token_data): - """ - Nothing to do here. Everything is done at post_approve. - """ - pass - - -class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin): - """ - Makes a new project for the given username. Will create the user if it - doesn't exists. - """ - - required = [ - 'domain_id', - 'parent_id', - 'project_name', - 'username', - 'email' - ] - - def __init__(self, *args, **kwargs): - super(NewProjectWithUserAction, self).__init__(*args, **kwargs) - - def _validate(self): - self.action.valid = ( - self._validate_domain_id() and - self._validate_parent_project() and - self._validate_project_absent() and - self._validate_user()) - self.action.save() - - def _validate_user(self): - id_manager = user_store.IdentityManager() - user = id_manager.find_user(self.username, self.domain_id) - - if not user: - # add to cache to use in template - self.action.task.cache['user_state'] = "default" - self.action.need_token = True - self.set_token_fields(["password"]) - self.add_note("No user present with username '%s'." % - self.username) - return True - - if user.email != self.email: - self.add_note("Existing user '%s' with non-matching email." % - self.username) - return False - - if not user.enabled: - self.action.state = "disabled" - # add to cache to use in template - self.action.task.cache['user_state'] = "disabled" - self.action.need_token = True - self.add_note( - "Existing disabled user '%s' with matching email." % - self.email) - return True - else: - self.action.state = "existing" - # add to cache to use in template - self.action.task.cache['user_state'] = "existing" - self.action.need_token = False - self.add_note("Existing user '%s' with matching email." % - self.email) - return True - - def _validate_user_submit(self): - user_id = self.get_cache('user_id') - project_id = self.get_cache('project_id') - - id_manager = user_store.IdentityManager() - - user = id_manager.get_user(user_id) - project = id_manager.get_project(project_id) - - if user and project: - self.action.valid = True - else: - self.action.valid = False - - self.action.save() - - def _pre_approve(self): - self._validate() - - def _post_approve(self): - """ - Approving a new project means we set up the project itself, - and if the user doesn't exist, create it right away. An existing - user automatically gets added to the new project. - """ - project_id = self.get_cache('project_id') - if project_id: - self.action.task.cache['project_id'] = project_id - self.add_note("Project already created.") - else: - self.action.valid = ( - self._validate_domain_id() and - self._validate_parent_project() and - self._validate_project_absent()) - self.action.save() - - if not self.valid: - return - - self._create_project() - - # User validation and checks - user_id = self.get_cache('user_id') - roles_granted = self.get_cache('roles_granted') - if user_id and roles_granted: - self.action.task.cache['user_id'] = user_id - self.add_note("User already setup.") - elif not user_id: - self.action.valid = self._validate_user() - self.action.save() - - if not self.valid: - return - - self._create_user_for_project() - elif not roles_granted: - self._create_user_for_project() - - def _create_user_for_project(self): - id_manager = user_store.IdentityManager() - default_roles = settings.ACTION_SETTINGS.get( - 'NewProjectAction', {}).get("default_roles", {}) - - project_id = self.get_cache('project_id') - - if self.action.state == "default": - try: - # Generate a temporary password: - password = uuid4().hex + uuid4().hex - - user_id = self.get_cache('user_id') - if not user_id: - user = id_manager.create_user( - name=self.username, password=password, - email=self.email, domain=self.domain_id, - created_on=str(timezone.now())) - self.set_cache('user_id', user.id) - else: - user = id_manager.get_user(user_id) - # put user_id into action cache: - self.action.task.cache['user_id'] = user.id - - self.grant_roles(user, default_roles, project_id) - except Exception as e: - self.add_note( - "Error: '%s' while creating user: %s with roles: %s" % - (e, self.username, default_roles)) - raise - - self.set_cache('roles_granted', True) - self.add_note( - "New user '%s' created for project %s with roles: %s" % - (self.username, project_id, default_roles)) - elif self.action.state == "existing": - try: - user_id = self.get_cache('user_id') - if not user_id: - user = id_manager.find_user( - self.username, self.domain_id) - self.set_cache('user_id', user.id) - else: - user = id_manager.get_user(user_id) - self.action.task.cache['user_id'] = user.id - - self.grant_roles(user, default_roles, project_id) - except Exception as e: - self.add_note( - "Error: '%s' while granting roles: %s to user: %s" % - (e, default_roles, self.username)) - raise - - self.set_cache('roles_granted', True) - self.add_note(("Existing user '%s' setup on project %s" + - " with roles: %s") - % (self.username, project_id, - default_roles)) - elif self.action.state == "disabled": - user_id = self.get_cache('user_id') - if not user_id: - # first re-enable user - try: - user = id_manager.find_user(self.username, self.domain_id) - id_manager.enable_user(user) - except Exception as e: - self.add_note( - "Error: '%s' while re-enabling user: %s" % - (e, self.username)) - raise - - # and now update their password - # Generate a temporary password: - password = uuid4().hex + uuid4().hex - try: - id_manager.update_user_password(user, password) - except Exception as e: - self.add_note( - "Error: '%s' while changing password for user: %s" % - (e, self.username)) - raise - self.add_note( - 'User %s password has been changed.' % self.username) - - self.set_cache('user_id', user.id) - else: - user = id_manager.get_user(user_id) - self.action.task.cache['user_id'] = user.id - - # now add their roles - roles_granted = self.get_cache('roles_granted') - if not roles_granted: - try: - self.grant_roles(user, default_roles, project_id) - except Exception as e: - self.add_note( - "Error: '%s' while granting user: %s roles: %s" % - (e, self.username, default_roles)) - raise - self.set_cache('roles_granted', True) - - self.add_note(("Existing user '%s' setup on project %s" + - " with roles: %s") - % (self.username, project_id, - default_roles)) - - def _submit(self, token_data): - """ - The submit action is performed when a token is submitted. - This is done to set a user password only, and so should now only - change the user password. The project and user themselves are created - on post_approve. - """ - - self._validate_user_submit() - - if not self.valid: - return - - # add to cache to use in template - self.action.task.cache['user_state'] = self.action.state - - project_id = self.get_cache('project_id') - self.action.task.cache['project_id'] = project_id - user_id = self.get_cache('user_id') - self.action.task.cache['user_id'] = user_id - id_manager = user_store.IdentityManager() - - if self.action.state in ["default", "disabled"]: - user = id_manager.get_user(user_id) - try: - id_manager.update_user_password( - user, token_data['password']) - except Exception as e: - self.add_note( - "Error: '%s' while changing password for user: %s" % - (e, self.username)) - raise - self.add_note('User %s password has been changed.' % self.username) - - elif self.action.state == "existing": - # do nothing, everything is already done. - self.add_note( - "Existing user '%s' already attached to project %s" % ( - user_id, project_id)) - - -class ResetUserPasswordAction(UserNameAction, UserMixin): - """ - Simple action to reset a password for a given user. - """ - - username = models.CharField(max_length=200) - email = models.EmailField() - - required = [ - 'domain_name', - 'username', - 'email' - ] - - blacklist = settings.ACTION_SETTINGS.get( - 'ResetUserPasswordAction', {}).get("blacklisted_roles", {}) - - def _validate_user_roles(self): - id_manager = user_store.IdentityManager() - - self.user = id_manager.find_user(self.username, self.domain.id) - roles = id_manager.get_all_roles(self.user) - - user_roles = [] - for project, roles in roles.iteritems(): - user_roles.extend(role.name for role in roles) - - if set(self.blacklist) & set(user_roles): - self.add_note('Cannot reset users with blacklisted roles.') - return False - - if self.user.email == self.email: - self.action.need_token = True - self.set_token_fields(["password"]) - self.add_note('Existing user with matching email.') - return True - else: - self.add_note('Existing user with non-matching email.') - return False - - def _validate(self): - # Here, the order of validation matters - # as each one adds new class variables - self.action.valid = ( - self._validate_domain_name() and - self._validate_username_exists() and - self._validate_user_roles() - ) - self.action.save() - - def _pre_approve(self): - self._validate() - - def _post_approve(self): - self._validate() - - def _submit(self, token_data): - self._validate() - - if not self.valid: - return - - self.update_password(token_data['password']) - self.add_note('User %s password has been changed.' % self.username) - - -class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin): - """ - A class for adding or removing roles - on a user for the given project. - """ - - required = [ - 'domain_id', - 'project_id', - 'user_id', - 'roles', - 'remove' - ] - - def _validate_target_user(self): - # Get target user - user = self._get_target_user() - if not user: - self.add_note('No user present with user_id') - return False - return True - - def _validate_user_roles(self): - id_manager = user_store.IdentityManager() - user = self._get_target_user() - project = id_manager.get_project(self.project_id) - # user roles - current_roles = id_manager.get_roles(user, project) - current_role_names = {role.name for role in current_roles} - if self.remove: - remaining = set(current_role_names) & set(self.roles) - if not remaining: - self.action.state = "complete" - self.add_note( - "User doesn't have roles to remove.") - else: - self.roles = list(remaining) - self.add_note( - 'User has roles to remove.') - else: - missing = set(self.roles) - set(current_role_names) - if not missing: - self.action.state = "complete" - self.add_note( - 'User already has roles.') - else: - self.roles = list(missing) - self.add_note( - 'User user missing roles.') - # All paths are valid here - # We've just set state and roles that need to be changed. - return True - - def _validate(self): - self.action.valid = ( - self._validate_keystone_user() and - self._validate_role_permissions() and - self._validate_domain_id() and - self._validate_project_id() and - self._validate_target_user() and - self._validate_user_roles() - ) - self.action.save() - - def _pre_approve(self): - self._validate() - - def _post_approve(self): - self._validate() - - def _submit(self, token_data): - self._validate() - - if not self.valid: - return - - if self.action.state == "default": - user = self._get_target_user() - self._user_roles_edit(user, self.roles, self.project_id, - remove=self.remove) - - if self.remove: - self.add_note( - 'User %s has had roles %s removed from project %s.' - % (self.user_id, self.roles, self.project_id)) - else: - self.add_note( - 'User %s has been given roles %s in project %s.' - % (self.user_id, self.roles, self.project_id)) - elif self.action.state == "complete": - if self.remove: - self.add_note( - 'User %s already had roles %s in project %s.' - % (self.user_id, self.roles, self.project_id)) - else: - self.add_note( - "User %s didn't have roles %s in project %s." - % (self.user_id, self.roles, self.project_id)) - - -# Update settings dict with tuples in the format: -# (, ) -def register_action_class(action_class, serializer_class): - data = {} - data[action_class.__name__] = (action_class, serializer_class) - settings.ACTION_CLASSES.update(data) - - -# Register each action model -register_action_class(NewUserAction, serializers.NewUserSerializer) -register_action_class( - NewProjectWithUserAction, serializers.NewProjectWithUserSerializer) -register_action_class(ResetUserPasswordAction, serializers.ResetUserSerializer) -register_action_class(EditUserRolesAction, serializers.EditUserRolesSerializer) diff --git a/stacktask/actions/tenant_setup/migrations/__init__.py b/stacktask/actions/tenant_setup/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/stacktask/actions/tenant_setup/serializers.py b/stacktask/actions/tenant_setup/serializers.py deleted file mode 100644 index b8a5b4e..0000000 --- a/stacktask/actions/tenant_setup/serializers.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) 2015 Catalyst IT Ltd -# -# 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. - -from rest_framework import serializers - - -class NewDefaultNetworkSerializer(serializers.Serializer): - setup_network = serializers.BooleanField(default=True) - project_id = serializers.CharField(max_length=64) - region = serializers.CharField(max_length=100) - - -class NewProjectDefaultNetworkSerializer(serializers.Serializer): - setup_network = serializers.BooleanField(default=False) - region = serializers.CharField(max_length=100) - - -class AddDefaultUsersToProjectSerializer(serializers.Serializer): - domain_id = serializers.CharField(max_length=64, default='default') - - -class SetProjectQuotaSerializer(serializers.Serializer): - pass diff --git a/stacktask/actions/v1/__init__.py b/stacktask/actions/v1/__init__.py new file mode 100644 index 0000000..2a1c42c --- /dev/null +++ b/stacktask/actions/v1/__init__.py @@ -0,0 +1 @@ +default_app_config = 'stacktask.actions.v1.app.ActionV1Config' diff --git a/stacktask/actions/v1/app.py b/stacktask/actions/v1/app.py new file mode 100644 index 0000000..1ef83df --- /dev/null +++ b/stacktask/actions/v1/app.py @@ -0,0 +1,7 @@ + +from django.apps import AppConfig + + +class ActionV1Config(AppConfig): + name = "stacktask.actions.v1" + label = 'actions_v1' diff --git a/stacktask/actions/v1/base.py b/stacktask/actions/v1/base.py new file mode 100644 index 0000000..77fbbe3 --- /dev/null +++ b/stacktask/actions/v1/base.py @@ -0,0 +1,397 @@ +# Copyright (C) 2015 Catalyst IT Ltd +# +# 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. + +from logging import getLogger + +from django.conf import settings +from django.utils import timezone + +from stacktask.actions import user_store +from stacktask.actions.models import Action + + +class BaseAction(object): + """ + Base class for the object wrapping around the database model. + Setup to allow multiple action types and different internal logic + per type but built from a single database type. + - 'required' defines what fields to setup from the data. + + If need_token MAY be true, you must implement '_token_email', + which should return the email the action wants the token sent to. + While there are checks to prevent duplicates or different emails, + try and only have one action in your chain provide the email. + + The Action can do anything it needs at one of the three functions + called by the views: + - 'pre_approve' + - 'post_approve' + - 'submit' + + All logic and validation should be handled within the action itself, + and any other actions it is linked to. The way in which pre_approve, + post_approve, and submit are called should rarely change. Actions + should be built with those steps in mind, thinking about what they mean, + and when they execute. + + By using 'get_cache' and 'set_cache' they can pass data along which + may be needed by the action later. This cache is backed to the database. + + Passing data along to other actions is done via the task and + it's cache, but this is in memory only, so it is only useful during the + same action stage ('post_approve', etc.). + + Other than the task cache, actions should not be altering database + models other than themselves. This is not enforced, just a guideline. + """ + + required = [] + + def __init__(self, data, action_model=None, task=None, + order=None): + """ + Build itself around an existing database model, + or build itself and creates a new database model. + Sets up required data as fields. + """ + + self.logger = getLogger('stacktask') + + for field in self.required: + field_data = data[field] + setattr(self, field, field_data) + + if action_model: + self.action = action_model + else: + # make new model and save in db + action = Action.objects.create( + action_name=self.__class__.__name__, + action_data=data, + task=task, + order=order + ) + action.save() + self.action = action + + @property + def valid(self): + return self.action.valid + + @property + def need_token(self): + return self.action.need_token + + def get_email(self): + return self._get_email() + + def _get_email(self): + return None + + def get_cache(self, key): + return self.action.cache.get(key, None) + + def set_cache(self, key, value): + self.action.cache[key] = value + self.action.save() + + @property + def token_fields(self): + return self.action.cache.get("token_fields", []) + + def set_token_fields(self, token_fields): + self.action.cache["token_fields"] = token_fields + self.action.save() + + def add_note(self, note): + """ + Logs the note, and also adds it to the task action notes. + """ + self.logger.info("(%s) - %s" % (timezone.now(), note)) + note = "%s - (%s)" % (note, timezone.now()) + self.action.task.add_action_note( + str(self), note) + + def pre_approve(self): + return self._pre_approve() + + def post_approve(self): + return self._post_approve() + + def submit(self, token_data): + return self._submit(token_data) + + def _pre_approve(self): + raise NotImplementedError + + def _post_approve(self): + raise NotImplementedError + + def _submit(self, token_data): + raise NotImplementedError + + def __str__(self): + return self.__class__.__name__ + + +class ResourceMixin(object): + """Base Mixin class for dealing with Openstack resources.""" + + def _validate_keystone_user(self): + keystone_user = self.action.task.keystone_user + + if keystone_user['project_domain_id'] != self.domain_id: + self.add_note('Domain id does not match keystone user domain.') + return False + + if keystone_user['project_id'] != self.project_id: + self.add_note('Project id does not match keystone user project.') + return False + return True + + def _validate_domain_id(self): + id_manager = user_store.IdentityManager() + domain = id_manager.get_domain(self.domain_id) + if not domain: + self.add_note('Domain does not exist.') + return False + + return True + + def _validate_project_id(self): + # Handle an edge_case where some actions set their + # own project_id value. + if not self.project_id: + self.add_note('No project_id given.') + return False + + # Now actually check the project exists. + id_manager = user_store.IdentityManager() + project = id_manager.get_project(self.project_id) + if not project: + self.add_note('Project with id %s does not exist.' % + self.project_id) + return False + self.add_note('Project with id %s exists.' % self.project_id) + return True + + def _validate_domain_name(self): + id_manager = user_store.IdentityManager() + self.domain = id_manager.find_domain(self.domain_name) + if not self.domain: + self.add_note('Domain does not exist.') + return False + # also store the domain_id separately for later use + self.domain_id = self.domain.id + return True + + +class UserMixin(ResourceMixin): + """Mixin with functions for users.""" + + # Accessors + def _validate_username_exists(self): + id_manager = user_store.IdentityManager() + + self.user = id_manager.find_user(self.username, self.domain.id) + if not self.user: + self.add_note('No user present with username') + return False + return True + + def _validate_role_permissions(self): + keystone_user = self.action.task.keystone_user + # Role permissions check + if not self.are_roles_managable(user_roles=keystone_user['roles'], + requested_roles=self.roles): + self.add_note('User does not have permission to edit role(s).') + return False + return True + + def are_roles_managable(self, user_roles=[], requested_roles=[]): + requested_roles = set(requested_roles) + # blacklist checks + blacklist_roles = set(['admin']) + if len(blacklist_roles & requested_roles) > 0: + return False + + # user managable role + managable_roles = user_store.get_managable_roles(user_roles) + intersection = set(managable_roles) & requested_roles + # if all requested roles match, we can proceed + return intersection == requested_roles + + def find_user(self): + id_manager = user_store.IdentityManager() + return id_manager.find_user(self.username, self.domain_id) + + # Mutators + def grant_roles(self, user, roles, project_id): + return self._user_roles_edit(user, roles, project_id, remove=False) + + def remove_roles(self, user, roles, project_id): + return self._user_roles_edit(user, roles, project_id, remove=True) + + # Helper function to add or remove roles + def _user_roles_edit(self, user, roles, project_id, remove=False): + id_manager = user_store.IdentityManager() + if not remove: + action_fn = id_manager.add_user_role + action_string = "granting" + else: + action_fn = id_manager.remove_user_role + action_string = "removing" + ks_roles = [] + try: + for role in roles: + ks_role = id_manager.find_role(role) + if ks_role: + ks_roles.append(ks_role) + else: + raise TypeError("Keystone missing role: %s" % role) + + for role in ks_roles: + action_fn(user, role, project_id) + except Exception as e: + self.add_note( + "Error: '%s' while %s the roles: %s on user: %s " % + (e, action_string, self.roles, user)) + raise + + def enable_user(self, user=None): + id_manager = user_store.IdentityManager() + try: + if not user: + user = self.find_user() + id_manager.enable_user(user) + except Exception as e: + self.add_note( + "Error: '%s' while re-enabling user: %s with roles: %s" % + (e, self.username, self.roles)) + raise + + def create_user(self, password): + id_manager = user_store.IdentityManager() + try: + user = id_manager.create_user( + name=self.username, password=password, + email=self.email, domain=self.domain_id, + created_on=str(timezone.now())) + except Exception as e: + # TODO: Narrow the Exceptions caught to a relevant set. + self.add_note( + "Error: '%s' while creating user: %s with roles: %s" % + (e, self.username, self.roles)) + raise + return user + + def update_password(self, password, user=None): + id_manager = user_store.IdentityManager() + try: + if not user: + user = self.find_user() + id_manager.update_user_password(user, password) + except Exception as e: + self.add_note( + "Error: '%s' while changing password for user: %s" % + (e, self.username)) + raise + + +class ProjectMixin(ResourceMixin): + """Mixin with functions for projects.""" + + def _validate_parent_project(self): + id_manager = user_store.IdentityManager() + # NOTE(adriant): If parent id is None, Keystone defaults to the domain. + # So we only care to validate if parent_id is not None. + if self.parent_id: + parent = id_manager.get_project(self.parent_id) + if not parent: + self.add_note("Parent id: '%s' does not exist." % + self.project_name) + return False + return True + + def _validate_project_absent(self): + id_manager = user_store.IdentityManager() + project = id_manager.find_project( + self.project_name, self.domain_id) + if project: + self.add_note("Existing project with name '%s'." % + self.project_name) + return False + + self.add_note("No existing project with name '%s'." % + self.project_name) + return True + + def _create_project(self): + id_manager = user_store.IdentityManager() + try: + project = id_manager.create_project( + self.project_name, created_on=str(timezone.now()), + parent=self.parent_id, domain=self.domain_id) + except Exception as e: + self.add_note( + "Error: '%s' while creating project: %s" % + (e, self.project_name)) + raise + # put project_id into action cache: + self.action.task.cache['project_id'] = project.id + self.set_cache('project_id', project.id) + self.add_note("New project '%s' created." % project.name) + + +class UserIdAction(BaseAction): + + def _get_target_user(self): + """ + Gets the target user by id + """ + id_manager = user_store.IdentityManager() + user = id_manager.get_user(self.user_id) + + return user + + +class UserNameAction(BaseAction): + """ + Base action for dealing with users. Removes username if + USERNAME_IS_EMAIL and sets email to be username. + """ + + def __init__(self, *args, **kwargs): + if settings.USERNAME_IS_EMAIL: + try: + self.required.remove('username') + except ValueError: + pass + # nothing to remove + super(UserNameAction, self).__init__(*args, **kwargs) + self.username = self.email + else: + super(UserNameAction, self).__init__(*args, **kwargs) + + def _get_email(self): + return self.email + + def _get_target_user(self): + """ + Gets the target user by their username + """ + id_manager = user_store.IdentityManager() + user = id_manager.find_user(self.username, self.domain_id) + + return user diff --git a/stacktask/actions/v1/models.py b/stacktask/actions/v1/models.py new file mode 100644 index 0000000..0d27ac6 --- /dev/null +++ b/stacktask/actions/v1/models.py @@ -0,0 +1,54 @@ +# Copyright (C) 2015 Catalyst IT Ltd +# +# 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. + +from django.conf import settings + +from stacktask.actions.v1 import serializers +from stacktask.actions.v1.projects import ( + NewProjectWithUserAction, AddDefaultUsersToProjectAction) +from stacktask.actions.v1.users import ( + EditUserRolesAction, NewUserAction, ResetUserPasswordAction) +from stacktask.actions.v1.resources import ( + NewDefaultNetworkAction, NewProjectDefaultNetworkAction, + SetProjectQuotaAction) + + +# Update settings dict with tuples in the format: +# (, ) +def register_action_class(action_class, serializer_class): + data = {} + data[action_class.__name__] = (action_class, serializer_class) + settings.ACTION_CLASSES.update(data) + + +# Register Project actions: +register_action_class( + NewProjectWithUserAction, serializers.NewProjectWithUserSerializer) +register_action_class( + AddDefaultUsersToProjectAction, + serializers.AddDefaultUsersToProjectSerializer) + +# Register User actions: +register_action_class(NewUserAction, serializers.NewUserSerializer) +register_action_class(ResetUserPasswordAction, serializers.ResetUserSerializer) +register_action_class(EditUserRolesAction, serializers.EditUserRolesSerializer) + +# Register Resource actions: +register_action_class( + NewDefaultNetworkAction, serializers.NewDefaultNetworkSerializer) +register_action_class( + NewProjectDefaultNetworkAction, + serializers.NewProjectDefaultNetworkSerializer) +register_action_class( + SetProjectQuotaAction, serializers.SetProjectQuotaSerializer) diff --git a/stacktask/actions/v1/projects.py b/stacktask/actions/v1/projects.py new file mode 100644 index 0000000..5d9110b --- /dev/null +++ b/stacktask/actions/v1/projects.py @@ -0,0 +1,456 @@ +# Copyright (C) 2015 Catalyst IT Ltd +# +# 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. + +from uuid import uuid4 + +from django.conf import settings +from django.utils import timezone + +from stacktask.actions import user_store +from stacktask.actions.v1.base import ( + BaseAction, UserNameAction, UserMixin, ProjectMixin) + + +# TODO(adriant): Write tests for this action. +class NewProjectAction(BaseAction, ProjectMixin, UserMixin): + """ + Creates a new project for the current keystone_user. + + This action can only be used for an autheticated taskview. + """ + + required = [ + 'domain_id', + 'parent_id', + 'project_name', + ] + + def __init__(self, *args, **kwargs): + super(NewProjectAction, self).__init__(*args, **kwargs) + + def _validate(self): + self.action.valid = ( + self._validate_domain_id() and + self._validate_parent_project() and + self._validate_project_absent()) + self.action.save() + + def _validate_domain_id(self): + keystone_user = self.action.task.keystone_user + + if keystone_user['project_domain_id'] != self.domain_id: + self.add_note('Domain id does not match keystone user domain.') + return False + + return super(NewProjectAction, self)._validate_domain_id() + + def _validate_parent_project(self): + if self.parent_id: + keystone_user = self.action.task.keystone_user + + if self.parent_id != keystone_user['project_id']: + self.add_note( + 'Parent id does not match keystone user project.') + return False + return super(NewProjectAction, self)._validate_parent_project() + return True + + def _pre_approve(self): + self._validate() + + def _post_approve(self): + project_id = self.get_cache('project_id') + if project_id: + self.action.task.cache['project_id'] = project_id + self.add_note("Project already created.") + else: + self._validate() + + if not self.valid: + return + + self._create_project() + + user_id = self.get_cache('user_id') + if user_id: + self.action.task.cache['user_id'] = user_id + self.add_note("User already given roles.") + else: + default_roles = settings.ACTION_SETTINGS.get( + 'NewProjectAction', {}).get("default_roles", {}) + + project_id = self.get_cache('project_id') + keystone_user = self.action.task.keystone_user + + try: + id_manager = user_store.IdentityManager() + user = id_manager.get_user(keystone_user['user_id']) + + self.grant_roles(user, default_roles, project_id) + except Exception as e: + self.add_note( + ("Error: '%s' while adding roles %s " + "to user '%s' on project '%s'") % + (e, self.username, default_roles, project_id)) + raise + + # put user_id into action cache: + self.action.task.cache['user_id'] = user.id + self.set_cache('user_id', user.id) + self.add_note(("Existing user '%s' attached to project %s" + + " with roles: %s") + % (self.username, project_id, + default_roles)) + + def _submit(self, token_data): + """ + Nothing to do here. Everything is done at post_approve. + """ + pass + + +class NewProjectWithUserAction(UserNameAction, ProjectMixin, UserMixin): + """ + Makes a new project for the given username. Will create the user if it + doesn't exists. + """ + + required = [ + 'domain_id', + 'parent_id', + 'project_name', + 'username', + 'email' + ] + + def __init__(self, *args, **kwargs): + super(NewProjectWithUserAction, self).__init__(*args, **kwargs) + + def _validate(self): + self.action.valid = ( + self._validate_domain_id() and + self._validate_parent_project() and + self._validate_project_absent() and + self._validate_user()) + self.action.save() + + def _validate_user(self): + id_manager = user_store.IdentityManager() + user = id_manager.find_user(self.username, self.domain_id) + + if not user: + # add to cache to use in template + self.action.task.cache['user_state'] = "default" + self.action.need_token = True + self.set_token_fields(["password"]) + self.add_note("No user present with username '%s'." % + self.username) + return True + + if user.email != self.email: + self.add_note("Existing user '%s' with non-matching email." % + self.username) + return False + + if not user.enabled: + self.action.state = "disabled" + # add to cache to use in template + self.action.task.cache['user_state'] = "disabled" + self.action.need_token = True + self.add_note( + "Existing disabled user '%s' with matching email." % + self.email) + return True + else: + self.action.state = "existing" + # add to cache to use in template + self.action.task.cache['user_state'] = "existing" + self.action.need_token = False + self.add_note("Existing user '%s' with matching email." % + self.email) + return True + + def _validate_user_submit(self): + user_id = self.get_cache('user_id') + project_id = self.get_cache('project_id') + + id_manager = user_store.IdentityManager() + + user = id_manager.get_user(user_id) + project = id_manager.get_project(project_id) + + if user and project: + self.action.valid = True + else: + self.action.valid = False + + self.action.save() + + def _pre_approve(self): + self._validate() + + def _post_approve(self): + """ + Approving a new project means we set up the project itself, + and if the user doesn't exist, create it right away. An existing + user automatically gets added to the new project. + """ + project_id = self.get_cache('project_id') + if project_id: + self.action.task.cache['project_id'] = project_id + self.add_note("Project already created.") + else: + self.action.valid = ( + self._validate_domain_id() and + self._validate_parent_project() and + self._validate_project_absent()) + self.action.save() + + if not self.valid: + return + + self._create_project() + + # User validation and checks + user_id = self.get_cache('user_id') + roles_granted = self.get_cache('roles_granted') + if user_id and roles_granted: + self.action.task.cache['user_id'] = user_id + self.add_note("User already setup.") + elif not user_id: + self.action.valid = self._validate_user() + self.action.save() + + if not self.valid: + return + + self._create_user_for_project() + elif not roles_granted: + self._create_user_for_project() + + def _create_user_for_project(self): + id_manager = user_store.IdentityManager() + default_roles = settings.ACTION_SETTINGS.get( + 'NewProjectAction', {}).get("default_roles", {}) + + project_id = self.get_cache('project_id') + + if self.action.state == "default": + try: + # Generate a temporary password: + password = uuid4().hex + uuid4().hex + + user_id = self.get_cache('user_id') + if not user_id: + user = id_manager.create_user( + name=self.username, password=password, + email=self.email, domain=self.domain_id, + created_on=str(timezone.now())) + self.set_cache('user_id', user.id) + else: + user = id_manager.get_user(user_id) + # put user_id into action cache: + self.action.task.cache['user_id'] = user.id + + self.grant_roles(user, default_roles, project_id) + except Exception as e: + self.add_note( + "Error: '%s' while creating user: %s with roles: %s" % + (e, self.username, default_roles)) + raise + + self.set_cache('roles_granted', True) + self.add_note( + "New user '%s' created for project %s with roles: %s" % + (self.username, project_id, default_roles)) + elif self.action.state == "existing": + try: + user_id = self.get_cache('user_id') + if not user_id: + user = id_manager.find_user( + self.username, self.domain_id) + self.set_cache('user_id', user.id) + else: + user = id_manager.get_user(user_id) + self.action.task.cache['user_id'] = user.id + + self.grant_roles(user, default_roles, project_id) + except Exception as e: + self.add_note( + "Error: '%s' while granting roles: %s to user: %s" % + (e, default_roles, self.username)) + raise + + self.set_cache('roles_granted', True) + self.add_note(("Existing user '%s' setup on project %s" + + " with roles: %s") + % (self.username, project_id, + default_roles)) + elif self.action.state == "disabled": + user_id = self.get_cache('user_id') + if not user_id: + # first re-enable user + try: + user = id_manager.find_user(self.username, self.domain_id) + id_manager.enable_user(user) + except Exception as e: + self.add_note( + "Error: '%s' while re-enabling user: %s" % + (e, self.username)) + raise + + # and now update their password + # Generate a temporary password: + password = uuid4().hex + uuid4().hex + try: + id_manager.update_user_password(user, password) + except Exception as e: + self.add_note( + "Error: '%s' while changing password for user: %s" % + (e, self.username)) + raise + self.add_note( + 'User %s password has been changed.' % self.username) + + self.set_cache('user_id', user.id) + else: + user = id_manager.get_user(user_id) + self.action.task.cache['user_id'] = user.id + + # now add their roles + roles_granted = self.get_cache('roles_granted') + if not roles_granted: + try: + self.grant_roles(user, default_roles, project_id) + except Exception as e: + self.add_note( + "Error: '%s' while granting user: %s roles: %s" % + (e, self.username, default_roles)) + raise + self.set_cache('roles_granted', True) + + self.add_note(("Existing user '%s' setup on project %s" + + " with roles: %s") + % (self.username, project_id, + default_roles)) + + def _submit(self, token_data): + """ + The submit action is performed when a token is submitted. + This is done to set a user password only, and so should now only + change the user password. The project and user themselves are created + on post_approve. + """ + + self._validate_user_submit() + + if not self.valid: + return + + project_id = self.get_cache('project_id') + self.action.task.cache['project_id'] = project_id + user_id = self.get_cache('user_id') + self.action.task.cache['user_id'] = user_id + id_manager = user_store.IdentityManager() + + if self.action.state in ["default", "disabled"]: + user = id_manager.get_user(user_id) + try: + id_manager.update_user_password( + user, token_data['password']) + except Exception as e: + self.add_note( + "Error: '%s' while changing password for user: %s" % + (e, self.username)) + raise + self.add_note('User %s password has been changed.' % self.username) + + elif self.action.state == "existing": + # do nothing, everything is already done. + self.add_note( + "Existing user '%s' already attached to project %s" % ( + user_id, project_id)) + + +class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin): + """ + The purpose of this action is to add a given set of users after + the creation of a new Project. This is mainly for administrative + purposes, and for users involved with migrations, monitoring, and + general admin tasks that should be present by default. + """ + + required = [ + 'domain_id', + ] + + def __init__(self, *args, **kwargs): + self.users = settings.ACTION_SETTINGS.get( + 'AddDefaultUsersToProjectAction', {}).get('default_users', []) + self.roles = settings.ACTION_SETTINGS.get( + 'AddDefaultUsersToProjectAction', {}).get('default_roles', []) + super(AddDefaultUsersToProjectAction, self).__init__(*args, **kwargs) + + def _validate_users(self): + id_manager = user_store.IdentityManager() + all_found = True + for user in self.users: + ks_user = id_manager.find_user(user, self.domain_id) + if ks_user: + self.add_note('User: %s exists.' % user) + else: + self.add_note('ERROR: User: %s does not exist.' % user) + all_found = False + + return all_found + + def _pre_validate(self): + self.action.valid = self._validate_users() + self.action.save() + + def _validate(self): + self.action.valid = ( + self._validate_users() and + self._validate_project_id() + ) + self.action.save() + + def _pre_approve(self): + self._pre_validate() + + def _post_approve(self): + id_manager = user_store.IdentityManager() + self.project_id = self.action.task.cache.get('project_id', None) + self._validate() + + if self.valid and not self.action.state == "completed": + try: + for user in self.users: + ks_user = id_manager.find_user(user, self.domain_id) + + self.grant_roles(ks_user, self.roles, self.project_id) + self.add_note( + 'User: "%s" given roles: %s on project: %s.' % + (ks_user.name, self.roles, self.project_id)) + except Exception as e: + self.add_note( + "Error: '%s' while adding users to project: %s" % + (e, self.project_id)) + raise + self.action.state = "completed" + self.action.save() + self.add_note("All users added.") + + def _submit(self, token_data): + pass diff --git a/stacktask/actions/tenant_setup/models.py b/stacktask/actions/v1/resources.py similarity index 77% rename from stacktask/actions/tenant_setup/models.py rename to stacktask/actions/v1/resources.py index 966ccd1..bce9559 100644 --- a/stacktask/actions/tenant_setup/models.py +++ b/stacktask/actions/v1/resources.py @@ -12,8 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from stacktask.actions.models import BaseAction, ProjectMixin, UserMixin -from stacktask.actions.tenant_setup import serializers +from stacktask.actions.v1.base import BaseAction, ProjectMixin from django.conf import settings from stacktask.actions import openstack_clients, user_store import six @@ -217,79 +216,6 @@ class NewProjectDefaultNetworkAction(NewDefaultNetworkAction): self._create_network() -class AddDefaultUsersToProjectAction(BaseAction, ProjectMixin, UserMixin): - """ - The purpose of this action is to add a given set of users after - the creation of a new Project. This is mainly for administrative - purposes, and for users involved with migrations, monitoring, and - general admin tasks that should be present by default. - """ - - required = [ - 'domain_id', - ] - - def __init__(self, *args, **kwargs): - self.users = settings.ACTION_SETTINGS.get( - 'AddDefaultUsersToProjectAction', {}).get('default_users', []) - self.roles = settings.ACTION_SETTINGS.get( - 'AddDefaultUsersToProjectAction', {}).get('default_roles', []) - super(AddDefaultUsersToProjectAction, self).__init__(*args, **kwargs) - - def _validate_users(self): - id_manager = user_store.IdentityManager() - all_found = True - for user in self.users: - ks_user = id_manager.find_user(user, self.domain_id) - if ks_user: - self.add_note('User: %s exists.' % user) - else: - self.add_note('ERROR: User: %s does not exist.' % user) - all_found = False - - return all_found - - def _pre_validate(self): - self.action.valid = self._validate_users() - self.action.save() - - def _validate(self): - self.action.valid = ( - self._validate_users() and - self._validate_project_id() - ) - self.action.save() - - def _pre_approve(self): - self._pre_validate() - - def _post_approve(self): - id_manager = user_store.IdentityManager() - self.project_id = self.action.task.cache.get('project_id', None) - self._validate() - - if self.valid and not self.action.state == "completed": - try: - for user in self.users: - ks_user = id_manager.find_user(user, self.domain_id) - - self.grant_roles(ks_user, self.roles, self.project_id) - self.add_note( - 'User: "%s" given roles: %s on project: %s.' % - (ks_user.name, self.roles, self.project_id)) - except Exception as e: - self.add_note( - "Error: '%s' while adding users to project: %s" % - (e, self.project_id)) - raise - self.action.state = "completed" - self.action.save() - self.add_note("All users added.") - - def _submit(self, token_data): - pass - - class SetProjectQuotaAction(BaseAction): """ Updates quota for a given project to a configured quota level """ @@ -391,21 +317,3 @@ class SetProjectQuotaAction(BaseAction): def _submit(self, token_data): pass - - -action_classes = { - 'NewDefaultNetworkAction': - (NewDefaultNetworkAction, - serializers.NewDefaultNetworkSerializer), - 'NewProjectDefaultNetworkAction': - (NewProjectDefaultNetworkAction, - serializers.NewProjectDefaultNetworkSerializer), - 'AddDefaultUsersToProjectAction': - (AddDefaultUsersToProjectAction, - serializers.AddDefaultUsersToProjectSerializer), - 'SetProjectQuotaAction': - (SetProjectQuotaAction, - serializers.SetProjectQuotaSerializer) -} - -settings.ACTION_CLASSES.update(action_classes) diff --git a/stacktask/actions/serializers.py b/stacktask/actions/v1/serializers.py similarity index 80% rename from stacktask/actions/serializers.py rename to stacktask/actions/v1/serializers.py index 4171592..8abdc25 100644 --- a/stacktask/actions/serializers.py +++ b/stacktask/actions/v1/serializers.py @@ -68,3 +68,22 @@ class EditUserRolesSerializer(BaseUserIdSerializer): remove = serializers.BooleanField(default=False) project_id = serializers.CharField(max_length=64) domain_id = serializers.CharField(max_length=64, default='default') + + +class NewDefaultNetworkSerializer(serializers.Serializer): + setup_network = serializers.BooleanField(default=True) + project_id = serializers.CharField(max_length=64) + region = serializers.CharField(max_length=100) + + +class NewProjectDefaultNetworkSerializer(serializers.Serializer): + setup_network = serializers.BooleanField(default=False) + region = serializers.CharField(max_length=100) + + +class AddDefaultUsersToProjectSerializer(serializers.Serializer): + domain_id = serializers.CharField(max_length=64, default='default') + + +class SetProjectQuotaSerializer(serializers.Serializer): + pass diff --git a/stacktask/actions/v1/tests/__init__.py b/stacktask/actions/v1/tests/__init__.py new file mode 100644 index 0000000..97a25b0 --- /dev/null +++ b/stacktask/actions/v1/tests/__init__.py @@ -0,0 +1,111 @@ +# Copyright (C) 2015 Catalyst IT Ltd +# +# 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. + + +neutron_cache = {} +nova_cache = {} +cinder_cache = {} + + +class FakeOpenstackClient(object): + class Quotas(object): + """ Stub class for testing quotas """ + def __init__(self, service): + self.service = service + + def update(self, project_id, **kwargs): + self.service.update_quota(project_id, **kwargs) + + def __init__(self, region, cache): + self.region = region + self._cache = cache + self.quotas = FakeOpenstackClient.Quotas(self) + + def update_quota(self, project_id, **kwargs): + if self.region not in self._cache: + self._cache[self.region] = {} + if project_id not in self._cache[self.region]: + self._cache[self.region][project_id] = { + 'quota': {} + } + quota = self._cache[self.region][project_id]['quota'] + quota.update(kwargs) + + +class FakeNeutronClient(object): + + def create_network(self, body): + global neutron_cache + net = {'network': {'id': 'net_id_%s' % neutron_cache['i'], + 'body': body}} + neutron_cache['networks'][net['network']['id']] = net + neutron_cache['i'] += 1 + return net + + def create_subnet(self, body): + global neutron_cache + subnet = {'subnet': {'id': 'subnet_id_%s' % neutron_cache['i'], + 'body': body}} + neutron_cache['subnets'][subnet['subnet']['id']] = subnet + neutron_cache['i'] += 1 + return subnet + + def create_router(self, body): + global neutron_cache + router = {'router': {'id': 'router_id_%s' % neutron_cache['i'], + 'body': body}} + neutron_cache['routers'][router['router']['id']] = router + neutron_cache['i'] += 1 + return router + + def add_interface_router(self, router_id, body): + global neutron_cache + router = neutron_cache['routers'][router_id] + router['router']['interface'] = body + return router + + def update_quota(self, project_id, body): + global neutron_cache + if project_id not in neutron_cache: + neutron_cache[project_id] = {} + if 'quota' not in neutron_cache[project_id]: + neutron_cache[project_id]['quota'] = {} + + quota = neutron_cache[project_id]['quota'] + quota.update(body['quota']) + + +def setup_neutron_cache(): + global neutron_cache + neutron_cache.clear() + neutron_cache.update({ + 'i': 0, + 'networks': {}, + 'subnets': {}, + 'routers': {}, + }) + + +def get_fake_neutron(region): + return FakeNeutronClient() + + +def get_fake_novaclient(region): + global nova_cache + return FakeOpenstackClient(region, nova_cache) + + +def get_fake_cinderclient(region): + global cinder_cache + return FakeOpenstackClient(region, cinder_cache) diff --git a/stacktask/actions/v1/tests/test_project_actions.py b/stacktask/actions/v1/tests/test_project_actions.py new file mode 100644 index 0000000..c614dfc --- /dev/null +++ b/stacktask/actions/v1/tests/test_project_actions.py @@ -0,0 +1,548 @@ +# Copyright (C) 2015 Catalyst IT Ltd +# +# 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. + +from django.test import TestCase + +import mock + +from stacktask.actions.v1.projects import ( + NewProjectWithUserAction, AddDefaultUsersToProjectAction) +from stacktask.api.models import Task +from stacktask.api.v1 import tests +from stacktask.api.v1.tests import FakeManager, setup_temp_cache + + +@mock.patch('stacktask.actions.user_store.IdentityManager', + FakeManager) +class ProjectActionTests(TestCase): + + def test_new_project(self): + """ + Base case, no project, no user. + + Project and user created at post_approve step, + user password at submit step. + """ + + setup_temp_cache({}, {}) + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + data = { + 'domain_id': 'default', + 'parent_id': None, + 'email': 'test@example.com', + 'project_name': 'test_project', + } + + action = NewProjectWithUserAction(data, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + action.post_approve() + self.assertEquals(action.valid, True) + self.assertEquals( + tests.temp_cache['projects']['test_project'].name, + 'test_project') + self.assertEquals( + task.cache, + {'project_id': 'project_id_1', 'user_id': 'user_id_1', + 'user_state': 'default'}) + + token_data = {'password': '123456'} + action.submit(token_data) + self.assertEquals(action.valid, True) + self.assertEquals( + tests.temp_cache['users']["user_id_1"].email, + 'test@example.com') + project = tests.temp_cache['projects']['test_project'] + self.assertEquals( + sorted(project.roles["user_id_1"]), + sorted(['_member_', 'project_admin', + 'project_mod', 'heat_stack_owner'])) + + def test_new_project_reapprove(self): + """ + Project created at post_approve step, + ensure reapprove does nothing. + """ + + setup_temp_cache({}, {}) + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + data = { + 'domain_id': 'default', + 'parent_id': None, + 'email': 'test@example.com', + 'project_name': 'test_project', + } + + action = NewProjectWithUserAction(data, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + action.post_approve() + self.assertEquals(action.valid, True) + self.assertEquals( + tests.temp_cache['projects']['test_project'].name, + 'test_project') + self.assertEquals( + task.cache, + {'project_id': 'project_id_1', 'user_id': 'user_id_1', + 'user_state': 'default'}) + + action.post_approve() + self.assertEquals(action.valid, True) + self.assertEquals( + tests.temp_cache['projects']['test_project'].name, + 'test_project') + self.assertEquals( + task.cache, + {'project_id': 'project_id_1', 'user_id': 'user_id_1', + 'user_state': 'default'}) + + token_data = {'password': '123456'} + action.submit(token_data) + self.assertEquals(action.valid, True) + + self.assertEquals( + tests.temp_cache['users']["user_id_1"].email, + 'test@example.com') + project = tests.temp_cache['projects']['test_project'] + self.assertEquals( + sorted(project.roles["user_id_1"]), + sorted(['_member_', 'project_admin', + 'project_mod', 'heat_stack_owner'])) + + def test_new_project_reapprove_failure(self): + """ + Project created at post_approve step, failure at role grant. + + Ensure reapprove correctly finishes. + """ + + setup_temp_cache({}, {}) + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + data = { + 'domain_id': 'default', + 'parent_id': None, + 'email': 'test@example.com', + 'project_name': 'test_project', + } + + action = NewProjectWithUserAction(data, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + # NOTE(adrian): We need the code to fail at the + # grant roles step so we can attempt reapproving it + class FakeException(Exception): + pass + + def fail_grant(user, default_roles, project_id): + raise FakeException + # We swap out the old grant function and keep + # it for later. + old_grant_function = action.grant_roles + action.grant_roles = fail_grant + + # Now we expect the failure + self.assertRaises(FakeException, action.post_approve) + + # No roles_granted yet, but user created + self.assertTrue("user_id" in action.action.cache) + self.assertFalse("roles_granted" in action.action.cache) + self.assertEquals( + tests.temp_cache['users']["user_id_1"].email, + 'test@example.com') + project = tests.temp_cache['projects']['test_project'] + self.assertFalse("user_id_1" in project.roles) + + # And then swap back the correct function + action.grant_roles = old_grant_function + # and try again, it should work this time + action.post_approve() + self.assertEquals(action.valid, True) + # roles_granted in cache + self.assertTrue("roles_granted" in action.action.cache) + + token_data = {'password': '123456'} + action.submit(token_data) + self.assertEquals(action.valid, True) + + project = tests.temp_cache['projects']['test_project'] + self.assertEquals( + sorted(project.roles["user_id_1"]), + sorted(['_member_', 'project_admin', + 'project_mod', 'heat_stack_owner'])) + + def test_new_project_existing_user(self): + """ + Create a project for a user that already exists. + """ + + user = mock.Mock() + user.id = 'user_id_1' + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = 'default' + + setup_temp_cache({}, {user.id: user}) + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + data = { + 'domain_id': 'default', + 'parent_id': None, + 'email': 'test@example.com', + 'project_name': 'test_project', + } + + action = NewProjectWithUserAction(data, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + action.post_approve() + self.assertEquals(action.valid, True) + self.assertEquals( + tests.temp_cache['projects']['test_project'].name, + 'test_project') + self.assertEquals( + task.cache, + {'user_id': 'user_id_1', 'project_id': 'project_id_1', + 'user_state': 'existing'}) + + token_data = {'password': '123456'} + action.submit(token_data) + self.assertEquals(action.valid, True) + + self.assertEquals( + tests.temp_cache['users'][user.id].email, + 'test@example.com') + project = tests.temp_cache['projects']['test_project'] + self.assertEquals( + sorted(project.roles[user.id]), + sorted(['_member_', 'project_admin', + 'project_mod', 'heat_stack_owner'])) + + def test_new_project_disabled_user(self): + """ + Create a project for a user that is disabled. + """ + + user = mock.Mock() + user.id = 'user_id_1' + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = 'default' + user.enabled = False + + # create disabled user + setup_temp_cache({}, {user.id: user}) + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + data = { + 'domain_id': 'default', + 'parent_id': None, + 'email': 'test@example.com', + 'project_name': 'test_project', + } + + # Sign up, approve + action = NewProjectWithUserAction(data, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + action.post_approve() + self.assertEquals(action.valid, True) + self.assertEquals( + tests.temp_cache['projects']['test_project'].name, + 'test_project') + self.assertEquals( + task.cache, + {'user_id': 'user_id_1', + 'project_id': 'project_id_1', + 'user_state': 'disabled'}) + + # submit password reset + token_data = {'password': '123456'} + action.submit(token_data) + self.assertEquals(action.valid, True) + + # check that user has been created correctly + self.assertEquals( + tests.temp_cache['users'][user.id].email, + 'test@example.com') + self.assertEquals( + tests.temp_cache['users'][user.id].enabled, + True) + + # Check user has correct roles in new project + project = tests.temp_cache['projects']['test_project'] + self.assertEquals( + sorted(project.roles[user.id]), + sorted(['_member_', 'project_admin', + 'project_mod', 'heat_stack_owner'])) + + def test_new_project_user_disabled_during_signup(self): + """ + Create a project for a user that is created and disabled during signup. + + This exercises the tasks ability to correctly act based on changed + circumstances between two states. + """ + + # Start with nothing created + setup_temp_cache({}, {}) + + # Sign up for the project+user, validate. + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + data = { + 'domain_id': 'default', + 'parent_id': None, + 'email': 'test@example.com', + 'project_name': 'test_project', + } + + # Sign up + action = NewProjectWithUserAction(data, task=task, order=1) + action.pre_approve() + self.assertEquals(action.valid, True) + + # Create the disabled user directly with the Identity Manager. + fm = FakeManager() + user = fm.create_user( + name="test@example.com", + password='origpass', + email="test@example.com", + created_on=None, + domain='default', + default_project=None + ) + fm.disable_user(user.id) + + # approve previous signup + action.post_approve() + self.assertEquals(action.valid, True) + project = tests.temp_cache['projects']['test_project'] + self.assertEquals( + project.name, + 'test_project') + self.assertEquals( + task.cache, + {'user_id': user.id, + 'project_id': project.id, + 'user_state': 'disabled'}) + + # check that user has been re-enabled with a generated password. + self.assertEquals(user.enabled, True) + self.assertNotEquals(user.password, 'origpass') + + # submit password reset + token_data = {'password': '123456'} + action.submit(token_data) + self.assertEquals(action.valid, True) + + # Ensure user has new password: + self.assertEquals(user.password, '123456') + + def test_new_project_existing_project(self): + """ + Create a project that already exists. + """ + + project = mock.Mock() + project.id = 'test_project_id' + project.name = 'test_project' + project.domain = 'default' + project.roles = {} + + setup_temp_cache({project.name: project}, {}) + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={ + 'roles': ['admin', 'project_mod'], + 'project_id': 'test_project_id', + 'project_domain_id': 'default', + }) + + data = { + 'domain_id': 'default', + 'parent_id': None, + 'email': 'test@example.com', + 'project_name': 'test_project', + } + + action = NewProjectWithUserAction(data, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, False) + + action.post_approve() + self.assertEquals(action.valid, False) + + def test_new_project_invalid_domain_id(self): + """ Create a project using an invalid domain """ + + setup_temp_cache({}, {}) + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={ + 'roles': ['admin', 'project_mod'], + 'project_id': 'test_project_id', + 'project_domain_id': 'default', + }) + + data = { + 'domain_id': 'not_default_id', + 'parent_id': None, + 'email': 'test@example.com', + 'project_name': 'test_project', + } + + action = NewProjectWithUserAction(data, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, False) + + action.post_approve() + self.assertEquals(action.valid, False) + + def test_add_default_users(self): + """ + Base case, adds admin user with admin role to project. + + NOTE(adriant): both the lists of users, and the roles to add + come from test_settings. This test assumes the conf setting of: + default_users = ['admin'] + default_roles = ['admin'] + """ + project = mock.Mock() + project.id = 'test_project_id' + project.name = 'test_project' + project.domain = 'default' + project.roles = {} + + setup_temp_cache({'test_project': project}, {}) + + task = Task.objects.create( + ip_address="0.0.0.0", keystone_user={'roles': ['admin']}) + + task.cache = {'project_id': "test_project_id"} + + action = AddDefaultUsersToProjectAction( + {'domain_id': 'default'}, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + action.post_approve() + self.assertEquals(action.valid, True) + + project = tests.temp_cache['projects']['test_project'] + self.assertEquals(project.roles['user_id_0'], ['admin']) + + def test_add_default_users_invalid_project(self): + """Add default users to a project that doesn't exist. + + Action should become invalid at the post_approve state, it's ok if + the project isn't created yet during pre_approve. + """ + project = mock.Mock() + project.id = 'test_project_id' + project.name = 'test_project' + project.domain = 'default' + project.roles = {} + + setup_temp_cache({'test_project': project}, {}) + + task = Task.objects.create( + ip_address="0.0.0.0", keystone_user={'roles': ['admin']}) + + task.cache = {'project_id': "invalid_project_id"} + + action = AddDefaultUsersToProjectAction( + {'domain_id': 'default'}, task=task, order=1) + action.pre_approve() + # No need to test project yet - it's ok if it doesn't exist + self.assertEquals(action.valid, True) + + action.post_approve() + # Now the missing project should make the action invalid + self.assertEquals(action.valid, False) + + def test_add_default_users_reapprove(self): + """ + Ensure nothing happens or changes during rerun of approve. + """ + project = mock.Mock() + project.id = 'test_project_id' + project.name = 'test_project' + project.domain = 'default' + project.roles = {} + + setup_temp_cache({'test_project': project}, {}) + + task = Task.objects.create( + ip_address="0.0.0.0", keystone_user={'roles': ['admin']}) + + task.cache = {'project_id': "test_project_id"} + + action = AddDefaultUsersToProjectAction( + {'domain_id': 'default'}, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + action.post_approve() + self.assertEquals(action.valid, True) + + project = tests.temp_cache['projects']['test_project'] + self.assertEquals(project.roles['user_id_0'], ['admin']) + + action.post_approve() + self.assertEquals(action.valid, True) + + project = tests.temp_cache['projects']['test_project'] + self.assertEquals(project.roles['user_id_0'], ['admin']) diff --git a/stacktask/actions/tenant_setup/tests.py b/stacktask/actions/v1/tests/test_resource_actions.py similarity index 57% rename from stacktask/actions/tenant_setup/tests.py rename to stacktask/actions/v1/tests/test_resource_actions.py index a556fa9..093831a 100644 --- a/stacktask/actions/tenant_setup/tests.py +++ b/stacktask/actions/v1/tests/test_resource_actions.py @@ -16,120 +16,32 @@ from django.test import TestCase import mock -from stacktask.actions.tenant_setup.models import ( +from stacktask.actions.v1.resources import ( NewDefaultNetworkAction, NewProjectDefaultNetworkAction, - AddDefaultUsersToProjectAction, SetProjectQuotaAction) + SetProjectQuotaAction) from stacktask.api.models import Task -from stacktask.api.v1 import tests from stacktask.api.v1.tests import FakeManager, setup_temp_cache +from stacktask.actions.v1.tests import ( + get_fake_neutron, get_fake_novaclient, get_fake_cinderclient, + setup_neutron_cache, neutron_cache, cinder_cache, nova_cache) -neutron_cache = {} -nova_cache = {} -cinder_cache = {} - - -class FakeOpenstackClient(object): - class Quotas(object): - """ Stub class for testing quotas """ - def __init__(self, service): - self.service = service - - def update(self, project_id, **kwargs): - self.service.update_quota(project_id, **kwargs) - - def __init__(self, region, cache): - self.region = region - self._cache = cache - self.quotas = FakeOpenstackClient.Quotas(self) - - def update_quota(self, project_id, **kwargs): - if self.region not in self._cache: - self._cache[self.region] = {} - if project_id not in self._cache[self.region]: - self._cache[self.region][project_id] = { - 'quota': {} - } - quota = self._cache[self.region][project_id]['quota'] - quota.update(kwargs) - - -class FakeNeutronClient(object): - - def create_network(self, body): - global neutron_cache - net = {'network': {'id': 'net_id_%s' % neutron_cache['i'], - 'body': body}} - neutron_cache['networks'][net['network']['id']] = net - neutron_cache['i'] += 1 - return net - - def create_subnet(self, body): - global neutron_cache - subnet = {'subnet': {'id': 'subnet_id_%s' % neutron_cache['i'], - 'body': body}} - neutron_cache['subnets'][subnet['subnet']['id']] = subnet - neutron_cache['i'] += 1 - return subnet - - def create_router(self, body): - global neutron_cache - router = {'router': {'id': 'router_id_%s' % neutron_cache['i'], - 'body': body}} - neutron_cache['routers'][router['router']['id']] = router - neutron_cache['i'] += 1 - return router - - def add_interface_router(self, router_id, body): - global neutron_cache - router = neutron_cache['routers'][router_id] - router['router']['interface'] = body - return router - - def update_quota(self, project_id, body): - global neutron_cache - if project_id not in neutron_cache: - neutron_cache[project_id] = {} - if 'quota' not in neutron_cache[project_id]: - neutron_cache[project_id]['quota'] = {} - - quota = neutron_cache[project_id]['quota'] - quota.update(body['quota']) - - -def setup_neutron_cache(): - global neutron_cache - neutron_cache = { - 'i': 0, - 'networks': {}, - 'subnets': {}, - 'routers': {}, - } - - -def get_fake_neutron(region): - return FakeNeutronClient() - - -def get_fake_novaclient(region): - global nova_cache - return FakeOpenstackClient(region, nova_cache) - - -def get_fake_cinderclient(region): - global cinder_cache - return FakeOpenstackClient(region, cinder_cache) - - +@mock.patch('stacktask.actions.user_store.IdentityManager', + FakeManager) +@mock.patch( + 'stacktask.actions.v1.resources.' + + 'openstack_clients.get_neutronclient', + get_fake_neutron) +@mock.patch( + 'stacktask.actions.v1.resources.' + + 'openstack_clients.get_novaclient', + get_fake_novaclient) +@mock.patch( + 'stacktask.actions.v1.resources.' + + 'openstack_clients.get_cinderclient', + get_fake_cinderclient) class ProjectSetupActionTests(TestCase): - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.' + - 'openstack_clients.get_neutronclient', - get_fake_neutron) def test_network_setup(self): """ Base case, setup a new network , no issues. @@ -176,13 +88,6 @@ class ProjectSetupActionTests(TestCase): self.assertEquals(len(neutron_cache['routers']), 1) self.assertEquals(len(neutron_cache['subnets']), 1) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.' + - 'openstack_clients.get_neutronclient', - get_fake_neutron) def test_network_setup_no_setup(self): """ Told not to setup, should do nothing. @@ -224,13 +129,6 @@ class ProjectSetupActionTests(TestCase): self.assertEquals(len(neutron_cache['routers']), 0) self.assertEquals(len(neutron_cache['subnets']), 0) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.' + - 'openstack_clients.get_neutronclient', - get_fake_neutron) def test_network_setup_fail(self): """ Should fail, but on re_approve will continue where it left off. @@ -296,13 +194,6 @@ class ProjectSetupActionTests(TestCase): self.assertEquals(len(neutron_cache['routers']), 1) self.assertEquals(len(neutron_cache['subnets']), 1) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.' + - 'openstack_clients.get_neutronclient', - get_fake_neutron) def test_new_project_network_setup(self): """ Base case, setup network after a new project, no issues. @@ -349,13 +240,6 @@ class ProjectSetupActionTests(TestCase): self.assertEquals(len(neutron_cache['routers']), 1) self.assertEquals(len(neutron_cache['subnets']), 1) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.' + - 'openstack_clients.get_neutronclient', - get_fake_neutron) def test_new_project_network_setup_no_id(self): """ No project id given, should do nothing. @@ -385,13 +269,6 @@ class ProjectSetupActionTests(TestCase): self.assertEquals(len(neutron_cache['routers']), 0) self.assertEquals(len(neutron_cache['subnets']), 0) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.' + - 'openstack_clients.get_neutronclient', - get_fake_neutron) def test_new_project_network_setup_no_setup(self): """ Told not to setup, should do nothing. @@ -433,13 +310,6 @@ class ProjectSetupActionTests(TestCase): self.assertEquals(len(neutron_cache['routers']), 0) self.assertEquals(len(neutron_cache['subnets']), 0) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.' + - 'openstack_clients.get_neutronclient', - get_fake_neutron) def test_new_project_network_setup_fail(self): """ Should fail, but on re_approve will continue where it left off. @@ -505,129 +375,6 @@ class ProjectSetupActionTests(TestCase): self.assertEquals(len(neutron_cache['routers']), 1) self.assertEquals(len(neutron_cache['subnets']), 1) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - def test_add_default_users(self): - """ - Base case, adds admin user with admin role to project. - - NOTE(adriant): both the lists of users, and the roles to add - come from test_settings. This test assumes the conf setting of: - default_users = ['admin'] - default_roles = ['admin'] - """ - project = mock.Mock() - project.id = 'test_project_id' - project.name = 'test_project' - project.domain = 'default' - project.roles = {} - - setup_temp_cache({'test_project': project}, {}) - - task = Task.objects.create( - ip_address="0.0.0.0", keystone_user={'roles': ['admin']}) - - task.cache = {'project_id': "test_project_id"} - - action = AddDefaultUsersToProjectAction( - {'domain_id': 'default'}, task=task, order=1) - - action.pre_approve() - self.assertEquals(action.valid, True) - - action.post_approve() - self.assertEquals(action.valid, True) - - project = tests.temp_cache['projects']['test_project'] - self.assertEquals(project.roles['user_id_0'], ['admin']) - - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - def test_add_default_users_invalid_project(self): - """Add default users to a project that doesn't exist. - - Action should become invalid at the post_approve state, it's ok if - the project isn't created yet during pre_approve. - """ - project = mock.Mock() - project.id = 'test_project_id' - project.name = 'test_project' - project.domain = 'default' - project.roles = {} - - setup_temp_cache({'test_project': project}, {}) - - task = Task.objects.create( - ip_address="0.0.0.0", keystone_user={'roles': ['admin']}) - - task.cache = {'project_id': "invalid_project_id"} - - action = AddDefaultUsersToProjectAction( - {'domain_id': 'default'}, task=task, order=1) - - action.pre_approve() - # No need to test project yet - it's ok if it doesn't exist - self.assertEquals(action.valid, True) - - action.post_approve() - # Now the missing project should make the action invalid - self.assertEquals(action.valid, False) - - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - def test_add_default_users_reapprove(self): - """ - Ensure nothing happens or changes during rerun of approve. - """ - project = mock.Mock() - project.id = 'test_project_id' - project.name = 'test_project' - project.domain = 'default' - project.roles = {} - - setup_temp_cache({'test_project': project}, {}) - - task = Task.objects.create( - ip_address="0.0.0.0", keystone_user={'roles': ['admin']}) - - task.cache = {'project_id': "test_project_id"} - - action = AddDefaultUsersToProjectAction( - {'domain_id': 'default'}, task=task, order=1) - - action.pre_approve() - self.assertEquals(action.valid, True) - - action.post_approve() - self.assertEquals(action.valid, True) - - project = tests.temp_cache['projects']['test_project'] - self.assertEquals(project.roles['user_id_0'], ['admin']) - - action.post_approve() - self.assertEquals(action.valid, True) - - project = tests.temp_cache['projects']['test_project'] - self.assertEquals(project.roles['user_id_0'], ['admin']) - - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.' + - 'openstack_clients.get_neutronclient', - get_fake_neutron) - @mock.patch( - 'stacktask.actions.tenant_setup.models.' + - 'openstack_clients.get_novaclient', - get_fake_novaclient) - @mock.patch( - 'stacktask.actions.tenant_setup.models.' + - 'openstack_clients.get_cinderclient', - get_fake_cinderclient) def test_set_quota(self): """ Base case, sets quota on all services of the cached project id. diff --git a/stacktask/actions/tests.py b/stacktask/actions/v1/tests/test_user_actions.py similarity index 55% rename from stacktask/actions/tests.py rename to stacktask/actions/v1/tests/test_user_actions.py index 4884636..61eade3 100644 --- a/stacktask/actions/tests.py +++ b/stacktask/actions/v1/tests/test_user_actions.py @@ -16,18 +16,17 @@ from django.test import TestCase import mock -from stacktask.actions.models import ( - EditUserRolesAction, NewProjectWithUserAction, NewUserAction, - ResetUserPasswordAction) +from stacktask.actions.v1.users import ( + EditUserRolesAction, NewUserAction, ResetUserPasswordAction) from stacktask.api.models import Task from stacktask.api.v1 import tests from stacktask.api.v1.tests import FakeManager, setup_temp_cache -class ActionTests(TestCase): +@mock.patch('stacktask.actions.user_store.IdentityManager', + FakeManager) +class UserActionTests(TestCase): - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user(self): """ Test the default case, all valid. @@ -78,8 +77,6 @@ class ActionTests(TestCase): self.assertEquals(project.roles["user_id_1"], ['_member_']) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user_existing(self): """ Existing user, valid tenant, no role. @@ -127,8 +124,6 @@ class ActionTests(TestCase): self.assertEquals(project.roles[user.id], ['_member_']) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user_disabled(self): """ Disabled user, valid existing tenant, no role. @@ -188,8 +183,6 @@ class ActionTests(TestCase): self.assertEquals(project.roles["user_id_1"], ['_member_']) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user_existing_role(self): """ Existing user, valid tenant, has role. @@ -242,8 +235,6 @@ class ActionTests(TestCase): self.assertEquals(project.roles[user.id], ['_member_']) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user_no_tenant(self): """ No user, no tenant. @@ -278,8 +269,6 @@ class ActionTests(TestCase): action.submit(token_data) self.assertEquals(action.valid, False) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user_wrong_project(self): """ Existing user, valid project, project does not match keystone user. @@ -321,8 +310,6 @@ class ActionTests(TestCase): action.pre_approve() self.assertEquals(action.valid, False) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user_only_member(self): """ Existing user, valid project, no edit permissions. @@ -364,8 +351,6 @@ class ActionTests(TestCase): action.pre_approve() self.assertFalse(action.valid) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user_wrong_domain(self): """ Existing user, valid project, invalid domain. @@ -407,446 +392,6 @@ class ActionTests(TestCase): action.pre_approve() self.assertFalse(action.valid) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - def test_new_project(self): - """ - Base case, no project, no user. - - Project and user created at post_approve step, - user password at submit step. - """ - - setup_temp_cache({}, {}) - - task = Task.objects.create( - ip_address="0.0.0.0", - keystone_user={} - ) - - data = { - 'domain_id': 'default', - 'parent_id': None, - 'email': 'test@example.com', - 'project_name': 'test_project', - } - - action = NewProjectWithUserAction(data, task=task, order=1) - - action.pre_approve() - self.assertEquals(action.valid, True) - - action.post_approve() - self.assertEquals(action.valid, True) - self.assertEquals( - tests.temp_cache['projects']['test_project'].name, - 'test_project') - self.assertEquals( - task.cache, - {'project_id': 'project_id_1', 'user_id': 'user_id_1', - 'user_state': 'default'}) - - token_data = {'password': '123456'} - action.submit(token_data) - self.assertEquals(action.valid, True) - self.assertEquals( - tests.temp_cache['users']["user_id_1"].email, - 'test@example.com') - project = tests.temp_cache['projects']['test_project'] - self.assertEquals( - sorted(project.roles["user_id_1"]), - sorted(['_member_', 'project_admin', - 'project_mod', 'heat_stack_owner'])) - - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - def test_new_project_reapprove(self): - """ - Project created at post_approve step, - ensure reapprove does nothing. - """ - - setup_temp_cache({}, {}) - - task = Task.objects.create( - ip_address="0.0.0.0", - keystone_user={} - ) - - data = { - 'domain_id': 'default', - 'parent_id': None, - 'email': 'test@example.com', - 'project_name': 'test_project', - } - - action = NewProjectWithUserAction(data, task=task, order=1) - - action.pre_approve() - self.assertEquals(action.valid, True) - - action.post_approve() - self.assertEquals(action.valid, True) - self.assertEquals( - tests.temp_cache['projects']['test_project'].name, - 'test_project') - self.assertEquals( - task.cache, - {'project_id': 'project_id_1', 'user_id': 'user_id_1', - 'user_state': 'default'}) - - action.post_approve() - self.assertEquals(action.valid, True) - self.assertEquals( - tests.temp_cache['projects']['test_project'].name, - 'test_project') - self.assertEquals( - task.cache, - {'project_id': 'project_id_1', 'user_id': 'user_id_1', - 'user_state': 'default'}) - - token_data = {'password': '123456'} - action.submit(token_data) - self.assertEquals(action.valid, True) - - self.assertEquals( - tests.temp_cache['users']["user_id_1"].email, - 'test@example.com') - project = tests.temp_cache['projects']['test_project'] - self.assertEquals( - sorted(project.roles["user_id_1"]), - sorted(['_member_', 'project_admin', - 'project_mod', 'heat_stack_owner'])) - - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - def test_new_project_reapprove_failure(self): - """ - Project created at post_approve step, failure at role grant. - - Ensure reapprove correctly finishes. - """ - - setup_temp_cache({}, {}) - - task = Task.objects.create( - ip_address="0.0.0.0", - keystone_user={} - ) - - data = { - 'domain_id': 'default', - 'parent_id': None, - 'email': 'test@example.com', - 'project_name': 'test_project', - } - - action = NewProjectWithUserAction(data, task=task, order=1) - - action.pre_approve() - self.assertEquals(action.valid, True) - - # NOTE(adrian): We need the code to fail at the - # grant roles step so we can attempt reapproving it - class FakeException(Exception): - pass - - def fail_grant(user, default_roles, project_id): - raise FakeException - # We swap out the old grant function and keep - # it for later. - old_grant_function = action.grant_roles - action.grant_roles = fail_grant - - # Now we expect the failure - self.assertRaises(FakeException, action.post_approve) - - # No roles_granted yet, but user created - self.assertTrue("user_id" in action.action.cache) - self.assertFalse("roles_granted" in action.action.cache) - self.assertEquals( - tests.temp_cache['users']["user_id_1"].email, - 'test@example.com') - project = tests.temp_cache['projects']['test_project'] - self.assertFalse("user_id_1" in project.roles) - - # And then swap back the correct function - action.grant_roles = old_grant_function - # and try again, it should work this time - action.post_approve() - self.assertEquals(action.valid, True) - # roles_granted in cache - self.assertTrue("roles_granted" in action.action.cache) - - token_data = {'password': '123456'} - action.submit(token_data) - self.assertEquals(action.valid, True) - - project = tests.temp_cache['projects']['test_project'] - self.assertEquals( - sorted(project.roles["user_id_1"]), - sorted(['_member_', 'project_admin', - 'project_mod', 'heat_stack_owner'])) - - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - def test_new_project_existing_user(self): - """ - Create a project for a user that already exists. - """ - - user = mock.Mock() - user.id = 'user_id_1' - user.name = "test@example.com" - user.email = "test@example.com" - user.domain = 'default' - - setup_temp_cache({}, {user.id: user}) - - task = Task.objects.create( - ip_address="0.0.0.0", - keystone_user={} - ) - - data = { - 'domain_id': 'default', - 'parent_id': None, - 'email': 'test@example.com', - 'project_name': 'test_project', - } - - action = NewProjectWithUserAction(data, task=task, order=1) - - action.pre_approve() - self.assertEquals(action.valid, True) - - action.post_approve() - self.assertEquals(action.valid, True) - self.assertEquals( - tests.temp_cache['projects']['test_project'].name, - 'test_project') - self.assertEquals( - task.cache, - {'user_id': 'user_id_1', 'project_id': 'project_id_1', - 'user_state': 'existing'}) - - token_data = {'password': '123456'} - action.submit(token_data) - self.assertEquals(action.valid, True) - - self.assertEquals( - tests.temp_cache['users'][user.id].email, - 'test@example.com') - project = tests.temp_cache['projects']['test_project'] - self.assertEquals( - sorted(project.roles[user.id]), - sorted(['_member_', 'project_admin', - 'project_mod', 'heat_stack_owner'])) - - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - def test_new_project_disabled_user(self): - """ - Create a project for a user that is disabled. - """ - - user = mock.Mock() - user.id = 'user_id_1' - user.name = "test@example.com" - user.email = "test@example.com" - user.domain = 'default' - user.enabled = False - - # create disabled user - setup_temp_cache({}, {user.id: user}) - - task = Task.objects.create( - ip_address="0.0.0.0", - keystone_user={} - ) - - data = { - 'domain_id': 'default', - 'parent_id': None, - 'email': 'test@example.com', - 'project_name': 'test_project', - } - - # Sign up, approve - action = NewProjectWithUserAction(data, task=task, order=1) - - action.pre_approve() - self.assertEquals(action.valid, True) - - action.post_approve() - self.assertEquals(action.valid, True) - self.assertEquals( - tests.temp_cache['projects']['test_project'].name, - 'test_project') - self.assertEquals( - task.cache, - {'user_id': 'user_id_1', - 'project_id': 'project_id_1', - 'user_state': 'disabled'}) - - # submit password reset - token_data = {'password': '123456'} - action.submit(token_data) - self.assertEquals(action.valid, True) - - # check that user has been created correctly - self.assertEquals( - tests.temp_cache['users'][user.id].email, - 'test@example.com') - self.assertEquals( - tests.temp_cache['users'][user.id].enabled, - True) - - # Check user has correct roles in new project - project = tests.temp_cache['projects']['test_project'] - self.assertEquals( - sorted(project.roles[user.id]), - sorted(['_member_', 'project_admin', - 'project_mod', 'heat_stack_owner'])) - - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - def test_new_project_user_disabled_during_signup(self): - """ - Create a project for a user that is created and disabled during signup. - - This exercises the tasks ability to correctly act based on changed - circumstances between two states. - """ - - # Start with nothing created - setup_temp_cache({}, {}) - - # Sign up for the project+user, validate. - task = Task.objects.create( - ip_address="0.0.0.0", - keystone_user={} - ) - - data = { - 'domain_id': 'default', - 'parent_id': None, - 'email': 'test@example.com', - 'project_name': 'test_project', - } - - # Sign up - action = NewProjectWithUserAction(data, task=task, order=1) - action.pre_approve() - self.assertEquals(action.valid, True) - - # Create the disabled user directly with the Identity Manager. - fm = FakeManager() - user = fm.create_user( - name="test@example.com", - password='origpass', - email="test@example.com", - created_on=None, - domain='default', - default_project=None - ) - fm.disable_user(user.id) - - # approve previous signup - action.post_approve() - self.assertEquals(action.valid, True) - project = tests.temp_cache['projects']['test_project'] - self.assertEquals( - project.name, - 'test_project') - self.assertEquals( - task.cache, - {'user_id': user.id, - 'project_id': project.id, - 'user_state': 'disabled'}) - - # check that user has been re-enabled with a generated password. - self.assertEquals(user.enabled, True) - self.assertNotEquals(user.password, 'origpass') - - # submit password reset - token_data = {'password': '123456'} - action.submit(token_data) - self.assertEquals(action.valid, True) - - # Ensure user has new password: - self.assertEquals(user.password, '123456') - - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - def test_new_project_existing_project(self): - """ - Create a project that already exists. - """ - - project = mock.Mock() - project.id = 'test_project_id' - project.name = 'test_project' - project.domain = 'default' - project.roles = {} - - setup_temp_cache({project.name: project}, {}) - - task = Task.objects.create( - ip_address="0.0.0.0", - keystone_user={ - 'roles': ['admin', 'project_mod'], - 'project_id': 'test_project_id', - 'project_domain_id': 'default', - }) - - data = { - 'domain_id': 'default', - 'parent_id': None, - 'email': 'test@example.com', - 'project_name': 'test_project', - } - - action = NewProjectWithUserAction(data, task=task, order=1) - - action.pre_approve() - self.assertEquals(action.valid, False) - - action.post_approve() - self.assertEquals(action.valid, False) - - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - def test_new_project_invalid_domain_id(self): - """ Create a project using an invalid domain """ - - setup_temp_cache({}, {}) - - task = Task.objects.create( - ip_address="0.0.0.0", - keystone_user={ - 'roles': ['admin', 'project_mod'], - 'project_id': 'test_project_id', - 'project_domain_id': 'default', - }) - - data = { - 'domain_id': 'not_default_id', - 'parent_id': None, - 'email': 'test@example.com', - 'project_name': 'test_project', - } - - action = NewProjectWithUserAction(data, task=task, order=1) - - action.pre_approve() - self.assertEquals(action.valid, False) - - action.post_approve() - self.assertEquals(action.valid, False) - - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_reset_user_password(self): """ Base case, existing user. @@ -891,8 +436,6 @@ class ActionTests(TestCase): tests.temp_cache['users'][user.id].password, '123456') - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_reset_user_password_no_user(self): """ Reset password for a non-existant user. @@ -926,8 +469,6 @@ class ActionTests(TestCase): action.submit(token_data) self.assertEquals(action.valid, False) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_edit_user_roles_add(self): """ Add roles to existing user. @@ -978,8 +519,6 @@ class ActionTests(TestCase): self.assertEquals(set(project.roles[user.id]), set(['_member_', 'project_mod'])) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_edit_user_roles_add_complete(self): """ Add roles to existing user. @@ -1031,8 +570,6 @@ class ActionTests(TestCase): self.assertEquals(set(project.roles[user.id]), set(['_member_', 'project_mod'])) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_edit_user_roles_remove(self): """ Remove roles from existing user. @@ -1082,8 +619,6 @@ class ActionTests(TestCase): self.assertEquals(project.roles[user.id], ['_member_']) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_edit_user_roles_remove_complete(self): """ Remove roles from user that does not have them. diff --git a/stacktask/actions/v1/users.py b/stacktask/actions/v1/users.py new file mode 100644 index 0000000..dd88b6e --- /dev/null +++ b/stacktask/actions/v1/users.py @@ -0,0 +1,315 @@ +# Copyright (C) 2015 Catalyst IT Ltd +# +# 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. + +from django.conf import settings +from django.db import models + +from stacktask.actions import user_store +from stacktask.actions.v1.base import ( + UserNameAction, UserIdAction, UserMixin, ProjectMixin) + + +class NewUserAction(UserNameAction, ProjectMixin, UserMixin): + """ + Setup a new user with a role on the given project. + Creates the user if they don't exist, otherwise + if the username and email for the request match the + existing one, will simply add the project role. + """ + + required = [ + 'username', + 'email', + 'project_id', + 'roles', + 'domain_id', + ] + + def _validate_target_user(self): + id_manager = user_store.IdentityManager() + + # check if user exists and is valid + # this may mean we need a token. + user = self._get_target_user() + if not user: + self.action.need_token = True + # add to cache to use in template + self.action.task.cache['user_state'] = "default" + self.set_token_fields(["password"]) + self.add_note( + 'No user present with username. Need to create new user.') + return True + if user.email != self.email: + self.add_note( + 'Found matching username, but email did not match.' + + 'Reporting as invalid.') + return False + + if not user.enabled: + self.action.need_token = True + self.action.state = "disabled" + # add to cache to use in template + self.action.task.cache['user_state'] = "disabled" + # as they are disabled we'll reset their password + self.set_token_fields(["password"]) + self.add_note( + 'Existing disabled user with matching email.') + return True + + # role_validation + roles = id_manager.get_roles(user, self.project_id) + role_names = {role.name for role in roles} + missing = set(self.roles) - role_names + if not missing: + self.action.need_token = False + self.action.state = "complete" + self.add_note( + 'Existing user already has roles.' + ) + else: + self.roles = list(missing) + self.action.need_token = True + self.set_token_fields(["confirm"]) + self.action.state = "existing" + # add to cache to use in template + self.action.task.cache['user_state'] = "existing" + self.add_note( + 'Existing user with matching email missing roles.') + + return True + + def _validate(self): + self.action.valid = ( + self._validate_role_permissions() and + self._validate_keystone_user() and + self._validate_domain_id() and + self._validate_project_id() and + self._validate_target_user() + ) + self.action.save() + + def _pre_approve(self): + self._validate() + + def _post_approve(self): + self._validate() + + def _submit(self, token_data): + self._validate() + + if not self.valid: + return + + if self.action.state == "default": + # default action: Create a new user in the tenant and add roles + user = self.create_user(token_data['password']) + self.grant_roles(user, self.roles, self.project_id) + + self.add_note( + 'User %s has been created, with roles %s in project %s.' + % (self.username, self.roles, self.project_id)) + + elif self.action.state == "disabled": + # first re-enable user + user = self.find_user() + self.enable_user(user) + self.grant_roles(user, self.roles, self.project_id) + self.update_password(token_data['password']) + + self.add_note('User %s password has been changed.' % self.username) + + self.add_note( + 'Existing user %s has been re-enabled and given roles %s' + ' in project %s.' + % (self.username, self.roles, self.project_id)) + + elif self.action.state == "existing": + # Existing action: only add roles. + user = self.find_user() + self.grant_roles(user, self.roles, self.project_id) + + self.add_note( + 'Existing user %s has been given roles %s in project %s.' + % (self.username, self.roles, self.project_id)) + elif self.action.state == "complete": + # complete action: nothing to do. + self.add_note( + 'Existing user %s already had roles %s in project %s.' + % (self.username, self.roles, self.project_id)) + + +class ResetUserPasswordAction(UserNameAction, UserMixin): + """ + Simple action to reset a password for a given user. + """ + + username = models.CharField(max_length=200) + email = models.EmailField() + + required = [ + 'domain_name', + 'username', + 'email' + ] + + blacklist = settings.ACTION_SETTINGS.get( + 'ResetUserPasswordAction', {}).get("blacklisted_roles", {}) + + def _validate_user_roles(self): + id_manager = user_store.IdentityManager() + + self.user = id_manager.find_user(self.username, self.domain.id) + roles = id_manager.get_all_roles(self.user) + + user_roles = [] + for roles in roles.itervalues(): + user_roles.extend(role.name for role in roles) + + if set(self.blacklist) & set(user_roles): + self.add_note('Cannot reset users with blacklisted roles.') + return False + + if self.user.email == self.email: + self.action.need_token = True + self.set_token_fields(["password"]) + self.add_note('Existing user with matching email.') + return True + else: + self.add_note('Existing user with non-matching email.') + return False + + def _validate(self): + # Here, the order of validation matters + # as each one adds new class variables + self.action.valid = ( + self._validate_domain_name() and + self._validate_username_exists() and + self._validate_user_roles() + ) + self.action.save() + + def _pre_approve(self): + self._validate() + + def _post_approve(self): + self._validate() + + def _submit(self, token_data): + self._validate() + + if not self.valid: + return + + self.update_password(token_data['password']) + self.add_note('User %s password has been changed.' % self.username) + + +class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin): + """ + A class for adding or removing roles + on a user for the given project. + """ + + required = [ + 'domain_id', + 'project_id', + 'user_id', + 'roles', + 'remove' + ] + + def _validate_target_user(self): + # Get target user + user = self._get_target_user() + if not user: + self.add_note('No user present with user_id') + return False + return True + + def _validate_user_roles(self): + id_manager = user_store.IdentityManager() + user = self._get_target_user() + project = id_manager.get_project(self.project_id) + # user roles + current_roles = id_manager.get_roles(user, project) + current_role_names = {role.name for role in current_roles} + if self.remove: + remaining = set(current_role_names) & set(self.roles) + if not remaining: + self.action.state = "complete" + self.add_note( + "User doesn't have roles to remove.") + else: + self.roles = list(remaining) + self.add_note( + 'User has roles to remove.') + else: + missing = set(self.roles) - set(current_role_names) + if not missing: + self.action.state = "complete" + self.add_note( + 'User already has roles.') + else: + self.roles = list(missing) + self.add_note( + 'User user missing roles.') + # All paths are valid here + # We've just set state and roles that need to be changed. + return True + + def _validate(self): + self.action.valid = ( + self._validate_keystone_user() and + self._validate_role_permissions() and + self._validate_domain_id() and + self._validate_project_id() and + self._validate_target_user() and + self._validate_user_roles() + ) + self.action.save() + + def _pre_approve(self): + self._validate() + + def _post_approve(self): + self._validate() + + def _submit(self, token_data): + self._validate() + + if not self.valid: + return + + if self.action.state == "default": + user = self._get_target_user() + self._user_roles_edit(user, self.roles, self.project_id, + remove=self.remove) + + if self.remove: + self.add_note( + 'User %s has had roles %s removed from project %s.' + % (self.user_id, self.roles, self.project_id)) + else: + self.add_note( + 'User %s has been given roles %s in project %s.' + % (self.user_id, self.roles, self.project_id)) + elif self.action.state == "complete": + if self.remove: + self.add_note( + 'User %s already had roles %s in project %s.' + % (self.user_id, self.roles, self.project_id)) + else: + self.add_note( + "User %s didn't have roles %s in project %s." + % (self.user_id, self.roles, self.project_id)) diff --git a/stacktask/api/__init__.py b/stacktask/api/__init__.py index e29f5f2..e69de29 100644 --- a/stacktask/api/__init__.py +++ b/stacktask/api/__init__.py @@ -1 +0,0 @@ -default_app_config = 'stacktask.api.startup.APIConfig' diff --git a/stacktask/api/v1/__init__.py b/stacktask/api/v1/__init__.py index e69de29..7756e17 100644 --- a/stacktask/api/v1/__init__.py +++ b/stacktask/api/v1/__init__.py @@ -0,0 +1 @@ +default_app_config = 'stacktask.api.v1.app.APIV1Config' diff --git a/stacktask/api/v1/app.py b/stacktask/api/v1/app.py new file mode 100644 index 0000000..8cdeb5c --- /dev/null +++ b/stacktask/api/v1/app.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class APIV1Config(AppConfig): + name = "stacktask.api.v1" + label = 'api_v1' diff --git a/stacktask/api/v1/tests/test_api_admin.py b/stacktask/api/v1/tests/test_api_admin.py index 1de7420..d82deb7 100644 --- a/stacktask/api/v1/tests/test_api_admin.py +++ b/stacktask/api/v1/tests/test_api_admin.py @@ -29,6 +29,8 @@ from stacktask.api.models import Task, Token from stacktask.api.v1.tests import FakeManager, setup_temp_cache +@mock.patch('stacktask.actions.user_store.IdentityManager', + FakeManager) class AdminAPITests(APITestCase): """ Tests to ensure the admin api endpoints work as expected within @@ -57,11 +59,6 @@ class AdminAPITests(APITestCase): response.data, {'errors': ['This token does not exist or has expired.']}) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_task_get(self): """ Test the basic task detail view. @@ -122,8 +119,6 @@ class AdminAPITests(APITestCase): self.assertEqual( response.data, {'errors': ['No task with this id.']}) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_token_expired_post(self): """ Expired token should do nothing, then delete itself. @@ -158,8 +153,6 @@ class AdminAPITests(APITestCase): {'errors': ['This token does not exist or has expired.']}) self.assertEqual(0, Token.objects.count()) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_token_expired_get(self): """ Expired token should do nothing, then delete itself. @@ -193,11 +186,6 @@ class AdminAPITests(APITestCase): {'errors': ['This token does not exist or has expired.']}) self.assertEqual(0, Token.objects.count()) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_task_complete(self): """ Can't approve a completed task. @@ -228,11 +216,6 @@ class AdminAPITests(APITestCase): response.data, {'errors': ['This task has already been completed.']}) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_task_update(self): """ Creates a invalid task. @@ -287,11 +270,6 @@ class AdminAPITests(APITestCase): response.data, {'notes': ['created token']}) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_notification_acknowledge(self): """ Test that you can acknowledge a notification. @@ -339,11 +317,6 @@ class AdminAPITests(APITestCase): ) self.assertEqual(response.data, {'notifications': []}) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_notification_acknowledge_list(self): """ Test that you can acknowledge a list of notifications. @@ -389,8 +362,6 @@ class AdminAPITests(APITestCase): ) self.assertEqual(response.data, {'notifications': []}) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_token_expired_delete(self): """ test deleting of expired tokens. @@ -451,8 +422,6 @@ class AdminAPITests(APITestCase): {'notes': ['Deleted all expired tokens.']}) self.assertEqual(Token.objects.count(), 1) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_token_reissue(self): """ test for reissue of tokens @@ -499,8 +468,6 @@ class AdminAPITests(APITestCase): new_token = Token.objects.all()[0] self.assertNotEquals(new_token.token, uuid) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_token_reissue_non_admin(self): """ test for reissue of tokens for non-admin @@ -553,11 +520,6 @@ class AdminAPITests(APITestCase): self.assertEqual(response.data, {'errors': ['No task with this id.']}) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_cancel_task(self): """ Ensure the ability to cancel a task. @@ -592,11 +554,6 @@ class AdminAPITests(APITestCase): headers=headers) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_cancel_task_sent_token(self): """ Ensure the ability to cancel a task after the token is sent. @@ -633,11 +590,6 @@ class AdminAPITests(APITestCase): response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_task_update_unapprove(self): """ Ensure task update doesn't work for approved actions. @@ -678,11 +630,6 @@ class AdminAPITests(APITestCase): headers=headers) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_cancel_task_own(self): """ Ensure the ability to cancel your own task. @@ -726,11 +673,6 @@ class AdminAPITests(APITestCase): headers=headers) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_cancel_task_own_fail(self): """ Ensure the ability to cancel ONLY your own task. @@ -766,8 +708,6 @@ class AdminAPITests(APITestCase): headers=headers) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) def test_task_list(self): """ """ @@ -814,8 +754,6 @@ class AdminAPITests(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['tasks']), 3) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) def test_task_list_ordering(self): """ Test that tasks returns in the default sort. @@ -870,11 +808,6 @@ class AdminAPITests(APITestCase): for i, task in enumerate(sorted_list): self.assertEqual(task, response.data['tasks'][i]) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_task_list_filter(self): """ """ @@ -944,8 +877,6 @@ class AdminAPITests(APITestCase): # TODO(adriant): enable this test again when filters are properly # blacklisted. @skip("Does not apply yet.") - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) def test_task_list_filter_cross_project(self): """ Ensure you can't override the initial project_id filter if @@ -1003,8 +934,6 @@ class AdminAPITests(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['tasks']), 0) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) def test_task_list_filter_formating(self): """ """ @@ -1093,11 +1022,6 @@ class AdminAPITests(APITestCase): ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_reset_admin(self): """ Ensure that you cannot issue a password reset for an diff --git a/stacktask/api/v1/tests/test_api_openstack.py b/stacktask/api/v1/tests/test_api_openstack.py index 3380829..b2eac57 100644 --- a/stacktask/api/v1/tests/test_api_openstack.py +++ b/stacktask/api/v1/tests/test_api_openstack.py @@ -21,6 +21,8 @@ from stacktask.api.models import Token from stacktask.api.v1.tests import FakeManager, setup_temp_cache +@mock.patch('stacktask.actions.user_store.IdentityManager', + FakeManager) class OpenstackAPITests(APITestCase): """ TaskView tests specific to the openstack style urls. @@ -29,8 +31,6 @@ class OpenstackAPITests(APITestCase): unique TaskViews need testing. """ - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user(self): """ Ensure the new user workflow goes as expected. @@ -65,8 +65,6 @@ class OpenstackAPITests(APITestCase): response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_user_list(self): """ """ @@ -110,8 +108,6 @@ class OpenstackAPITests(APITestCase): response = self.client.get(url, headers=headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) def test_force_reset_password(self): """ Ensure the force password endpoint works as expected, @@ -158,8 +154,6 @@ class OpenstackAPITests(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(user.password, 'new_test_password') - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) def test_remove_user_role(self): """ Remove all roles on a user from our project """ project = mock.Mock() diff --git a/stacktask/api/v1/tests/test_api_taskview.py b/stacktask/api/v1/tests/test_api_taskview.py index 90af94e..5a95bce 100644 --- a/stacktask/api/v1/tests/test_api_taskview.py +++ b/stacktask/api/v1/tests/test_api_taskview.py @@ -21,6 +21,8 @@ from stacktask.api.models import Task, Token from stacktask.api.v1.tests import FakeManager, setup_temp_cache +@mock.patch('stacktask.actions.user_store.IdentityManager', + FakeManager) class TaskViewTests(APITestCase): """ Tests to ensure the approval/token workflow does what is @@ -65,8 +67,6 @@ class TaskViewTests(APITestCase): 'email': ['Enter a valid email address.'], 'roles': ['"not_a_valid_role" is not a valid choice.']}) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user(self): """ Ensure the new user workflow goes as expected. @@ -101,8 +101,6 @@ class TaskViewTests(APITestCase): response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user_no_project(self): """ Can't create a user for a non-existent project. @@ -124,8 +122,6 @@ class TaskViewTests(APITestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data, {'errors': ['actions invalid']}) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user_not_my_project(self): """ Can't create a user for project that isn't mine. @@ -146,8 +142,6 @@ class TaskViewTests(APITestCase): response = self.client.post(url, data, format='json', headers=headers) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_new_user_not_authenticated(self): """ Can't create a user if unauthenticated. @@ -166,8 +160,6 @@ class TaskViewTests(APITestCase): {'errors': ["Credentials incorrect or none given."]} ) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_add_user_existing(self): """ Adding existing user to project. @@ -207,8 +199,6 @@ class TaskViewTests(APITestCase): response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_add_user_existing_with_role(self): """ Adding existing user to project. @@ -246,11 +236,6 @@ class TaskViewTests(APITestCase): response.data, {'notes': ['Task completed successfully.']}) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_new_project(self): """ Ensure the new project workflow goes as expected. @@ -287,12 +272,6 @@ class TaskViewTests(APITestCase): response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_new_project_existing(self): """ Test to ensure validation marks actions as invalid @@ -330,12 +309,6 @@ class TaskViewTests(APITestCase): {'errors': ['Cannot approve an invalid task. ' + 'Update data and rerun pre_approve.']}) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_new_project_existing_user(self): """ Project created if not present, existing user attached. @@ -378,12 +351,6 @@ class TaskViewTests(APITestCase): {'notes': ['Task completed successfully.']} ) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_new_project_existing_project_new_user(self): """ Project already exists but new user attempting to create it. @@ -431,8 +398,6 @@ class TaskViewTests(APITestCase): {'errors': ['actions invalid']} ) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_reset_user(self): """ Ensure the reset user workflow goes as expected. @@ -463,8 +428,6 @@ class TaskViewTests(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(user.password, 'new_test_password') - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_reset_user_duplicate(self): """ Request password reset twice in a row @@ -513,8 +476,6 @@ class TaskViewTests(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(user.password, 'new_test_password2') - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_reset_user_no_existing(self): """ Actions should be successful, so usernames are not exposed. @@ -530,11 +491,6 @@ class TaskViewTests(APITestCase): response.data['notes'], ['If user with email exists, reset token will be issued.']) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_notification_createproject(self): """ CreateProject should create a notification. @@ -565,11 +521,6 @@ class TaskViewTests(APITestCase): response.data['notifications'][0]['task'], new_task.uuid) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) - @mock.patch( - 'stacktask.actions.tenant_setup.models.user_store.IdentityManager', - FakeManager) def test_duplicate_tasks_new_project(self): """ Ensure we can't submit duplicate tasks @@ -594,8 +545,6 @@ class TaskViewTests(APITestCase): response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - @mock.patch( - 'stacktask.actions.models.user_store.IdentityManager', FakeManager) def test_duplicate_tasks_new_user(self): """ Ensure we can't submit duplicate tasks @@ -633,8 +582,6 @@ class TaskViewTests(APITestCase): response = self.client.post(url, data, format='json', headers=headers) self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_return_task_id_if_admin(self): """ Confirm that the task id is returned when admin. @@ -667,8 +614,6 @@ class TaskViewTests(APITestCase): response.data['task'], new_task.uuid) - @mock.patch('stacktask.actions.models.user_store.IdentityManager', - FakeManager) def test_return_task_id_if_admin_fail(self): """ Confirm that the task id is not returned unless admin. diff --git a/stacktask/settings.py b/stacktask/settings.py index 86c0d1c..44d63fd 100644 --- a/stacktask/settings.py +++ b/stacktask/settings.py @@ -124,6 +124,10 @@ for app in CONFIG['ADDITIONAL_APPS']: INSTALLED_APPS = list(INSTALLED_APPS) INSTALLED_APPS.append(app) +# NOTE(adriant): Because the order matters, we want this import to be last +# so the startup checks run after everything is imported. +INSTALLED_APPS.append("stacktask.startup") + DATABASES = CONFIG['DATABASES'] LOGGING = CONFIG['LOGGING'] diff --git a/stacktask/startup/__init__.py b/stacktask/startup/__init__.py new file mode 100644 index 0000000..71b9be3 --- /dev/null +++ b/stacktask/startup/__init__.py @@ -0,0 +1 @@ +default_app_config = 'stacktask.startup.checks.StartUpConfig' diff --git a/stacktask/api/startup.py b/stacktask/startup/checks.py similarity index 93% rename from stacktask/api/startup.py rename to stacktask/startup/checks.py index 77a317d..4f4d757 100644 --- a/stacktask/api/startup.py +++ b/stacktask/startup/checks.py @@ -1,5 +1,6 @@ from django.apps import AppConfig from django.conf import settings + from stacktask.exceptions import ActionNotFound, TaskViewNotFound @@ -38,17 +39,16 @@ def check_configured_actions(): "Configured actions are unregistered: %s" % missing_actions) -class APIConfig(AppConfig): - name = 'stacktask.api' +class StartUpConfig(AppConfig): + name = "stacktask.startup" def ready(self): - """A pre-startup function for the api. + """A pre-startup function for the api Code run here will occur before the API is up and active but after all models have been loaded. Useful for any start up checks. - """ # First check that all expect taskviews are present diff --git a/stacktask/actions/tenant_setup/__init__.py b/stacktask/startup/models.py similarity index 100% rename from stacktask/actions/tenant_setup/__init__.py rename to stacktask/startup/models.py diff --git a/stacktask/test_settings.py b/stacktask/test_settings.py index 014f129..13fabe9 100644 --- a/stacktask/test_settings.py +++ b/stacktask/test_settings.py @@ -12,11 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. -SECRET_KEY = '+er!!4olta#17a=n%uotcazg2ncpl==yjog%1*o-(cr%zys-)!' +SECRET_KEY = '+er!4olta#17a=n%uotcazg2ncpl==yjog%1*o-(cr%zys-)!' ADDITIONAL_APPS = [ 'stacktask.api.v1', - 'stacktask.actions.tenant_setup' + 'stacktask.actions.v1', ] DATABASES = {