def save(self, endpoint, request): dhcp_snippet = super().save() create_audit_event( EVENT_TYPES.SETTINGS, endpoint, request, None, description=( "%s DHCP snippet '%s'." % ( "Updated" if self.is_update else "Created", dhcp_snippet.name, ) ), ) return dhcp_snippet
def delete(self, request, system_id, id): """Delete bcache on a machine. Returns 404 if the machine or bcache is not found. Returns 409 if the machine is not Ready. """ bcache = Bcache.objects.get_object_or_404(system_id, id, request.user, NODE_PERMISSION.ADMIN) node = bcache.get_node() if node.status != NODE_STATUS.READY: raise NodeStateViolation( "Cannot delete Bcache because the machine is not Ready.") bcache.delete() create_audit_event(EVENT_TYPES.NODE, ENDPOINT.API, request, system_id, "Deleted bcache.") return rc.DELETED
def update(self, request, system_id, id): """@description-title Update a bcache set @description Update bcache cache set on a machine. Note: specifying both a cache_device and a cache_partition is not allowed. @param (string) "{system_id}" [required=true] A machine system_id. @param (string) "{id}" [required=true] A cache_set_id. @param (string) "cache_device" [required=false] Cache block device to replace current one. @param (string) "cache_partition" [required=false] Cache partition to replace current one. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing a bcache set. @success-example "success-json" [exkey=bcache-placeholder] placeholder text @error (http-status-code) "404" 404 @error (content) "not-found" The requested machine is not found. @error-example "not-found" Not Found @error (http-status-code) "409" 409 @error (content) "not-ready" The requested machine is not ready. """ cache_set = CacheSet.objects.get_cache_set_or_404( system_id, id, request.user, NodePermission.admin) node = cache_set.get_node() if node.status != NODE_STATUS.READY: raise NodeStateViolation( "Cannot update cache set because the machine is not Ready.") form = UpdateCacheSetForm(cache_set, data=request.data) if form.is_valid(): create_audit_event( EVENT_TYPES.NODE, ENDPOINT.API, request, system_id, "Updated bcache cache set.", ) return form.save() else: raise MAASAPIValidationError(form.errors)
def create(self, request): """Create a MAAS user account. This is not safe: the password is sent in plaintext. Avoid it for production, unless you are confident that you can prevent eavesdroppers from observing the request. :param username: Identifier-style username for the new user. :type username: unicode :param email: Email address for the new user. :type email: unicode :param password: Password for the new user. :type password: unicode :param is_superuser: Whether the new user is to be an administrator. :type is_superuser: bool ('0' for False, '1' for True) Returns 400 if any mandatory parameters are missing. """ username = get_mandatory_param(request.data, 'username') email = get_mandatory_param(request.data, 'email') if request.external_auth_info: password = request.data.get('password') else: password = get_mandatory_param(request.data, 'password') is_superuser = extract_bool( get_mandatory_param(request.data, 'is_superuser')) create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.API, request, None, description=("Created %s '%s'." % ('admin' if is_superuser else 'user', username))) if is_superuser: user = User.objects.create_superuser(username=username, password=password, email=email) if request.data.get('key') is not None: request.user = user sshkeys_handler = SSHKeysHandler() sshkeys_handler.create(request) return user else: return User.objects.create_user(username=username, password=password, email=email)
def revert(self, request, name): """@description-title Revert a script version @description Revert a script with the given name to an earlier version. @param (string) "{name}" [required=true] The name of the script. @param (int) "to" [required=false] What revision in the script's history to revert to. This can either be an ID or a negative number representing how far back to go. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing information about the reverted script. @success-example "success-json" [exkey=scripts-revert] placeholder text @error (http-status-code) "404" 404 @error (content) "not-found" The requested script is not found. @error-example "not-found" Not Found """ revert_to = get_mandatory_param(request.data, "to", Int) if name.isdigit(): script = get_object_or_404(Script, id=int(name)) else: script = get_object_or_404(Script, name=name) try: if script.default: raise MAASAPIValidationError("Unable to revert default script") def gc_hook(value): script.script = value script.save() script.script.revert(revert_to, gc_hook=gc_hook) create_audit_event( EVENT_TYPES.SETTINGS, ENDPOINT.API, request, None, description=("Reverted script '%s' to revision '%s'." % (script.name, revert_to)), ) return script except ValueError as e: raise MAASAPIValidationError(e.args[0])
def test_create_audit_event_creates_audit_event_with_user_agent(self): node = factory.make_Node() request = HttpRequest() request.user = node.owner request.META = { 'HTTP_USER_AGENT': factory.make_name('user_agent'), 'HTTP_HOST': factory.make_ipv4_address(), } endpoint = factory.pick_choice(ENDPOINT_CHOICES) create_audit_event(EVENT_TYPES.NODE_PXE_REQUEST, endpoint, request, system_id=node.system_id) event = Event.objects.get(node=node, user=node.owner) self.assertIsNotNone(event) self.assertIsNotNone(EventType.objects.get(level=AUDIT)) self.assertEquals(request.META['HTTP_USER_AGENT'], event.user_agent)
def delete(self, request, name): """Delete a script.""" if name.isdigit(): script = get_object_or_404(Script, id=int(name)) else: script = get_object_or_404(Script, name=name) if script.default: raise MAASAPIValidationError("Unable to delete default script") script.delete() create_audit_event(EVENT_TYPES.SETTINGS, ENDPOINT.API, request, None, description="Deleted script '%s'." % script.name) return rc.DELETED
def import_ssh_keys(self, request): """@description-title Import SSH keys @description Import the requesting user's SSH keys for a given protocol and authorization ID in protocol:auth_id format. @param (string) "keysource" [required=true,formatting=true] The source of the keys to import should be provided in the request payload as form data: E.g. source:user - ``source``: lp (Launchpad), gh (GitHub) - ``user``: User login @success (http-status-code) "200" 200 @success (json) "success-json" A JSON object containing a list of imported keys. @success-example "success-json" [exkey=ssh-keys-import] placeholder text """ keysource = request.data.get("keysource", None) if keysource is not None: if ":" in keysource: protocol, auth_id = keysource.split(":", 1) else: protocol = KEYS_PROTOCOL_TYPE.LP auth_id = keysource try: keysource = KeySource.objects.save_keys_for_user( user=request.user, protocol=protocol, auth_id=auth_id) create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.API, request, None, description="Imported SSH keys.", ) return keysource except (ImportSSHKeysError, RequestException) as e: raise MAASAPIBadRequest(e.args[0]) else: raise MAASAPIBadRequest( "Importing SSH keys failed. " "Input needs to be in protocol:auth_id or auth_id format.")
def delete(self, request, username): """@description-title Delete a user @description Deletes a given username. @param (string) "{username}" [required=true] The username to delete. @param (string) "transfer_resources_to" [required=false] An optional username. If supplied, the allocated resources of the user being deleted will be transferred to this user. A user can't be removed unless its resources (machines, IP addresses, ...), are released or transfered to another user. @success (http-status-code) "204" 204 """ if request.user.username == username: raise ValidationError("An administrator cannot self-delete.") form = DeleteUserForm(data=request.GET) if not form.is_valid(): raise MAASAPIValidationError(form.errors) user = get_one(User.objects.filter(username=username)) if user is not None: new_owner_username = form.cleaned_data["transfer_resources_to"] if new_owner_username: new_owner = get_object_or_404(User, username=new_owner_username) if new_owner is not None: user.userprofile.transfer_resources(new_owner) Consumer.objects.filter(user=user).delete() try: user.userprofile.delete() create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.API, request, None, description=( "Deleted %s '%s'." % ("admin" if user.is_superuser else "user", username)), ) except CannotDeleteUserException as e: raise ValidationError(str(e)) return rc.DELETED
def execute(self, *args, **kwargs): """Perform this action. Even though this is not the API, the action may raise :class:`MAASAPIException` exceptions. When this happens, the view will return to the client an http response reflecting the exception. """ self._execute(*args, **kwargs) description = self.get_node_action_audit_description(self) # Log audit event for the action. create_audit_event( EVENT_TYPES.NODE, self.endpoint, self.request, self.node.system_id, description=description, )
def delete(self, request, *args, **kwargs): profile = self.get_object() username = profile.user.username try: create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.UI, request, None, description=("%s %s" % ('Admin' if profile.user.is_superuser else 'User', username) + " deleted by '%(username)s'.")) profile.delete() messages.info(request, "User %s deleted." % username) except CannotDeleteUserException as e: messages.info(request, str(e)) return HttpResponseRedirect(self.get_next_url())
def logout(request): if request.method == 'POST': form = LogoutForm(request.POST) if form.is_valid(): create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.UI, request, None, description=( "%s" % ('Admin' if request.user.is_superuser else 'User') + " '%(username)s' logged out.")) return dj_logout(request, next_page=reverse('login')) else: form = LogoutForm() return render(request, 'maasserver/logout_confirm.html', {'form': form})
def revert(self, request, id): """@description-title Revert DHCP snippet to earlier version @description Revert the value of a DHCP snippet with the given id to an earlier revision. @param (int) "{id}" [required=true] A DHCP snippet id. @param (int) "to" [required=true] What revision in the DHCP snippet's history to revert to. This can either be an ID or a negative number representing how far back to go. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing information about the reverted DHCP snippet. @success-example "success-json" [exkey=dhcp-snippets-revert] placeholder text @error (http-status-code) "404" 404 @error (content) "not-found" The requested DHCP snippet is not found. @error-example "not-found" Not Found """ revert_to = request.data.get('to') if revert_to is None: raise MAASAPIValidationError('You must specify where to revert to') try: revert_to = int(revert_to) except ValueError: raise MAASAPIValidationError( "%s is an invalid 'to' value" % revert_to) dhcp_snippet = DHCPSnippet.objects.get_dhcp_snippet_or_404(id) try: def gc_hook(value): dhcp_snippet.value = value dhcp_snippet.save() dhcp_snippet.value.revert(revert_to, gc_hook=gc_hook) create_audit_event( EVENT_TYPES.SETTINGS, ENDPOINT.API, request, None, description=( "Reverted DHCP snippet '%s' to revision '%s'." % ( dhcp_snippet.name, revert_to))) return dhcp_snippet except ValueError as e: raise MAASAPIValidationError(e.args[0])
def import_keys(self, params): """Import the requesting user's SSH keys. Import SSH keys for a given protocol and authorization ID in protocol:auth_id format. """ try: KeySource.objects.save_keys_for_user( user=self.user, protocol=params['protocol'], auth_id=params['auth_id']) request = HttpRequest() request.user = self.user create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.UI, request, None, description="Imported SSH keys.") except ImportSSHKeysError as e: raise HandlerError(str(e))
def logout(request): if request.method == "POST": form = LogoutForm(request.POST) if form.is_valid(): create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.UI, request, None, description=( "Logged out %s." % ("admin" if request.user.is_superuser else "user")), ) auth_logout(request) return HttpResponse(status=204) return HttpResponseBadRequest(json.dumps(form.errors), content_type="application/json") return HttpResponseNotAllowed([request.method])
def delete(self, request, id): """@description-title Delete a DHCP snippet @description Delete a DHCP snippet with the given id. @param (int) "{id}" [required=true] A DHCP snippet id. @success (http-status-code) "server-success" 204 @error (http-status-code) "404" 404 @error (content) "not-found" The requested DHCP snippet is not found. @error-example "not-found" Not Found """ dhcp_snippet = DHCPSnippet.objects.get_dhcp_snippet_or_404(id) dhcp_snippet.delete() create_audit_event( EVENT_TYPES.SETTINGS, ENDPOINT.API, request, None, description=( "Deleted DHCP snippet '%s'." % dhcp_snippet.name)) return rc.DELETED
def logout(request): if request.method == "POST": form = LogoutForm(request.POST) if form.is_valid(): create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.UI, request, None, description=( "Logged out %s." % ("admin" if request.user.is_superuser else "user") ), ) return dj_logout(request, next_page=reverse("login")) else: form = LogoutForm() return render(request, "maasserver/logout_confirm.html", {"form": form})
def delete(self, request, id): """DELETE an SSH key. Returns 404 if the key does not exist. Returns 401 if the key does not belong to the calling user. """ key = get_object_or_404(SSHKey, id=id) if key.user != request.user: return HttpResponseForbidden( "Can't delete a key you don't own.", content_type=("text/plain; charset=%s" % settings.DEFAULT_CHARSET)) key.delete() create_audit_event(EVENT_TYPES.AUTHORISATION, ENDPOINT.API, request, None, description="Deleted SSH key id='%s'." % id) return rc.DELETED
def delete(self, request, id): """DELETE an SSL key. Returns 401 if the key does not belong to the requesting user. Returns 204 if the key is successfully deleted. """ key = get_object_or_404(SSLKey, id=id) if key.user != request.user: return HttpResponseForbidden( "Can't delete a key you don't own.", content_type=("text/plain; charset=%s" % settings.DEFAULT_CHARSET)) key.delete() create_audit_event(EVENT_TYPES.AUTHORISATION, ENDPOINT.API, request, None, description=("SSL key id=%s" % id + " deleted by '%(username)s'.")) return rc.DELETED
def update(self, params): """Update a Token. Only the name can be updated. """ token = self.get_object(params) name = params.get("name") if name != token.consumer.name: token.consumer.name = params.get("name") token.consumer.save() request = HttpRequest() request.user = self.user create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.UI, request, None, "Modified consumer name of token.", ) return self.full_dehydrate(token)
def create_authorisation_token(self, params): """Create an authorisation token for the user. This is only for the authenticated user. This cannot be performed on a different user. """ request = HttpRequest() request.user = self.user profile = self.user.userprofile consumer, token = profile.create_authorisation_token() create_audit_event(EVENT_TYPES.AUTHORISATION, ENDPOINT.UI, request, None, "Created token.") return { 'key': token.key, 'secret': token.secret, 'consumer': { 'key': consumer.key, 'name': consumer.name, }, }
def login(request): if request.method == "POST": if request.user.is_authenticated: return HttpResponse(status=204) form = AuthenticationForm(data=request.POST, request=request) if form.is_valid(): auth_login(request, form.get_user()) create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.UI, request, None, description=( "Logged in %s." % ("admin" if request.user.is_superuser else "user")), ) return HttpResponse(status=204) return HttpResponseBadRequest(json.dumps(form.errors), content_type="application/json") raise Http404()
def post(self, request, *args, **kwargs): """Called by `TemplateView`: handle a POST request.""" self.object = user = self.get_object() next_page = reverse('settings') # Process the profile-editing form, if that's what was submitted. profile_form, response = process_form(request, EditUserForm, next_page, 'profile', "Profile updated.", {'instance': user}) if response is not None: create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.UI, request, None, description=( "User profile (username: %s, full name: %s, email: %s, " "administrator: %s)" % (profile_form.cleaned_data['username'], profile_form.cleaned_data['last_name'], profile_form.cleaned_data['email'], profile_form.cleaned_data['is_superuser']) + " updated by '%(username)s'.")) return response # Process the password change form, if that's what was submitted. password_form, response = process_form(request, AdminPasswordChangeForm, next_page, 'password', "Password updated.", {'user': user}) if response is not None: create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.UI, request, None, description="Password changed for '%(username)s'.") return response return self.respond(request, profile_form, password_form)
def update_token_name(self, request): """Modify the consumer name of an authorisation OAuth token. :param token: Can be the whole token or only the token key. :type token: unicode :param name: New name of the token. :type name: unicode """ profile = request.user.userprofile token = get_mandatory_param(request.data, 'token') token_fields = token.split(":") if len(token_fields) == 3: token_key = token_fields[1] else: token_key = token consumer_name = get_mandatory_param(request.data, 'name') profile.modify_consumer_name(token_key, consumer_name) create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.API, request, None, "Modified consumer name of token.") return rc.ACCEPTED
def delete_authorisation_token(self, request): """@description-title Delete an authorisation token @description Delete an authorisation OAuth token and the related OAuth consumer. @param (string) "token_key" [required=true] The key of the token to be deleted. @success (http-status-code) "204" 204 """ profile = request.user.userprofile token_key = get_mandatory_param(request.data, "token_key") profile.delete_authorisation_token(token_key) create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.API, request, None, "Deleted token.", ) return rc.DELETED
def delete(self, request, system_id, id): """Delete bcache cache set on a machine. Returns 400 if the cache set is in use. Returns 404 if the machine or cache set is not found. Returns 409 if the machine is not Ready. """ cache_set = CacheSet.objects.get_cache_set_or_404( system_id, id, request.user, NODE_PERMISSION.ADMIN) node = cache_set.get_node() if node.status != NODE_STATUS.READY: raise NodeStateViolation( "Cannot delete cache set because the machine is not Ready.") if cache_set.filesystemgroup_set.exists(): raise MAASAPIBadRequest( "Cannot delete cache set; it's currently in use.") else: cache_set.delete() create_audit_event(EVENT_TYPES.NODE, ENDPOINT.API, request, system_id, "Deleted bcache cache set.") return rc.DELETED
def test_create_audit_event_creates_audit_event_with_description(self): node = factory.make_Node() request = HttpRequest() request.user = node.owner request.META = { "HTTP_USER_AGENT": factory.make_name("user_agent"), "HTTP_HOST": factory.make_ipv4_address(), } endpoint = factory.pick_choice(ENDPOINT_CHOICES) description = factory.make_name("description") create_audit_event( EVENT_TYPES.NODE_PXE_REQUEST, endpoint, request, system_id=node.system_id, description=description, ) event = Event.objects.get(node=node, type__level=AUDIT) self.assertIsNotNone(event) self.assertEqual(request.META["HTTP_USER_AGENT"], event.user_agent) self.assertEqual(description, event.description)
def add_tag(self, request, name): """@description-title Add a tag @description Add a single tag to a script with the given name. @param (string) "{name}" [required=true] The name of the script. @param (string) "tag" [required=false] The tag being added. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing information about the updated script. @success-example "success-json" [exkey=scripts-add-tag] placeholder text @error (http-status-code) "404" 404 @error (content) "not-found" The requested script is not found. @error-example "not-found" Not Found """ tag = get_mandatory_param(request.data, "tag", String) if "," in tag: raise MAASAPIValidationError('Tag may not contain a ",".') if name.isdigit(): script = get_object_or_404(Script, id=int(name)) else: script = get_object_or_404(Script, name=name) script.add_tag(tag) script.save() create_audit_event( EVENT_TYPES.SETTINGS, ENDPOINT.API, request, None, description=("Added tag '%s' to script '%s'." % (tag, script.name)), ) return script
def delete(self, request, username): """@description-title Delete a user @description Deletes a given username. @param (string) "{username}" [required=true] The username to delete. @success (http-status-code) "204" 204 """ if request.user.username == username: raise ValidationError("An administrator cannot self-delete.") form = DeleteUserForm(data=request.GET) if not form.is_valid(): raise MAASAPIValidationError(form.errors) user = get_one(User.objects.filter(username=username)) if user is not None: new_owner_username = form.cleaned_data['transfer_resources_to'] if new_owner_username: new_owner = get_object_or_404(User, username=new_owner_username) if new_owner is not None: user.userprofile.transfer_resources(new_owner) Consumer.objects.filter(user=user).delete() try: user.userprofile.delete() create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.API, request, None, description=( "Deleted %s '%s'." % ('admin' if user.is_superuser else 'user', username))) except CannotDeleteUserException as e: raise ValidationError(str(e)) return rc.DELETED
def delete(self, request, system_id, id): """@description-title Delete a bcache set @description Delete bcache cache set on a machine. @param (string) "{system_id}" [required=true] A machine system_id. @param (string) "{id}" [required=true] A cache_set_id. @success (http-status-code) "server-success" 204 @error (http-status-code) "400" 400 @error (content) "not-ready" The cache set is in use. @error (http-status-code) "404" 404 @error (content) "not-found" The requested machine is not found. @error-example "not-found" Not Found @error (http-status-code) "409" 409 @error (content) "not-ready" The requested machine is not ready. """ cache_set = CacheSet.objects.get_cache_set_or_404( system_id, id, request.user, NodePermission.admin) node = cache_set.get_node() if node.status != NODE_STATUS.READY: raise NodeStateViolation( "Cannot delete cache set because the machine is not Ready.") if cache_set.filesystemgroup_set.exists(): raise MAASAPIBadRequest( "Cannot delete cache set; it's currently in use.") else: cache_set.delete() create_audit_event( EVENT_TYPES.NODE, ENDPOINT.API, request, system_id, "Deleted bcache cache set.", ) return rc.DELETED