diff --git a/server/src/uds/REST/methods/accounts.py b/server/src/uds/REST/methods/accounts.py new file mode 100644 index 000000000..80e5ca1fc --- /dev/null +++ b/server/src/uds/REST/methods/accounts.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@itemor: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _, ugettext +from uds.models import Account, AccountUsage +from uds.core.util import permissions + +from uds.REST.model import ModelHandler + + +import logging + +logger = logging.getLogger(__name__) + +# Enclosed methods under /item path + + +class Accounts(ModelHandler): + ''' + Processes REST requests about calendars + ''' + model = Account + detail = {'usage': AccountUsage} + + save_fields = ['name', 'comments', 'tags'] + + table_title = _('Accounts') + table_fields = [ + {'name': {'title': _('Name'), 'visible': True, 'type': 'iconType'}}, + {'comments': {'title': _('Comments')}}, + {'tags': {'title': _('tags'), 'visible': False}}, + ] + + def item_as_dict(self, calendar): + return { + 'id': calendar.uuid, + 'name': calendar.name, + 'tags': [tag.tag for tag in calendar.tags.all()], + 'comments': calendar.comments, + 'permission': permissions.getEffectivePermission(self._user, calendar) + } + + def getGui(self, type_): + return self.addDefaultFields([], ['name', 'comments', 'tags']) diff --git a/server/src/uds/REST/methods/services_pools.py b/server/src/uds/REST/methods/services_pools.py index 4954359b9..0116e7223 100644 --- a/server/src/uds/REST/methods/services_pools.py +++ b/server/src/uds/REST/methods/services_pools.py @@ -33,7 +33,7 @@ from __future__ import unicode_literals from django.utils.translation import ugettext, ugettext_lazy as _ -from uds.models import DeployedService, OSManager, Service, Image, ServicesPoolGroup +from uds.models import DeployedService, OSManager, Service, Image, ServicesPoolGroup, Account from uds.models.CalendarAction import CALENDAR_ACTION_INITIAL, CALENDAR_ACTION_MAX, CALENDAR_ACTION_CACHE_L1, CALENDAR_ACTION_CACHE_L2, CALENDAR_ACTION_PUBLISH from uds.core.ui.images import DEFAULT_THUMB_BASE64 from uds.core.util.State import State @@ -69,7 +69,21 @@ class ServicesPools(ModelHandler): 'actions': ActionsCalendars } - save_fields = ['name', 'comments', 'tags', 'service_id', 'osmanager_id', 'image_id', 'servicesPoolGroup_id', 'initial_srvs', 'cache_l1_srvs', 'cache_l2_srvs', 'max_srvs', 'show_transports'] + save_fields = [ + 'name', + 'comments', + 'tags', + 'service_id', + 'osmanager_id', + 'image_id', + 'account_id', + 'servicesPoolGroup_id', + 'initial_srvs', + 'cache_l1_srvs', + 'cache_l2_srvs', + 'max_srvs', + 'show_transports' + ] remove_fields = ['osmanager_id', 'service_id'] table_title = _('Service Pools') @@ -116,10 +130,12 @@ class ServicesPools(ModelHandler): 'comments': item.comments, 'state': state, 'thumb': item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64, + 'account': item.account.name if item.account is not None else '', 'service_id': item.service.uuid, 'provider_id': item.service.provider.uuid, 'image_id': item.image.uuid if item.image is not None else None, 'servicesPoolGroup_id': poolGroupId, + 'account_id': item.account.uuid if item.account is not None else None, 'pool_group_name': poolGroupName, 'pool_group_thumb': poolGroupThumb, 'initial_srvs': item.initial_srvs, @@ -165,13 +181,20 @@ class ServicesPools(ModelHandler): 'type': gui.InputField.CHOICE_TYPE, 'rdonly': True, 'order': 101, + }, { + 'name': 'account_id', + 'values': [gui.choiceItem(-1, '')] + gui.sortedChoices([gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()]), + 'label': ugettext('Account'), + 'tooltip': ugettext('Account associated to this service pool'), + 'type': gui.InputField.CHOICE_TYPE, + 'order': 102, }, { 'name': 'image_id', 'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in Image.objects.all()]), 'label': ugettext('Associated Image'), 'tooltip': ugettext('Image assocciated with this service'), 'type': gui.InputField.IMAGECHOICE_TYPE, - 'order': 102, + 'order': 105, 'tab': ugettext('Display'), }, { 'name': 'servicesPoolGroup_id', @@ -179,7 +202,7 @@ class ServicesPools(ModelHandler): 'label': ugettext('Pool group'), 'tooltip': ugettext('Pool group for this pool (for pool clasify on display)'), 'type': gui.InputField.IMAGECHOICE_TYPE, - 'order': 103, + 'order': 106, 'tab': ugettext('Display'), }, { 'name': 'initial_srvs', @@ -270,6 +293,18 @@ class ServicesPools(ModelHandler): # If max < initial or cache_1 or cache_l2 fields['max_srvs'] = max((int(fields['initial_srvs']), int(fields['cache_l1_srvs']), int(fields['max_srvs']))) + + accountId = fields['account_id'] + fields['account_id'] = None + logger.debug('Account id: {}'.format(accountId)) + + if accountId != '-1': + try: + fields['account_id'] = Account.objects.get(uuid=processUuid(accountId)).id + except Exception: + logger.exception('Getting account ID') + + imgId = fields['image_id'] fields['image_id'] = None logger.debug('Image id: {}'.format(imgId)) diff --git a/server/src/uds/migrations/0024_auto_20161010_0753.py b/server/src/uds/migrations/0024_auto_20170117_0845.py similarity index 95% rename from server/src/uds/migrations/0024_auto_20161010_0753.py rename to server/src/uds/migrations/0024_auto_20170117_0845.py index a736338c5..03cb627a2 100644 --- a/server/src/uds/migrations/0024_auto_20161010_0753.py +++ b/server/src/uds/migrations/0024_auto_20170117_0845.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2016-10-10 07:53 +# Generated by Django 1.10.5 on 2017-01-17 08:45 from __future__ import unicode_literals import datetime @@ -21,6 +21,7 @@ class Migration(migrations.Migration): ('uuid', models.CharField(default=None, max_length=50, null=True, unique=True)), ('name', models.CharField(db_index=True, max_length=128)), ('comments', models.CharField(max_length=256)), + ('tags', models.ManyToManyField(to='uds.Tag')), ], options={ 'db_table': 'uds_accounts', diff --git a/server/src/uds/models/Account.py b/server/src/uds/models/Account.py index 7f0003026..642ffedbb 100644 --- a/server/src/uds/models/Account.py +++ b/server/src/uds/models/Account.py @@ -31,11 +31,12 @@ from __future__ import unicode_literals -__updated__ = '2016-09-21' +__updated__ = '2017-01-17' from django.db import models from uds.models.UUIDModel import UUIDModel +from uds.models.Tag import TaggingMixin from uds.models.Util import getSqlDatetime from django.db.models import signals @@ -44,11 +45,9 @@ import logging logger = logging.getLogger(__name__) -class Account(UUIDModel): +class Account(UUIDModel, TaggingMixin): ''' Account storing on DB model - This is intended for small images (i will limit them to 128x128), so storing at db is fine - ''' name = models.CharField(max_length=128, unique=False, db_index=True) comments = models.CharField(max_length=256) diff --git a/server/src/uds/models/UserService.py b/server/src/uds/models/UserService.py index 93a4beb13..6356819ab 100644 --- a/server/src/uds/models/UserService.py +++ b/server/src/uds/models/UserService.py @@ -57,7 +57,7 @@ import six import pickle import logging -__updated__ = '2017-01-12' +__updated__ = '2017-01-17' logger = logging.getLogger(__name__) @@ -368,7 +368,7 @@ class UserService(UUIDModel): # 1.- If do not have any account associated, do nothing # 2.- If called but already accounting, do nothing # 3.- If called and not accounting, start accounting - if self.deployed_service.account is None or hasattr(self, 'accounting'): + if self.deployed_service.account is None or hasattr(self, 'accounting'): # accounting comes from AccountUsage, and is a OneToOneRelation with UserService return self.deployed_service.account.startUsageAccounting(self) diff --git a/server/src/uds/static/adm/js/api.coffee b/server/src/uds/static/adm/js/api.coffee index 4225694fb..ff002039b 100644 --- a/server/src/uds/static/adm/js/api.coffee +++ b/server/src/uds/static/adm/js/api.coffee @@ -444,6 +444,7 @@ api.sPoolGroups = new BasicModelRest("gallery/servicespoolgroups") api.system = new BasicModelRest("system") api.reports = new BasicModelRest("reports") # Not fully used, but basic usage is common api.calendars = new BasicModelRest("calendars") +api.accounts = new BasicModelRest("accounts") # In fact, reports do not have any type api.reports.types = (success_fnc, fail_fnc) -> diff --git a/server/src/uds/static/adm/js/gui-d-account.coffee b/server/src/uds/static/adm/js/gui-d-account.coffee new file mode 100644 index 000000000..ecfe1b1c4 --- /dev/null +++ b/server/src/uds/static/adm/js/gui-d-account.coffee @@ -0,0 +1,439 @@ +# jshint strict: true +gui.accounts = new GuiElement(api.accounts, "accounts") +gui.accounts.link = (event) -> + "use strict" + + # Button definition to trigger "Test" action + testButton = testButton: + text: gettext("Test") + css: "btn-info" + + + # Clears the log of the detail, in this case, the log of "users" + # Memory saver :-) + detailLogTable = null + clearDetailLog = -> + if detailLogTable? + $tbl = $(detailLogTable).dataTable() + $tbl.fnClearTable() + $tbl.fnDestroy() + detailLogTable = null + $("#users-log-placeholder").empty() + return + + + # Clears the details + # Memory saver :-) + prevTables = [] + clearDetails = -> + clearDetailLog() + + $.each prevTables, (undefined_, tbl) -> + $tbl = $(tbl).dataTable() + $tbl.fnClearTable() + $tbl.fnDestroy() + return + + $("#users-placeholder").empty() + $("#groups-placeholder").empty() + $("#logs-placeholder").empty() + $("#detail-placeholder").addClass "hidden" + prevTables = [] + return + + + # Search button event generator for user/group + searchForm = (parentModalId, type, id, title, searchLabel, resultsLabel) -> + errorModal = gui.failRequestModalFnc(gettext("Search error")) + srcSelector = parentModalId + " input[name=\"name\"]" + $(parentModalId + " .button-search").on "click", -> + api.templates.get "search", (tmpl) -> # Get form template + modalId = gui.launchModal(title, api.templates.evaluate(tmpl, + search_label: searchLabel + results_label: resultsLabel + ), + actionButton: "" + ) + $searchInput = $(modalId + " input[name=\"search\"]") + $select = $(modalId + " select[name=\"results\"]") + $searchButton = $(modalId + " .button-do-search") + $saveButton = $(modalId + " .button-accept") + $searchInput.val $(srcSelector).val() + $saveButton.on "click", -> + value = $select.val() + if value + $(srcSelector).val value + $(modalId).modal "hide" + return + + $searchButton.on "click", -> + $searchButton.addClass "disabled" + term = $searchInput.val() + api.accounts.search id, type, term, ((data) -> + $searchButton.removeClass "disabled" + $select.empty() + gui.doLog data + $.each data, (undefined_, value) -> + $select.append "" + return + + return + ), (jqXHR, textStatus, errorThrown) -> + $searchButton.removeClass "disabled" + errorModal jqXHR, textStatus, errorThrown + return + + return + + $(modalId + " form").submit (event) -> + event.preventDefault() + $searchButton.click() + return + + $searchButton.click() if $searchInput.val() isnt "" + return + + return + + return + + api.templates.get "accounts", (tmpl) -> + gui.clearWorkspace() + gui.appendToWorkspace api.templates.evaluate(tmpl, + auths: "auths-placeholder" + auths_info: "auths-info-placeholder" + users: "users-placeholder" + users_log: "users-log-placeholder" + groups: "groups-placeholder" + logs: "logs-placeholder" + ) + gui.setLinksEvents() + + # Append tabs click events + $(".bottom_tabs").on "click", (event) -> + gui.doLog event.target + setTimeout (-> + $($(event.target).attr("href") + " span.fa-refresh").click() + return + ), 10 + return + + tableId = gui.accounts.table( + icon: 'accounts' + container: "auths-placeholder" + rowSelect: "multi" + buttons: [ + "new" + "edit" + "delete" + "xls" + "permissions" + ] + + onFoundUuid: (item) -> + # Invoked if our table has found a "desirable" item (uuid) + if gui.lookup2Uuid? + type = gui.lookup2Uuid[0] + gui.lookupUuid = gui.lookup2Uuid.substr(1) + gui.lookup2Uuid = null + setTimeout( () -> + if type == 'g' + $('a[href="#groups-placeholder"]').tab('show') + $("#groups-placeholder span.fa-refresh").click() + else + $('a[href="#users-placeholder_tab"]').tab('show') + $("#users-placeholder_tab span.fa-refresh").click() + , 500) + + onRefresh: (tbl) -> + gui.doLog 'Refresh called for accounts' + clearDetails() + return + + onRowDeselect: (deselected, dtable) -> + clearDetails() + return + + onRowSelect: (selected) -> + clearDetails() + + if selected.length > 1 + return + + # We can have lots of users, so memory can grow up rapidly if we do not keep thins clean + # To do so, we empty previous table contents before storing new table contents + # Anyway, TabletTools will keep "leaking" memory, but we can handle a little "leak" that will be fixed as soon as we change the section + $("#detail-placeholder").removeClass "hidden" + $('#detail-placeholder a[href="#auths-info-placeholder"]').tab('show') + + gui.tools.blockUI() + + # Load provider "info" + gui.methods.typedShow gui.accounts, selected[0], '#auths-info-placeholder .well', gettext('Error accessing data') + + id = selected[0].id + type = gui.accounts.types[selected[0].type] + gui.doLog "Type", type + user = new GuiElement(api.accounts.detail(id, "users", { permission: selected[0].permission }), "users") + group = new GuiElement(api.accounts.detail(id, "groups", { permission: selected[0].permission }), "groups") + grpTable = group.table( + icon: 'groups' + container: "groups-placeholder" + doNotLoadData: true + rowSelect: "multi" + buttons: [ + "new" + "edit" + "delete" + "xls" + ] + onLoad: (k) -> + gui.tools.unblockUI() + return + + onEdit: (value, event, table, refreshFnc) -> + exec = (groups_all) -> + gui.tools.blockUI() + api.templates.get "group", (tmpl) -> # Get form template + group.rest.item value.id, (item) -> # Get item to edit + # Creates modal + modalId = gui.launchModal(gettext("Edit group") + " " + item.name + "", api.templates.evaluate(tmpl, + id: item.id + type: item.type + meta_if_any: item.meta_if_any + groupname: item.name + groupname_label: type.groupNameLabel + comments: item.comments + state: item.state + external: type.isExternal + canSearchGroups: type.canSearchGroups + groups: item.groups + groups_all: groups_all + )) + gui.tools.applyCustoms modalId + gui.tools.unblockUI() + $(modalId + " .button-accept").click -> + fields = gui.forms.read(modalId) + gui.doLog "Fields", fields + group.rest.save fields, ((data) -> # Success on put + $(modalId).modal "hide" + refreshFnc() + gui.notify gettext("Group saved"), "success" + return + ), gui.failRequestModalFnc("Error saving group", true) + return + + return + + return + + return + + if value.type is "meta" + + # Meta will get all groups + group.rest.overview (groups) -> + exec groups + return + + else + exec() + return + + onNew: (t, table, refreshFnc) -> + exec = (groups_all) -> + gui.tools.blockUI() + api.templates.get "group", (tmpl) -> # Get form template + # Creates modal + if t is "meta" + title = gettext("New meta group") + else + title = gettext("New group") + modalId = gui.launchModal(title, api.templates.evaluate(tmpl, + type: t + groupname_label: type.groupNameLabel + external: type.isExternal + canSearchGroups: type.canSearchGroups + groups: [] + groups_all: groups_all + )) + gui.tools.unblockUI() + gui.tools.applyCustoms modalId + searchForm modalId, "group", id, gettext("Search groups"), gettext("Group"), gettext("Groups found") # Enable search button click, if it exist ofc + $(modalId + " .button-accept").click -> + fields = gui.forms.read(modalId) + gui.doLog "Fields", fields + group.rest.create fields, ((data) -> # Success on put + $(modalId).modal "hide" + refreshFnc() + gui.notify gettext("Group saved"), "success" + return + ), gui.failRequestModalFnc(gettext("Group saving error"), true) + return + + return + + return + + if t is "meta" + # Meta will get all groups + group.rest.overview (groups) -> + exec groups + return + + else + exec() + return + + onDelete: gui.methods.del(group, gettext("Delete group"), gettext("Group deletion error")) + ) + tmpLogTable = null + + # New button will only be shown on accounts that can create new users + usrButtons = [ + "edit" + "delete" + "xls" + ] + usrButtons = ["new"].concat(usrButtons) if type.canCreateUsers # New is first button + usrTable = user.table( + icon: 'users' + container: "users-placeholder" + doNotLoadData: true + rowSelect: "multi" + onRowSelect: (uselected) -> + gui.doLog 'User row selected ', uselected + gui.tools.blockUI() + uId = uselected[0].id + clearDetailLog() + tmpLogTable = user.logTable(uId, + container: "users-log-placeholder" + onLoad: -> + detailLogTable = tmpLogTable + gui.tools.unblockUI() + return + ) + return + + onRowDeselect: -> + clearDetailLog() + return + + buttons: usrButtons + deferedRender: true # Use defered rendering for users, this table can be "huge" + scrollToTable: false + onLoad: (k) -> + gui.tools.unblockUI() + return + + onRefresh: -> + gui.doLog "Refreshing" + clearDetailLog() + return + + onEdit: (value, event, table, refreshFnc) -> + password = "#æð~¬ŋ@æß”¢€~½¬@#~þ¬@|" # Garbage for password (to detect change) + gui.tools.blockUI() + api.templates.get "user", (tmpl) -> # Get form template + group.rest.overview (groups) -> # Get groups + user.rest.item value.id, (item) -> # Get item to edit + + # Creates modal + modalId = gui.launchModal(gettext("Edit user") + " " + value.name + "", api.templates.evaluate(tmpl, + id: item.id + username: item.name + username_label: type.userNameLabel + realname: item.real_name + comments: item.comments + state: item.state + staff_member: item.staff_member + is_admin: item.is_admin + needs_password: type.needsPassword + password: (if type.needsPassword then password else undefined) + password_label: type.passwordLabel + groups_all: groups + groups: item.groups + external: type.isExternal + canSearchUsers: type.canSearchUsers + )) + gui.tools.applyCustoms modalId + gui.tools.unblockUI() + $(modalId + " .button-accept").click -> + fields = gui.forms.read(modalId) + + # If needs password, and password has changed + gui.doLog "passwords", type.needsPassword, password, fields.password + delete fields.password if fields.password is password if type.needsPassword + gui.doLog "Fields", fields + user.rest.save fields, ((data) -> # Success on put + $(modalId).modal "hide" + refreshFnc() + gui.notify gettext("User saved"), "success" + return + ), gui.failRequestModalFnc(gettext("User saving error"), true) + return + + return + + return + + return + + return + + onNew: (undefined_, table, refreshFnc) -> + gui.tools.blockUI() + api.templates.get "user", (tmpl) -> # Get form template + group.rest.overview (groups) -> # Get groups + # Creates modal + modalId = gui.launchModal(gettext("New user"), api.templates.evaluate(tmpl, + username_label: type.userNameLabel + needs_password: type.needsPassword + password_label: type.passwordLabel + groups_all: groups + groups: [] + external: type.isExternal + canSearchUsers: type.canSearchUsers + )) + gui.tools.applyCustoms modalId + gui.tools.unblockUI() + searchForm modalId, "user", id, gettext("Search users"), gettext("User"), gettext("Users found") # Enable search button click, if it exist ofc + $(modalId + " .button-accept").click -> + fields = gui.forms.read(modalId) + + # If needs password, and password has changed + gui.doLog "Fields", fields + user.rest.create fields, ((data) -> # Success on put + $(modalId).modal "hide" + refreshFnc() + gui.notify gettext("User saved"), "success" + return + ), gui.failRequestModalFnc(gettext("User saving error"), true) + return + + return + + return + + return + + onDelete: gui.methods.del(user, gettext("Delete user"), gettext("User deletion error")) + ) + logTable = gui.accounts.logTable(id, + container: "logs-placeholder" + doNotLoadData: true + ) + + # So we can destroy the tables beforing adding new ones + prevTables.push grpTable + prevTables.push usrTable + prevTables.push logTable + false + + onNew: gui.methods.typedNew(gui.accounts, gettext("New authenticator"), gettext("Authenticator creation error"), testButton) + onEdit: gui.methods.typedEdit(gui.accounts, gettext("Edit authenticator"), gettext("Authenticator saving error"), testButton) + onDelete: gui.methods.del(gui.accounts, gettext("Delete authenticator"), gettext("Authenticator deletion error")) + ) + return + + false diff --git a/server/src/uds/templates/uds/admin/index.html b/server/src/uds/templates/uds/admin/index.html index 1a799a7fb..d018227c4 100644 --- a/server/src/uds/templates/uds/admin/index.html +++ b/server/src/uds/templates/uds/admin/index.html @@ -127,6 +127,8 @@ + +