Commit 3e25ff90 authored by Daniele Venzano's avatar Daniele Venzano

Implementation of admin pages, first part

parent 9769a171
...@@ -283,20 +283,19 @@ class APIEndpoint: ...@@ -283,20 +283,19 @@ class APIEndpoint:
if 'auth_source' in user_data: if 'auth_source' in user_data:
update_fields['auth_source'] = user_data['auth_source'] update_fields['auth_source'] = user_data['auth_source']
if user_data['auth_source'] != 'internal': if user_data['auth_source'] != 'internal':
user_data['password'] = None update_fields['password'] = None
if 'quota_id' in user_data: if 'quota_id' in user_data:
quota = self.quota_by_id(user_data['quota_id']) quota = self.quota_by_id(user_data['quota_id'])
if quota is None: if quota is None:
raise zoe_api.exceptions.ZoeRestAPIException('No quota with ID {}'.format(user_data['quota_id'])) raise zoe_api.exceptions.ZoeRestAPIException('No quota with ID {}'.format(user_data['quota_id']))
update_fields['quota_id'] = quota.id update_fields['quota_id'] = quota.id
if 'role' in user_data: if 'role_id' in user_data:
role = self.role_by_id(user_data['role_id']) role = self.role_by_id(user_data['role_id'])
if role is None: if role is None:
raise zoe_api.exceptions.ZoeRestAPIException('No role with ID {}'.format(user_data['role_id'])) raise zoe_api.exceptions.ZoeRestAPIException('No role with ID {}'.format(user_data['role_id']))
update_fields['role_id'] = role.id update_fields['role_id'] = role.id
if 'password' in user_data: if 'password' in user_data:
update_fields['password'] = user_data['password'] update_fields['password'] = user_data['password']
update_fields['auth_source'] = 'internal'
if 'fs_uid' in user_data: if 'fs_uid' in user_data:
update_fields['fs_uid'] = user_data['fs_uid'] update_fields['fs_uid'] = user_data['fs_uid']
......
...@@ -24,6 +24,7 @@ import zoe_api.web.websockets ...@@ -24,6 +24,7 @@ import zoe_api.web.websockets
import zoe_api.web.executions import zoe_api.web.executions
import zoe_api.web.zapp_shop import zoe_api.web.zapp_shop
import zoe_api.web.status import zoe_api.web.status
import zoe_api.web.admin
import zoe_lib.config import zoe_lib.config
from zoe_lib.version import ZOE_API_VERSION, ZOE_VERSION from zoe_lib.version import ZOE_API_VERSION, ZOE_VERSION
...@@ -56,7 +57,11 @@ def web_init(api_endpoint) -> List[tornado.web.URLSpec]: ...@@ -56,7 +57,11 @@ def web_init(api_endpoint) -> List[tornado.web.URLSpec]:
tornado.web.url(base_path + r'/zapp-shop/logo/([0-9a-z_\-.]+)', zoe_api.web.zapp_shop.ZAppLogoWeb, route_args, name='zappshop_logo'), tornado.web.url(base_path + r'/zapp-shop/logo/([0-9a-z_\-.]+)', zoe_api.web.zapp_shop.ZAppLogoWeb, route_args, name='zappshop_logo'),
tornado.web.url(base_path + r'/zapp-shop/start/([0-9a-z_\-.]+)', zoe_api.web.zapp_shop.ZAppStartWeb, route_args, name='zappshop_start'), tornado.web.url(base_path + r'/zapp-shop/start/([0-9a-z_\-.]+)', zoe_api.web.zapp_shop.ZAppStartWeb, route_args, name='zappshop_start'),
tornado.web.url(base_path + r'/status', zoe_api.web.status.StatusEndpointWeb, route_args, name='status') tornado.web.url(base_path + r'/status', zoe_api.web.status.StatusEndpointWeb, route_args, name='status'),
tornado.web.url(base_path + r'/admin/users', zoe_api.web.admin.UsersEndpointWeb, route_args, name='admin_users'),
tornado.web.url(base_path + r'/admin/roles', zoe_api.web.admin.RolesEndpointWeb, route_args, name='admin_roles'),
tornado.web.url(base_path + r'/admin/quotas', zoe_api.web.admin.QuotasEndpointWeb, route_args, name='admin_quotas')
] ]
return web_routes return web_routes
......
# Copyright (c) 2018, Daniele Venzano
#
# 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.
"""Main points of entry for the Zoe web interface."""
from zoe_api.web.request_handler import ZoeWebRequestHandler
class UsersEndpointWeb(ZoeWebRequestHandler):
"""Handler class"""
def get(self):
"""User admin page."""
if self.current_user is None or not self.current_user.role.can_change_config:
return
template_vars = {
'kind': 'users',
'column_keys': ['username', 'email', 'enabled', 'priority', 'fs_uid', 'auth_source', 'role', 'quota'],
'column_names': ['Username', 'Email', 'Enabled', 'Priority', 'FS UID', 'Authentication', 'Role', 'Quota'],
'column_types': {
'username': None,
'email': 'text',
'enabled': 'bool',
'priority': 'number',
'fs_uid': 'number',
'auth_source': 'list',
'role': 'list',
'quota': 'list'
},
'lists': {
'auth_source': ['internal', 'pam', 'ldap', 'ldap+sasl', 'gitlab-eurecom'],
'role': self.api_endpoint.role_list(self.current_user),
'quota': self.api_endpoint.quota_list(self.current_user)
},
'rows': self.api_endpoint.user_list(self.current_user)
}
self.render('admin.jinja2', **template_vars)
def post(self):
"""Form submitted."""
if self.current_user is None or not self.current_user.role.can_change_config:
return
user_id = int(self.get_argument('id'))
if self.get_argument('action') == 'update':
user_data = {
'email': None if self.get_argument('email') == '' or self.get_argument('email') == 'None' else self.get_argument('email'),
'enabled': True if self.get_argument('enabled') == 'on' else False,
'fs_uid': int(self.get_argument('fs_uid')),
'auth_source': self.get_argument('auth_source'),
'role_id': self.api_endpoint.role_by_name(self.get_argument('role')).id,
'quota_id': self.api_endpoint.quota_by_name(self.get_argument('quota')).id
}
self.api_endpoint.user_update(self.current_user, user_id, user_data)
elif self.get_argument('action') == 'delete':
self.api_endpoint.user_delete(self.current_user, user_id)
self.redirect(self.reverse_url('admin_users'))
class RolesEndpointWeb(ZoeWebRequestHandler):
"""Handler class"""
def get(self):
"""Roles admin page"""
if self.current_user is None or not self.current_user.role.can_change_config:
return
template_vars = {
'kind': 'roles',
'column_keys': ['name', 'can_see_status', 'can_change_config', 'can_operate_others', 'can_delete_executions', 'can_access_api', 'can_customize_resources', 'can_access_full_zapp_shop'],
'column_names': ['Name', 'See status page', 'Change configuration', 'Operate on non-own executions', 'Delete executions', 'API access', 'Web customize resources', 'Access all ZApps'],
'column_types': {
'name': None,
'can_see_status': 'bool',
'can_change_config': 'bool',
'can_operate_others': 'bool',
'can_delete_executions': 'bool',
'can_access_api': 'bool',
'can_customize_resources': 'bool',
'can_access_full_zapp_shop': 'bool'
},
'rows': self.api_endpoint.role_list(self.current_user)
}
self.render('admin.jinja2', **template_vars)
def post(self):
"""Form submitted."""
if self.current_user is None or not self.current_user.role.can_change_config:
return
class QuotasEndpointWeb(ZoeWebRequestHandler):
"""Handler class"""
def get(self):
"""Quota admin page"""
if self.current_user is None or not self.current_user.role.can_change_config:
return
template_vars = {
'kind': 'quotas',
'column_keys': ['name', 'concurrent_executions', 'cores', 'memory', 'runtime_limit'],
'column_names': ['Name', 'Concurrent executions', 'Cores', 'Memory (GiB)', 'Run time (hours)'],
'column_types': {
'name': None,
'concurrent_executions': 'number',
'cores': 'number',
'memory': 'bytes',
'runtime_limit': 'number'
},
'rows': self.api_endpoint.quota_list(self.current_user)
}
self.render('admin.jinja2', **template_vars)
def post(self):
"""Form submitted."""
if self.current_user is None or not self.current_user.role.can_change_config:
return
...@@ -24,7 +24,7 @@ class StatusEndpointWeb(ZoeWebRequestHandler): ...@@ -24,7 +24,7 @@ class StatusEndpointWeb(ZoeWebRequestHandler):
def get(self): def get(self):
"""Status and statistics page.""" """Status and statistics page."""
if self.current_user is None: if self.current_user is None or not self.current_user.role.can_see_status:
return return
stats = self.api_endpoint.statistics_scheduler() stats = self.api_endpoint.statistics_scheduler()
......
{% extends "base_user.jinja2" %}
{% block title %}Zoe {{ kind }} admin{% endblock %}
{% block custom_head %}
{% endblock %}
{% block content %}
<h2>Zoe {{ kind }} admin</h2>
<table id="exec_list" class="app_list">
<thead>
<tr>
<th>ID</th>
{% for h in column_names %}
<th>{{ h }}</th>
{% endfor %}
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for row in rows|sort(attribute='id') %}
{% set row_index = loop.index0 %}
<tr>
<td><form id="form{{ row_index }}" method="post" action="{{ reverse_url('admin_' + kind) }}"><input type="hidden" name="id" value="{{ row.id }}">{{ row.id }}</form></td>
{% for key in column_keys %}
{% if column_types[key] == None %}
<td>{{ row[key] }}</td>
{% elif column_types[key] == 'text' %}
<td><input form="form{{ row_index }}" type="text" name="{{ key }}" value="{{ row[key] }}" title="{{ key }}"></td>
{% elif column_types[key] == 'number' %}
<td><input form="form{{ row_index }}" type="number" name="{{ key }}" value="{{ row[key] }}" title="{{ key }}"></td>
{% elif column_types[key] == 'bytes' %}
<td><input form="form{{ row_index }}" type="number" name="{{ key }}" value="{{ row[key]/(1024**3) }}" title="{{ key }}"></td>
{% elif column_types[key] == 'bool' %}
{% if row[key] %}
<td><input form="form{{ row_index }}" type="checkbox" name="{{ key }}" checked title="{{ key }}"></td>
{% else %}
<td><input form="form{{ row_index }}" type="checkbox" name="{{ key }}" title="{{ key }}"></td>
{% endif %}
{% elif column_types[key] == 'list' %}
<td>
<select form="form{{ row_index }}" title="{{ key }}" name="{{ key }}">
{% for item in lists[key] %}
{% if row[key] == item %}
<option value="{{ item }}" selected>{{ item }}</option>
{% else %}
<option value="{{ item }}">{{ item }}</option>
{% endif %}
{% endfor %}
</select>
</td>
{% endif %}
{% endfor %}
<td>
<button form="form{{ row_index }}" name="action" type="submit" value="update">Update</button>
<button form="form{{ row_index }}" name="action" type="submit" value="delete">Delete</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
...@@ -20,6 +20,17 @@ ...@@ -20,6 +20,17 @@
<a href="{{ reverse_url("status") }}">Status</a> <a href="{{ reverse_url("status") }}">Status</a>
</div> </div>
{% endif %} {% endif %}
{% if user.role.can_change_config %}
<div class="nav-item">
<a href="{{ reverse_url("admin_users") }}">Users</a>
</div>
<div class="nav-item">
<a href="{{ reverse_url("admin_roles") }}">Roles</a>
</div>
<div class="nav-item">
<a href="{{ reverse_url("admin_quotas") }}">Quotas</a>
</div>
{% endif %}
</div> </div>
<div id="user_info"> <div id="user_info">
{{ user.username }} ({{ user.role.name }}) <a href="{{ reverse_url("logout") }}">logout</a> {{ user.username }} ({{ user.role.name }}) <a href="{{ reverse_url("logout") }}">logout</a>
......
...@@ -65,6 +65,15 @@ class Quota(BaseRecord): ...@@ -65,6 +65,15 @@ class Quota(BaseRecord):
self.runtime_limit = value self.runtime_limit = value
self.sql_manager.quota_update(self.id, runtime_limit=value) self.sql_manager.quota_update(self.id, runtime_limit=value)
def __repr__(self):
return self.name
def __eq__(self, other):
if isinstance(other, Quota):
return self.id == other.id
else:
return False
class QuotaTable(BaseTable): class QuotaTable(BaseTable):
"""Abstraction for the quota table in the database.""" """Abstraction for the quota table in the database."""
......
...@@ -51,6 +51,15 @@ class Role(BaseRecord): ...@@ -51,6 +51,15 @@ class Role(BaseRecord):
'can_access_full_zapp_shop': self.can_access_full_zapp_shop 'can_access_full_zapp_shop': self.can_access_full_zapp_shop
} }
def __repr__(self):
return self.name
def __eq__(self, other):
if isinstance(other, Role):
return self.id == other.id
else:
return False
class RoleTable(BaseTable): class RoleTable(BaseTable):
"""Abstraction for the role table in the database.""" """Abstraction for the role table in the database."""
......
...@@ -105,6 +105,15 @@ class User(BaseRecord): ...@@ -105,6 +105,15 @@ class User(BaseRecord):
"""Get the quota for this user.""" """Get the quota for this user."""
return self.sql_manager.quota.select(only_one=True, id=self.quota_id) return self.sql_manager.quota.select(only_one=True, id=self.quota_id)
def __repr__(self):
return self.username
def __eq__(self, other):
if isinstance(other, User):
return self.id == other.id
else:
return False
class UserTable(BaseTable): class UserTable(BaseTable):
"""Abstraction for the user table in the database.""" """Abstraction for the user table in the database."""
...@@ -182,6 +191,6 @@ class UserTable(BaseTable): ...@@ -182,6 +191,6 @@ class UserTable(BaseTable):
def update(self, user_id, **fields): def update(self, user_id, **fields):
"""Update a user record.""" """Update a user record."""
if 'password' in fields: if 'password' in fields and fields['password'] is not None:
fields['password'] = hash_algo.hash(fields['password']) fields['password'] = hash_algo.hash(fields['password'])
super().update(user_id, **fields) super().update(user_id, **fields)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment