317 lines
11 KiB
Python
317 lines
11 KiB
Python
# Copyright (c) 2013 OpenStack Foundation
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 oslo_log import log as logging
|
|
|
|
from trove.common import cfg
|
|
from trove.common import exception
|
|
from trove.common.i18n import _
|
|
from trove.common.notification import EndNotification
|
|
from trove.common import utils
|
|
from trove.guestagent.common import guestagent_utils
|
|
from trove.guestagent.datastore.experimental.postgresql import pgutil
|
|
from trove.guestagent.datastore.experimental.postgresql.service.access import (
|
|
PgSqlAccess)
|
|
from trove.guestagent.db import models
|
|
from trove.guestagent.db.models import PostgreSQLSchema
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
|
|
|
|
class PgSqlUsers(PgSqlAccess):
|
|
"""Mixin implementing the user CRUD API.
|
|
|
|
This mixin has a dependency on the PgSqlAccess mixin.
|
|
"""
|
|
|
|
@property
|
|
def ADMIN_USER(self):
|
|
"""Trove's administrative user."""
|
|
return 'os_admin'
|
|
|
|
@property
|
|
def ADMIN_OPTIONS(self):
|
|
"""Default set of options of an administrative account."""
|
|
return [
|
|
'SUPERUSER',
|
|
'CREATEDB',
|
|
'CREATEROLE',
|
|
'INHERIT',
|
|
'REPLICATION',
|
|
'LOGIN']
|
|
|
|
def _create_admin_user(self, context, databases=None):
|
|
"""Create an administrative user for Trove.
|
|
Force password encryption.
|
|
"""
|
|
password = utils.generate_random_password()
|
|
os_admin = models.PostgreSQLUser(self.ADMIN_USER, password)
|
|
if databases:
|
|
os_admin.databases.extend([db.serialize() for db in databases])
|
|
self._create_user(context, os_admin, True, *self.ADMIN_OPTIONS)
|
|
|
|
def create_user(self, context, users):
|
|
"""Create users and grant privileges for the specified databases.
|
|
|
|
The users parameter is a list of serialized Postgres users.
|
|
"""
|
|
with EndNotification(context):
|
|
for user in users:
|
|
self._create_user(
|
|
context,
|
|
models.PostgreSQLUser.deserialize_user(user), None)
|
|
|
|
def _create_user(self, context, user, encrypt_password=None, *options):
|
|
"""Create a user and grant privileges for the specified databases.
|
|
|
|
:param user: User to be created.
|
|
:type user: PostgreSQLUser
|
|
|
|
:param encrypt_password: Store passwords encrypted if True.
|
|
Fallback to configured default
|
|
behavior if None.
|
|
:type encrypt_password: boolean
|
|
|
|
:param options: Other user options.
|
|
:type options: list
|
|
"""
|
|
LOG.info(
|
|
_("{guest_id}: Creating user {user} {with_clause}.")
|
|
.format(
|
|
guest_id=CONF.guest_id,
|
|
user=user.name,
|
|
with_clause=pgutil.UserQuery._build_with_clause(
|
|
'<SANITIZED>',
|
|
encrypt_password,
|
|
*options
|
|
),
|
|
)
|
|
)
|
|
pgutil.psql(
|
|
pgutil.UserQuery.create(
|
|
user.name,
|
|
user.password,
|
|
encrypt_password,
|
|
*options
|
|
),
|
|
timeout=30,
|
|
)
|
|
self._grant_access(
|
|
context, user.name,
|
|
[PostgreSQLSchema.deserialize_schema(db) for db in user.databases])
|
|
|
|
def _grant_access(self, context, username, databases):
|
|
self.grant_access(
|
|
context,
|
|
username,
|
|
None,
|
|
[db.name for db in databases],
|
|
)
|
|
|
|
def list_users(
|
|
self,
|
|
context,
|
|
limit=None,
|
|
marker=None,
|
|
include_marker=False,
|
|
):
|
|
"""List all users on the instance along with their access permissions.
|
|
Return a paginated list of serialized Postgres users.
|
|
"""
|
|
return guestagent_utils.serialize_list(
|
|
self._get_users(context),
|
|
limit=limit, marker=marker, include_marker=include_marker)
|
|
|
|
def _get_users(self, context):
|
|
"""Return all non-system Postgres users on the instance."""
|
|
results = pgutil.query(
|
|
pgutil.UserQuery.list(ignore=cfg.get_ignored_users()),
|
|
timeout=30,
|
|
)
|
|
|
|
names = set([row[0].strip() for row in results])
|
|
return [self._build_user(context, name, results) for name in names]
|
|
|
|
def _build_user(self, context, username, acl=None):
|
|
"""Build a model representation of a Postgres user.
|
|
Include all databases it has access to.
|
|
"""
|
|
user = models.PostgreSQLUser(username)
|
|
if acl:
|
|
dbs = [models.PostgreSQLSchema(row[1].strip(),
|
|
character_set=row[2],
|
|
collate=row[3])
|
|
for row in acl if row[0] == username and row[1] is not None]
|
|
for d in dbs:
|
|
user.databases.append(d.serialize())
|
|
|
|
return user
|
|
|
|
def delete_user(self, context, user):
|
|
"""Delete the specified user.
|
|
"""
|
|
with EndNotification(context):
|
|
self._drop_user(
|
|
context, models.PostgreSQLUser.deserialize_user(user))
|
|
|
|
def _drop_user(self, context, user):
|
|
"""Drop a given Postgres user.
|
|
|
|
:param user: User to be dropped.
|
|
:type user: PostgreSQLUser
|
|
"""
|
|
# Postgresql requires that you revoke grants before dropping the user
|
|
dbs = self.list_access(context, user.name, None)
|
|
for d in dbs:
|
|
db = models.PostgreSQLSchema.deserialize_schema(d)
|
|
self.revoke_access(context, user.name, None, db.name)
|
|
|
|
LOG.info(
|
|
_("{guest_id}: Dropping user {name}.").format(
|
|
guest_id=CONF.guest_id,
|
|
name=user.name,
|
|
)
|
|
)
|
|
pgutil.psql(
|
|
pgutil.UserQuery.drop(name=user.name),
|
|
timeout=30,
|
|
)
|
|
|
|
def get_user(self, context, username, hostname):
|
|
"""Return a serialized representation of a user with a given name.
|
|
"""
|
|
user = self._find_user(context, username)
|
|
return user.serialize() if user is not None else None
|
|
|
|
def _find_user(self, context, username):
|
|
"""Lookup a user with a given username.
|
|
Return a new Postgres user instance or None if no match is found.
|
|
"""
|
|
results = pgutil.query(
|
|
pgutil.UserQuery.get(name=username),
|
|
timeout=30,
|
|
)
|
|
|
|
if results:
|
|
return self._build_user(context, username, results)
|
|
|
|
return None
|
|
|
|
def user_exists(self, username):
|
|
"""Return whether a given user exists on the instance."""
|
|
results = pgutil.query(
|
|
pgutil.UserQuery.get(name=username),
|
|
timeout=30,
|
|
)
|
|
|
|
return bool(results)
|
|
|
|
def change_passwords(self, context, users):
|
|
"""Change the passwords of one or more existing users.
|
|
The users parameter is a list of serialized Postgres users.
|
|
"""
|
|
with EndNotification(context):
|
|
for user in users:
|
|
self.alter_user(
|
|
context,
|
|
models.PostgreSQLUser.deserialize_user(user), None)
|
|
|
|
def alter_user(self, context, user, encrypt_password=None, *options):
|
|
"""Change the password and options of an existing users.
|
|
|
|
:param user: User to be altered.
|
|
:type user: PostgreSQLUser
|
|
|
|
:param encrypt_password: Store passwords encrypted if True.
|
|
Fallback to configured default
|
|
behavior if None.
|
|
:type encrypt_password: boolean
|
|
|
|
:param options: Other user options.
|
|
:type options: list
|
|
"""
|
|
LOG.info(
|
|
_("{guest_id}: Altering user {user} {with_clause}.")
|
|
.format(
|
|
guest_id=CONF.guest_id,
|
|
user=user.name,
|
|
with_clause=pgutil.UserQuery._build_with_clause(
|
|
'<SANITIZED>',
|
|
encrypt_password,
|
|
*options
|
|
),
|
|
)
|
|
)
|
|
pgutil.psql(
|
|
pgutil.UserQuery.alter_user(
|
|
user.name,
|
|
user.password,
|
|
encrypt_password,
|
|
*options),
|
|
timeout=30,
|
|
)
|
|
|
|
def update_attributes(self, context, username, hostname, user_attrs):
|
|
"""Change the attributes of one existing user.
|
|
|
|
The username and hostname parameters are strings.
|
|
The user_attrs parameter is a dictionary in the following form:
|
|
|
|
{"password": "", "name": ""}
|
|
|
|
Each key/value pair in user_attrs is optional.
|
|
"""
|
|
with EndNotification(context):
|
|
user = self._build_user(context, username)
|
|
new_username = user_attrs.get('name')
|
|
new_password = user_attrs.get('password')
|
|
|
|
if new_username is not None:
|
|
self._rename_user(context, user, new_username)
|
|
# Make sure we can retrieve the renamed user.
|
|
user = self._find_user(context, new_username)
|
|
if user is None:
|
|
raise exception.TroveError(_(
|
|
"Renamed user %s could not be found on the instance.")
|
|
% new_username)
|
|
|
|
if new_password is not None:
|
|
user.password = new_password
|
|
self.alter_user(context, user)
|
|
|
|
def _rename_user(self, context, user, new_username):
|
|
"""Rename a given Postgres user and transfer all access to the
|
|
new name.
|
|
|
|
:param user: User to be renamed.
|
|
:type user: PostgreSQLUser
|
|
"""
|
|
LOG.info(
|
|
_("{guest_id}: Changing username for {old} to {new}.").format(
|
|
guest_id=CONF.guest_id,
|
|
old=user.name,
|
|
new=new_username,
|
|
)
|
|
)
|
|
# PostgreSQL handles the permission transfer itself.
|
|
pgutil.psql(
|
|
pgutil.UserQuery.update_name(
|
|
old=user.name,
|
|
new=new_username,
|
|
),
|
|
timeout=30,
|
|
)
|