diff --git a/cterasdk/cio/core/commands.py b/cterasdk/cio/core/commands.py index da958147..3e3ac5fb 100644 --- a/cterasdk/cio/core/commands.py +++ b/cterasdk/cio/core/commands.py @@ -532,6 +532,20 @@ def _handle_response(self, r): return PortalResource.from_server_object(metadata) +class GetShareCandidate(GetProperties): + + def _handle_response(self, r): + candidate = r.root.gwShareManagementCandidate + return Object(**{ + 'full_path': candidate.fullPath, + 'display_path': candidate.displayPath, + 'cloud_folder_uid': candidate.cloudFolderUid, + 'has_acl': candidate.hasAcl, + 'web_dav_url': candidate.webDavUrl, + 'cloud_folder_name': r.root.name + }) + + class GetPermalink(GetMetadata): def _handle_response(self, r): diff --git a/cterasdk/cio/core/types.py b/cterasdk/cio/core/types.py index 151a7139..4cb6da1a 100644 --- a/cterasdk/cio/core/types.py +++ b/cterasdk/cio/core/types.py @@ -102,7 +102,7 @@ class GlobalAdminPath(PortalPath): @staticmethod def from_context(reference): - return ServicesPortalPath(GlobalAdminPath.Namespace, reference) + return GlobalAdminPath(GlobalAdminPath.Namespace, reference) class InvitationPath(PortalPath): diff --git a/cterasdk/clients/clients.py b/cterasdk/clients/clients.py index 9ed818c7..3df1e8ce 100644 --- a/cterasdk/clients/clients.py +++ b/cterasdk/clients/clients.py @@ -236,8 +236,8 @@ def multipart(self, path, form, *, on_response=None, on_error=None, **kwargs): return self.request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - def delete(self, path, *, on_response=None, on_error=None, **kwargs): - request = async_requests.DeleteRequest(self._builder(path), **kwargs) + def delete(self, path, data=None, *, data_serializer=None, on_response=None, on_error=None, **kwargs): + request = async_requests.DeleteRequest(self._builder(path), data=data_serializer(data), **kwargs) return self.request(request, on_response=on_response, on_error=on_error) def _request(self, request, *, on_response=None, on_error=None): @@ -325,8 +325,8 @@ def form_data(self, path, data, **kwargs): response = super().form_data(path, data, on_error=XMLHandler(), **kwargs) return response.xml() - def delete(self, path, **kwargs): - response = super().delete(path, on_error=XMLHandler(), **kwargs) + def delete(self, path, data=None, **kwargs): + response = super().delete(path, data, on_error=XMLHandler(), **kwargs) return response.xml() @@ -338,15 +338,21 @@ def get(self, path, **kwargs): return response.json() def put(self, path, data, **kwargs): - response = super().put(path, data, data_serializer=Serializers.JSON, on_error=JSONHandler(), **kwargs) + response = super().put(path, data, data_serializer=Serializers.JSON, headers={ + 'Content-Type': 'application/json' + }, on_error=JSONHandler(), **kwargs) return response.json() def post(self, path, data, **kwargs): - response = super().post(path, data, data_serializer=Serializers.JSON, on_error=JSONHandler(), **kwargs) + response = super().post(path, data, data_serializer=Serializers.JSON, headers={ + 'Content-Type': 'application/json' + }, on_error=JSONHandler(), **kwargs) return response.json() - def delete(self, path, **kwargs): - response = super().delete(path, on_error=JSONHandler(), **kwargs) + def delete(self, path, data=None, **kwargs): + response = super().delete(path, data, data_serializer=Serializers.JSON, headers={ + 'Content-Type': 'application/json' + }, on_error=JSONHandler(), **kwargs) return response.json() diff --git a/cterasdk/convert/serializers.py b/cterasdk/convert/serializers.py index 9ddca1ef..f15887d9 100644 --- a/cterasdk/convert/serializers.py +++ b/cterasdk/convert/serializers.py @@ -36,7 +36,18 @@ def _to_protected_dict(o): return ret -def tojsonstr(obj, pretty_print=True, no_log=True): +class Encoder(json.JSONEncoder): + + def default(self, o): + d = o.get('__dict__', None) + if d: + if '_classname' in d: + d['$class'] = d.pop('_classname') + return d + return super().default(o) + + +def tojsonstr(obj, pretty_print=True, no_log=False): """ Convert a Python object to a JSON string. @@ -49,7 +60,7 @@ def tojsonstr(obj, pretty_print=True, no_log=True): indent = 5 if pretty_print else None if no_log: return json.dumps(obj, default=_to_protected_dict, indent=indent) - return json.dumps(obj, default=lambda o: o.__dict__, indent=indent) + return json.dumps(obj, cls=Encoder, indent=indent) def toxmlstr(obj, pretty_print=False, no_log=False): diff --git a/cterasdk/core/enum.py b/cterasdk/core/enum.py index 80e7060a..76711b99 100644 --- a/cterasdk/core/enum.py +++ b/cterasdk/core/enum.py @@ -202,6 +202,14 @@ class CollaboratorType: DG = "adGroup" EXT = "external" + @staticmethod + def from_account(account): + if account.account_type == PortalAccountType.User: + return CollaboratorType.LU if account.is_local else CollaboratorType.DU + if account.account_type == PortalAccountType.Group: + return CollaboratorType.DU if account.is_local else CollaboratorType.DG + raise ValueError(f'Unknown principal type: {account.account_type}') + class PortalType: """ @@ -752,3 +760,38 @@ class NativeFormat: Filesystem = 'Filesystem' Bucket = 'Bucket' Bidirectional = 'Bidirectional' + + +class ShareGroup: + """ + Grouping Criteria when Listing Network Shares + + :ivar str Path: Group by Path + :ivar str Edge Filer: Group by Edge Filers + """ + Path = 'path' + Edge = 'edgeFilers' + + +class ShareProtocol: + """ + Filter Shares by Protocol + + :ivar str NFS: NFS Shares + :ivar str SMB: SMB Shares + """ + SMB = 'smb' + NFS = 'nfs' + + +class KRBSecurity: + """ + Kerberos Security. + + :ivar str KRB5: krb5 + :ivar str KRB5I: krb5i + :ivar str KRB5P: krb5p + """ + KRB5 = "krb5" + KRB5I = "krb5i" + KRB5P = "krb5p" diff --git a/cterasdk/core/groups.py b/cterasdk/core/groups.py index a8c45ab2..875e23f5 100644 --- a/cterasdk/core/groups.py +++ b/cterasdk/core/groups.py @@ -46,30 +46,45 @@ def get(self, group_account, include=None): raise ObjectNotFoundException(baseurl) return group_object - def list_local_groups(self, include=None): + def list_local_groups(self, include=None, filters=None): """ List all local groups :param list[str] include: List of fields to retrieve, defaults to ['name'] + :param list[],optional filters: List of additional filters, defaults to None :return: Iterator for all local groups :rtype: cterasdk.lib.iterator.QueryIterator """ - include = union(include or [], Groups.default) - param = query.QueryParamBuilder().include(include).build() - return query.iterator(self._core, '/localGroups', param) + return self._groups('/localGroups', include, filters) - def list_domain_groups(self, domain, include=None): + def list_domain_groups(self, domain, include=None, filters=None): """ List all the groups in the domain :param str domain: Domain name :param list[str] include: List of fields to retrieve, defaults to ['name'] + :param list[],optional filters: List of additional filters, defaults to None :return: Iterator for all the domain groups :rtype: cterasdk.lib.iterator.QueryIterator """ + return self._groups(f'/domains/{domain}/adGroups', include, filters) + + def _groups(self, path, include, filters): + """ + List Groups. + + :param str path: Path + :param list[str],optional include: List of fields to retrieve, defaults to ['name'] + :param list[],optional filters: List of additional filters, defaults to None + """ include = union(include or [], Groups.default) - param = query.QueryParamBuilder().include(include).build() - return query.iterator(self._core, f'/domains/{domain}/adGroups', param) + builder = query.QueryParamBuilder().include(include) + if filters: + for query_filter in filters: + builder.addFilter(query_filter) + builder.orFilter((len(filters) > 1)) + param = builder.build() + return query.iterator(self._core, path, param) def _members_reference(self, users): return [self._core.users.get(user, include=['baseObjectRef']).baseObjectRef for user in users] diff --git a/cterasdk/core/query.py b/cterasdk/core/query.py index 13e86b5d..027cca34 100644 --- a/cterasdk/core/query.py +++ b/cterasdk/core/query.py @@ -1,14 +1,16 @@ from datetime import datetime -from ..lib import QueryIterator, DefaultResponse, Command +from ..lib import QueryIterator, DefaultResponse, v2DefaultResponse, Command from ..common import Object -def run(core, path, param): - return create_callback_function(core, path, callback_response=DefaultResponse)(param) +def run(core, path, param, *, version=None): + if version != 'v2': + return v1_callback_function(core, path, callback_response=DefaultResponse)(param) + return v2_callback_function(core, path) -def create_callback_function(core, path, name=None, *, callback_response=None): +def v1_callback_function(core, path, name=None, *, callback_response=None): """ Create a query callback function @@ -29,22 +31,38 @@ def execute(core, path, name, param): return Command(execute if name else database, core, path, name or 'query') -def iterator(core, path, param=None, name=None, *, callback_response=None): +def v2_callback_function(core, path): + + def wrapper(core, path, params): + return v2DefaultResponse(core.clients.v2.get(path, params=dict(params))) + + return Command(wrapper, core, path) + + +def iterator(core, path, param=None, name=None, *, callback_response=None, version=None): """ Create iterator :param cterasdk.objects.core.Portal core: Portal object :param str path: URL Path :param str,optional name: Schema method name - :param cterasdk.core.query.QueryParams,optional param: Query paramter object + :param cterasdk.core.query.QueryParams or dict,optional param: + Query parameters, either as a ``QueryParams`` object or a dictionary of key-value pairs. :param cterasdk.lib.iterator.BaseResponse callback_response: Class to consume callback response + :param str,optional version: Iterator API Version :returns: Query iterator object """ - callback_response = callback_response if callback_response else DefaultResponse - callback_function = create_callback_function(core, path, name, callback_response=callback_response) - return QueryIterator(callback_function, param if param else QueryParams()) + if version != 'v2': + + callback_response = callback_response if callback_response else DefaultResponse + + callback_function = v1_callback_function(core, path, name, callback_response=callback_response) + + return QueryIterator(callback_function, param if param else QueryParams()) + + return QueryIterator(v2_callback_function(core, path), v2QueryParams(**param)) class Restriction: @@ -148,6 +166,19 @@ def setValue(self, value): return self.filter +class v2QueryParams(Object): + + def __init__(self, **kwargs): + super().__init__() + self.page = 0 + self.size = 150 + for key, value in kwargs.items(): + setattr(self, key, value) + + def increment(self): + self.page = self.page + 1 + + class QueryParams(Object): def __init__(self): diff --git a/cterasdk/core/shares.py b/cterasdk/core/shares.py new file mode 100644 index 00000000..287da54f --- /dev/null +++ b/cterasdk/core/shares.py @@ -0,0 +1,274 @@ +import logging +from .base_command import BaseCommand +from . import query +from .enum import ShareGroup, CollaboratorType +from .types import Share, BlockRule, ShareInfo +from .enum import Context +from ..edge.enum import Acl +from ..common import Object +from ..cio.core.commands import GetShareCandidate +from ..exceptions import ObjectNotFoundException + + +logger = logging.getLogger('cterasdk.core') + + +def wrapper(core, param): + return core.api.execute('', 'fetchGwShareCandidates', param) + + +class Shares(BaseCommand): + """ + Share Management APIs + """ + + @staticmethod + def _deduce_unique_identifier(share): + if isinstance(share, (Share, ShareInfo)): + return share.id + if isinstance(share, str): + return share + raise ValueError('Could not determine share identifier') + + def _get_configuration(self, share): + """ + Get Share + + :param cterasdk.core.types.Share or str: Share object or unique idenifier. + :returns: Share metadata + :rtype: cterasdk.common.object.Object + """ + uid = Shares._deduce_unique_identifier(share) + response = self._core.clients.v2.get(f'shareManagement/configurations/{uid}') + if response: + return response.data + raise ObjectNotFoundException(uid) + + def get(self, share): + """ + Get Share. + + :param cterasdk.core.types.Share or str: Share object or unique idenifier. + :returns: Share information + :rtype: cterasdk.core.types.ShareInfo + """ + response = self._get_configuration(share) + return ShareInfo.from_server_object(response) + + def all(self, devices=None, protocol=None, search=None): + """ + List Shares. + + :param list[str], optional devices: Filter results to shares belonging to one or more Edge Filers. + :param cterasdk.core.enum.ShareProtocol, optional protocol: Filter results by share protocol. + :param str, optional search: Return only shares matching the specified search string. + + :returns: An iterator yielding share objects. + :rtype: generator[cterasdk.core.types.Share] + """ + params = {} + if devices: + params['group_by'] = ShareGroup.Edge + if protocol: + params['protocol'] = protocol + if search: + params['search'] = search + response = query.iterator(self._core, 'shareManagement/configurations/display', params, version='v2') + + def from_server_object(server_object): + return Share.from_server_object(server_object) + + for item in response: + if devices and item.group in devices: + yield item.group, [from_server_object(share) for share in item.shares] + yield from_server_object(item) + + def _users(self, domain, acl): + + def user_ace(user, perm): + param = Object() + param.permissions = perm + param.collaborator = user + param.collaborator._type = 'user' # pylint: disable=protected-access + param.collaborator.type = CollaboratorType.DU + return param + + users = {ace.name: ace.perm for ace in acl} + + filters = [query.FilterBuilder('name').eq(name) for name in users.keys()] + + include = ['baseObjectRef', 'domain', 'email', 'firstName', 'lastName', 'name', 'uid', 'isTrashcan'] + + return [user_ace(user, users[user.name]) for user in self._core.users.list_domain_users(domain, include, filters)] if acl else [] + + def _groups(self, domain, acl): + + def group_ace(group, perm): + param = Object() + param.permissions = perm + param.collaborator = group + param.collaborator._type = 'group' # pylint: disable=protected-access + param.collaborator.type = CollaboratorType.DG + return param + + groups = {ace.name: ace.perm for ace in acl} + + filters = [query.FilterBuilder('name').eq(name) for name in groups.keys()] + + include = ['baseObjectRef', 'domain', 'name', 'uid'] + + return [ + group_ace(group, groups[group.name]) + for group in self._core.groups.list_domain_groups(domain=domain, include=include, filters=filters) + ] + + def _prepare_access_control_entries(self, acl, validate_acl): + + mapping = {ace.account.directory: ([], []) for ace in acl} # group principals by domain, and type + + access_control_entries = [] + + if validate_acl: + + for ace in acl: + + if ace.principal_type in [CollaboratorType.DU]: + mapping[ace.account.directory][0].append(ace) + + if ace.principal_type in [CollaboratorType.DG]: + mapping[ace.account.directory][1].append(ace) + + for domain, principals in mapping.items(): # for each domain, search for members and update access control entries + + users, groups = self._users(domain, principals[0]), self._groups(domain, principals[1]) + + access_control_entries.extend(users) + + access_control_entries.extend(groups) + + return access_control_entries + + return [ace.to_server_object() for ace in acl] + + def modify(self, share, name=None, directory=None, # pylint: disable=too-many-arguments, too-many-locals, too-many-branches + devices=None, acl=None, description=None, access=None, export_to_nfs=None, + nfs_krb=None, trusted_nfs_clients=None, krb_sec=None, block_files=None, + export_to_ftp=None, validate_acl=True): + param = self._get_configuration(share) + + if name: + param.name = name + + if directory: + param.path_info = GetShareCandidate(wrapper, self._core, directory).execute() + param.directory = param.path_info.full_path + + if devices: + param.device_ids = [device.uid for device in self._core.devices.by_name(devices, include=['uid'])] + else: + param.device_ids = [device.device_id for device in param.device_shares] + + if acl: + acl = self._prepare_access_control_entries(acl, validate_acl) + param.acl_rules = acl + + if trusted_nfs_clients: + param.trusted_nfs_clients = [network.to_server_object() for network in trusted_nfs_clients] + + if block_files: + param.screened_file_types_rules = [rule.to_server_object() for rule in block_files] if block_files else [BlockRule.default()] + param.screened_file_types_enabled = True + + if description: + param.description = description + + if access: + param.access = access + + if export_to_nfs is not None: + param.export_to_nfs = export_to_nfs + + if param.nfs_kerberos is not None: + param.nfs_kerberos = nfs_krb + + if export_to_ftp is not None: + param.export_to_ftp = export_to_ftp + + if krb_sec: + Shares._nfs_krb(param, krb_sec) + + logger.info("Modifying Share. %s", {'name': param.name}) + response = self._core.clients.v2.put(f'shareManagement/configurations/{param.id}', param) + logger.info("Share modified. %s", {'name': param.name}) + return response.data.share_id + + def add(self, name, directory, devices, acl=None, # pylint: disable=too-many-arguments, too-many-locals, too-many-branches + description=None, access=Acl.WindowsNT, export_to_nfs=False, + nfs_krb=False, trusted_nfs_clients=None, krb_sec=None, block_files=None, + export_to_ftp=False, validate_acl=True): + """ + Add Share. + + :param str name: Share name + :param str directory: Path + :param list[str] devices: Edge filers + :param list[cterasdk.core.types.ShareAccessControlEntry],optional acl: List of access control entries + :param str,optional description: Description + :param cterasdk.edge.enum.Acl,optional access: Windows File Sharing authentication mode, defaults to ``winAclMode`` + :param bool export_to_nfs: Whether to enable NFS access, defaults to ``False`` + :param list[cterasdk.core.types.NFSv3AccessControlEntry] trusted_nfs_clients: Trusted NFS v3 clients, defaults to ``None`` + :param list[cterasdk.core.enum.KRBSecurity],optional krb_sec: NFS Kerberos Security Priority + :param bool,optional export_to_ftp: Whether to enable FTP access, defaults to ``False`` + :param list[BlockRule],optional block_files: Screen file extensions. + :param bool,optional validate_acl: Validate ACLs with the CTERA Portal. Defaults to ``True``. + + :returns: Share ID + :rtype: str + """ + acl = self._prepare_access_control_entries(acl, validate_acl) + + if self._core.session().context == Context.admin: + directory = f'Users/{directory}' + + with GetShareCandidate(wrapper, self._core, directory) as metadata: + param = Object() + param.name = name + param.device_ids = [device.uid for device in self._core.devices.by_name(devices, include=['uid'])] + param.path_info = metadata + param.access_type = access + param.acl_rules = acl + param.export_to_nfs = export_to_nfs + param.nfs_kerberos = nfs_krb + param.export_to_ftp = export_to_ftp + + if description: + param.description = description + + Shares._nfs_krb(param, krb_sec) + + if trusted_nfs_clients: + param.trusted_nfs_clients = [network.to_server_object() for network in (trusted_nfs_clients or [])] + + param.screened_file_types_rules = [rule.to_server_object() for rule in block_files] if block_files else [BlockRule.default()] + param.screened_file_types_enabled = bool(block_files) + + logger.info("Creating Share. %s", {'name': name}) + response = self._core.clients.v2.post('shareManagement/configurations', param) + logger.info("Share created. %s", {'name': name}) + return response.data.share_id + + def delete(self, *shares): + """ + Delete Shares. + + :param list[cterasdk.core.types.Share] or str shares: List of Shares objects, or unique identifers + """ + return self._core.clients.v2.delete('/shareManagement/configurations', + [Shares._deduce_unique_identifier(share) for share in shares]) + + @staticmethod + def _nfs_krb(param, krb_sec): + if krb_sec: + krb_sec = [krb_sec] if isinstance(krb_sec, str) else krb_sec + for label, value in zip(['first', 'second', 'third'], krb_sec or []): + setattr(param, f'nfs_sec_{label}', value) diff --git a/cterasdk/core/types.py b/cterasdk/core/types.py index 8fd0f140..41f40ab1 100644 --- a/cterasdk/core/types.py +++ b/cterasdk/core/types.py @@ -1,4 +1,7 @@ +# pylint: disable=too-many-lines + from abc import ABC +from datetime import datetime from collections import namedtuple from ..common import DateTimeUtils, StringCriteriaBuilder, PredefinedListCriteriaBuilder, CustomListCriteriaBuilder, Object from ..lib.storage import commonfs @@ -7,6 +10,9 @@ BucketType, LocationType, Platform, RetentionMode, Duration, ExtendedAttributes, ConflictHandler, NativeFormat +from ..edge.types import AccessControlEntryValidator + + CloudFSFolderFindingHelper = namedtuple('CloudFSFolderFindingHelper', ('name', 'owner')) CloudFSFolderFindingHelper.__doc__ = 'Tuple holding the folder name and owner (UserAccount) to search for cloud drive folders' CloudFSFolderFindingHelper.name.__doc__ = 'The name of the CloudFS folder' @@ -987,3 +993,222 @@ def __init__(self, access, is_dir): @staticmethod def from_server_object(server_object): return PortalInvitation(server_object.mode, server_object.isDirectory) + + +class ShareInfo: # pylint: disable=too-many-instance-attributes + + def __init__(self, uid, name, path, access, description, devices, acl, # pylint: disable=too-many-arguments + export_to_nfs, nfs_kerberos, trusted_nfs_clients, export_to_ftp, created_at, updated_at): + self.id = uid + self.name = name + self.path = path + self.description = description + self.devices = devices + self.smb = Object() + self.smb.acl = acl + + self.nfs = Object() + self.nfs.enabled = export_to_nfs + self.nfs.acl = trusted_nfs_clients + self.nfs.kerberos = Object() + self.nfs.kerberos.enabled = nfs_kerberos + + self.ftp = Object() + self.ftp.enabled = export_to_ftp + self.created_at = created_at + self.updated_at = updated_at + + self.access = access + + @staticmethod + def from_server_object(server_object): + return ShareInfo( + server_object.id, + server_object.name, + server_object.path_info.display_path, + server_object.access_type, + getattr(server_object, 'description', None), + [device.device_name for device in server_object.device_shares], + [ShareAccessControlEntry.from_server_object(ace) for ace in server_object.acl_rules], + server_object.export_to_nfs, + server_object.nfs_kerberos, + [NFSv3AccessControlEntry.from_server_object(ace) for ace in server_object.trusted_nfs_clients], + server_object.export_to_ftp, + datetime.fromisoformat(server_object.created_at), + datetime.fromisoformat(server_object.updated_at) + ) + + def __repr__(self): + return str(self) + + def __str__(self): + return ( + f"{self.__class__.__name__}(" + f"{{'name': {self.name}, " + f"'created_at': {self.created_at.isoformat()}, " + f"'path': {self.path}, " + f"'devices': {self.devices}}})" + ) + + +class Share(Object): + """ + Class for Portal Share + + :ivar str id: Share ID + :ivar str name: Name + :ivar str path: Path + :ivar list[str] protocols: Protocols + :ivar str access: Access Mode + :ivar list[str] devices: Devices + :ivar str absolute: Path, including '/cloud/users' namespace + """ + + def __init__(self, uid, name, path, protocols, access, devices): + super().__init__() + self.id = uid + self.name = name + self.path = path + self.protocols = protocols + self.access = access + self.devices = devices + + @property + def absolute(self): + return f'/cloud/users/{self.path}' + + @staticmethod + def from_server_object(server_object): + return Share(server_object.id, server_object.name, server_object.display_path, server_object.protocol, + server_object.access, server_object.edge_filers) + + +class ShareAccessControlEntry(): + """ + Share access control entry for Edge Filer shares + + :ivar cterasdk.core.types.PortalAccount account: Domain User or Group + :ivar cterasdk.edge.enum.FileAccessMode perm: Permission + """ + + def __init__(self, account, perm): + AccessControlEntryValidator.validate_permission(perm) + self._account = account + self._perm = perm + + @property + def account(self): + return self._account + + @property + def principal_type(self): + return CollaboratorType.from_account(self._account) + + @property + def name(self): + return self._account.name + + @name.setter + def name(self, name): + self._account.name = name + + @property + def perm(self): + return self._perm + + @perm.setter + def perm(self, perm): + AccessControlEntryValidator.validate_permission(perm) + self._perm = perm + + def to_server_object(self): + ace = Object() + ace.permissions = self.perm + ace.manual_entry = Object() + ace.manual_entry.term = self.name + ace.manual_entry.type = self.principal_type + return ace + + @staticmethod + def from_server_object(server_object): + account = PortalAccount.from_collaborator(server_object.collaborator) + return ShareAccessControlEntry(account, server_object.permissions) + + +class NFSv3AccessControlEntry(): + """ + NFS v3 Access Control Entry + + :ivar str address: IP address, hostname or fully qualified domain name of client machine + :ivar str netmask: Subnet mask + :ivar cterasdk.edge.enum.FileAccessMode perm: File access permission + """ + + def __init__(self, address, netmask, perm): + AccessControlEntryValidator.validate_permission(perm) + self._address = address + self._netmask = netmask + self._perm = perm + + @property + def address(self): + return self._address + + @property + def netmask(self): + return self._netmask + + @property + def perm(self): + return self._perm + + @perm.setter + def perm(self, perm): + AccessControlEntryValidator.validate_permission(perm) + self._perm = perm + + @staticmethod + def from_server_object(server_object): + return NFSv3AccessControlEntry( + server_object.address, + server_object.netmask, + server_object.access_level, + ) + + def to_server_object(self): + param = Object() + param.access_level = self._perm + param.address = self._address + param.netmask = self._netmask + return param + + +class BlockRule: + """ + Block Extensions Rule for a Given Principal + """ + def __init__(self, account, extensions): + """ + :param cterasdk.core.types.PortalAccount account: Account + :param list[str]: File extensions. + """ + self.account = account + self.extensions = extensions + + @staticmethod + def default(): + param = Object() + param.screened_file_types = [] + param.collaborator = Object() + param.collaborator.name = 'Everyone' + param.collaborator.type = CollaboratorType.LG + return param + + def to_server_object(self): + param = Object() + param.collaborator = Object() + param.collaborator.domain = self.account.directory + param.collaborator.name = self.account.name + param.collaborator.type = CollaboratorType.from_account(self.account) + param.screened_file_types = [extension.lstrip('.') for extension in self.extensions] + return param diff --git a/cterasdk/core/users.py b/cterasdk/core/users.py index 5f2c811b..103d53de 100644 --- a/cterasdk/core/users.py +++ b/cterasdk/core/users.py @@ -50,30 +50,44 @@ def get(self, user_account, include=None): raise ObjectNotFoundException(baseurl) return user_object - def list_local_users(self, include=None): + def list_local_users(self, include=None, filters=None): """ List all local users - :param list[str] include: List of fields to retrieve, defaults to ['name'] + :param list[str],optional include: List of fields to retrieve, defaults to ['name'] :return: Iterator for all local users :rtype: cterasdk.lib.iterator.QueryIterator """ - include = union(include or [], Users.default) - param = query.QueryParamBuilder().include(include).build() - return query.iterator(self._core, '/users', param) + return self._users('/users', include, filters) - def list_domain_users(self, domain, include=None): + def list_domain_users(self, domain, include=None, filters=None): """ List all the users in the domain :param str domain: Domain name - :param list[str] include: List of fields to retrieve, defaults to ['name'] + :param list[str],optional include: List of fields to retrieve, defaults to ['name'] + :param list[],optional filters: List of additional filters, defaults to None :return: Iterator for all the domain users :rtype: cterasdk.lib.iterator.QueryIterator """ + return self._users(f'/domains/{domain}/adUsers', include, filters) + + def _users(self, path, include, filters): + """ + List Users. + + :param str path: Path + :param list[str],optional include: List of fields to retrieve, defaults to ['name'] + :param list[],optional filters: List of additional filters, defaults to None + """ include = union(include or [], Users.default) - param = query.QueryParamBuilder().include(include).build() - return query.iterator(self._core, f'/domains/{domain}/adUsers', param) + builder = query.QueryParamBuilder().include(include) + if filters: + for query_filter in filters: + builder.addFilter(query_filter) + builder.orFilter((len(filters) > 1)) + param = builder.build() + return query.iterator(self._core, path, param) def add(self, name, email, first_name, last_name, password, role, company=None, comment=None, password_change=False): """ diff --git a/cterasdk/edge/ransom_protect.py b/cterasdk/edge/ransom_protect.py index f9622e1a..8d836118 100644 --- a/cterasdk/edge/ransom_protect.py +++ b/cterasdk/edge/ransom_protect.py @@ -12,6 +12,10 @@ class RansomProtect(BaseCommand): Ransomware Protect APIs """ + def __init__(self, edge): + super().__init__(edge) + self.honeypot = Honeypot(self._edge) + def get_configuration(self): """ Get Ransom Protect Configuration @@ -72,3 +76,26 @@ def details(self, incident): param = query.QueryParamBuilder() param.put('incidentId', incident if isinstance(incident, int) else incident.incident_id) return query.iterator(self._edge, '/proc/rpsrv', param.build(), 'getIncidentDetails') + + +class Honeypot(BaseCommand): + + def enable(self): + """ + Enable Honeypot. + """ + self._edge.api.put('/config/ransomProtect/enableHoneypot', True) + + def is_enabled(self): + """ + Check if Honeypot is enabled. + + :rtype: bool + """ + return self._edge.api.get('/config/ransomProtect/enableHoneypot') + + def disable(self): + """ + Disable Honeypot. + """ + self._edge.api.put('/config/ransomProtect/enableHoneypot', False) diff --git a/cterasdk/edge/types.py b/cterasdk/edge/types.py index c3812cba..7814b2cb 100644 --- a/cterasdk/edge/types.py +++ b/cterasdk/edge/types.py @@ -204,8 +204,6 @@ class ShareAccessControlEntry(): :ivar cterasdk.edge.enum.FileAccessMode perm: The file access permission """ - _valid_permissions = list({k: v for k, v in enum.FileAccessMode.__dict__.items() if not k.startswith('_')}.values()) - def __init__(self, principal_type, name, perm): AccessControlEntryValidator.validate_permission(perm) self._user_group_entry = UserGroupEntry(principal_type, name) diff --git a/cterasdk/lib/__init__.py b/cterasdk/lib/__init__.py index 88982a8a..4fdf6a00 100644 --- a/cterasdk/lib/__init__.py +++ b/cterasdk/lib/__init__.py @@ -2,6 +2,6 @@ from .consent import ask # noqa: E402, F401 from .tempfile import TempfileServices # noqa: E402, F401 from .iterator import QueryIterator, BaseResponse, \ - DefaultResponse, KeyValueQueryIterator, QueryLogsResponse, CursorResponse # noqa: E402, F401 + DefaultResponse, v2DefaultResponse, KeyValueQueryIterator, QueryLogsResponse, CursorResponse # noqa: E402, F401 from .tracker import track, ErrorStatus # noqa: E402, F401 from .crypto import CryptoServices, X509Certificate, PrivateKey, create_certificate_chain # noqa: E402, F401 diff --git a/cterasdk/lib/iterator.py b/cterasdk/lib/iterator.py index 9bc8258f..fd34333b 100644 --- a/cterasdk/lib/iterator.py +++ b/cterasdk/lib/iterator.py @@ -82,6 +82,18 @@ def objects(self): return self._response.objects +class v2DefaultResponse(BaseResponse): + + @property + def more(self): + return self._response.data.has_next + + @property + @abstractmethod + def objects(self): + return self._response.data.items + + class QueryLogsResponse(DefaultResponse): @property diff --git a/cterasdk/objects/synchronous/core.py b/cterasdk/objects/synchronous/core.py index eb77bee3..602318f1 100644 --- a/cterasdk/objects/synchronous/core.py +++ b/cterasdk/objects/synchronous/core.py @@ -9,7 +9,7 @@ activation, admins, antivirus, buckets, cli, cloudfs, connection, credentials, devices, directoryservice, domains, files, firmwares, groups, kms, licenses, login, logs, mail, messaging, plans, portals, reports, roles, servers, settings, - setup, ssl, startup, storage_classes, syslog, tasks, templates, users, + setup, shares, ssl, startup, storage_classes, syslog, tasks, templates, users, ) @@ -18,6 +18,7 @@ class Clients: def __init__(self, core): self.ctera = core.default.clone(clients.Extended, EndpointBuilder.new(core.base, core.context)) self.api = core.default.clone(clients.API, EndpointBuilder.new(core.base, core.context, '/api')) + self.v2 = core.default.clone(clients.JSON, EndpointBuilder.new(core.base, core.context, '/v2')) self.io = IO(core) @@ -67,6 +68,7 @@ def __init__(self, host, port=None, https=True): self.reports = reports.Reports(self) self.roles = roles.Roles(self) self.settings = settings.Settings(self) + self.shares = shares.Shares(self) self.storage_classes = storage_classes.StorageClasses(self) self.tasks = tasks.Tasks(self) self.templates = templates.Templates(self) @@ -113,7 +115,7 @@ def public_info(self): def _omit_fields(self): return super()._omit_fields + ['activation', 'admins', 'backups', 'cloudfs', 'credentials', 'devices', 'directoryservice', 'domains', 'files', 'firmwares', 'groups', 'logs', 'plans', 'reports', 'roles', 'settings', - 'storage_classes', 'tasks', 'templates', 'users'] + 'shares', 'storage_classes', 'tasks', 'templates', 'users'] class GlobalAdmin(Portal): # pylint: disable=too-many-instance-attributes diff --git a/docs/source/UserGuides/Edge/Configuration.rst b/docs/source/UserGuides/Edge/Configuration.rst index 96eb5333..d695dca3 100644 --- a/docs/source/UserGuides/Edge/Configuration.rst +++ b/docs/source/UserGuides/Edge/Configuration.rst @@ -1141,6 +1141,15 @@ Ransomware Protection .. automethod:: cterasdk.edge.ransom_protect.RansomProtect.details :noindex: +.. automethod:: cterasdk.edge.ransom_protect.Honeypot.enable + :noindex: + +.. automethod:: cterasdk.edge.ransom_protect.Honeypot.is_enabled + :noindex: + +.. automethod:: cterasdk.edge.ransom_protect.Honeypot.disable + :noindex: + Mail Server =========== diff --git a/docs/source/UserGuides/Portal/Administration.rst b/docs/source/UserGuides/Portal/Administration.rst index 85234c71..2ae7701c 100644 --- a/docs/source/UserGuides/Portal/Administration.rst +++ b/docs/source/UserGuides/Portal/Administration.rst @@ -1542,6 +1542,55 @@ Code examples to create Cloud Drive folders using Fusion Direct admin.cloudfs.drives.setoacl(folders_paths, owner_sid, True) +Shares +------ + +.. automethod:: cterasdk.core.shares.Shares.all + :noindex: + + for share in admin.shares.all(): + print(share.name) + +.. automethod:: cterasdk.core.shares.Shares.get + :noindex: + +.. automethod:: cterasdk.core.shares.Shares.add + :noindex: + +.. code:: python + + arose = core_types.UserAccount('arose', 'ad.local') + support = core_types.GroupAccount('Support', 'ad.local') + + acl = [ + core_types.ShareAccessControlEntry(arose, 'ReadWrite'), + core_types.ShareAccessControlEntry(support, 'ReadOnly') + ] + + trusted_nfs_clients = [ + core_types.NFSv3AccessControlEntry('192.168.0.1', '255.255.255.0', 'ReadWrite'), + core_types.NFSv3AccessControlEntry('192.168.1.1', '255.255.0.0', 'ReadOnly') + ] + + block_rules = [ + core_types.BlockRule(arose, ['exe', 'bat', 'cmd']) + ] + + admin.shares.add('ProjectX', 'Service Account/Root/ProjectX', ['demo-edge1'], acl, + export_to_nfs=True, nfs_krb=True, trusted_nfs_clients=trusted_nfs_clients, block_files=block_rules) + +.. automethod:: cterasdk.core.shares.Shares.modify + :noindex: + +.. automethod:: cterasdk.core.shares.Shares.delete + :noindex: + + admin.shares.delete('05db1713-842e-4f7b-ac27-43787f10c695') + + for share in admin.shares.all(): + admin.shares.delete(share) + + Global File Locking ------------------- diff --git a/tests/ut/edge/test_ransom_protect.py b/tests/ut/edge/test_ransom_protect.py index 2eb84a5b..222394a6 100644 --- a/tests/ut/edge/test_ransom_protect.py +++ b/tests/ut/edge/test_ransom_protect.py @@ -71,6 +71,25 @@ def test_modify_raise(self): ransom_protect.RansomProtect(self._filer).modify() self.assertEqual('Ransom Protect must be enabled to modify its configuration', str(error.exception)) + def test_enable_honeypot(self): + put_response = 'success' + self._init_filer(put_response=put_response) + ransom_protect.Honeypot(self._filer).enable() + self._filer.api.put.assert_called_once_with('/config/ransomProtect/enableHoneypot', True) + + def test_disable_honeypot(self): + put_response = 'success' + self._init_filer(put_response=put_response) + ransom_protect.Honeypot(self._filer).disable() + self._filer.api.put.assert_called_once_with('/config/ransomProtect/enableHoneypot', False) + + def test_is_enabled_honeypot(self): + get_response = True + self._init_filer(get_response=get_response) + ret = ransom_protect.Honeypot(self._filer).is_enabled() + self._filer.api.get.assert_called_once_with('/config/ransomProtect/enableHoneypot') + self.assertEqual(ret, get_response) + @staticmethod def _get_ransom_protect_config(block_users=None, detection_threshold=None, detection_interval=None): obj = munch.Munch({