Skip to content

Commit 8fba266

Browse files
authored
Support Portal Active Directory conn. mgmt. (#132)
* Support Portal Active Directory conn. mgmt.
1 parent b842c2f commit 8fba266

File tree

5 files changed

+320
-10
lines changed

5 files changed

+320
-10
lines changed

cterasdk/core/directoryservice.py

Lines changed: 172 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,178 @@
33
from .base_command import BaseCommand
44
from ..common import Object
55
from ..exception import CTERAException
6-
from .enum import PortalAccountType, SearchType
6+
from .enum import PortalAccountType, SearchType, DirectoryServiceType, DirectoryServiceFetchMode, Role, DirectorySearchEntityType
7+
from .types import AccessControlEntry, AccessControlRule, UserAccount, GroupAccount
78

89

910
class DirectoryService(BaseCommand):
1011
"""
1112
Portal Active Directory APIs
1213
"""
1314

15+
def _get_configuration(self):
16+
return self._portal.get('/directoryConnector')
17+
18+
def connected(self):
19+
directory_services_config = self._get_configuration()
20+
if directory_services_config is None:
21+
return False
22+
try:
23+
self._connect_to_directory_services(directory_services_config)
24+
return True
25+
except CTERAException:
26+
return False
27+
28+
# pylint: disable=too-many-arguments
29+
def connect(self, domain, username, password, directory=DirectoryServiceType.Microsoft,
30+
domain_controllers=None, ou=None, ssl=False, krb=False, mapping=None, acl=None,
31+
default=Role.Disabled, fetch=DirectoryServiceFetchMode.Lazy):
32+
"""
33+
Connect a Portal tenant to directory services
34+
35+
:param str domain: The directory services domain to connect to
36+
:param str username: The user name to use when connecting to the active directory services
37+
:param str password: The password to use when connecting to the active directory services
38+
:param str,optional ou: The OU path to use when connecting to the active directory services, defaults to `None`
39+
:param cterasdk.core.enum.DirectoryServiceType,optional directory: The directory service type, deafults to `'ActiveDirectory'`
40+
:param cterasdk.core.types.DomainControllers,optional domain_controllers:
41+
Connect to a primary and a secondary domain controllers, defaults to `None`
42+
:param bool,optional ssl: Connect using SSL, defaults to `False`
43+
:param bool,optional krb: Connect using Kerberos, defaults to `False`
44+
:param list[cterasdk.core.types.ADDomainIDMapping],optional: The directory services UID/GID mapping
45+
:param list[cterasdk.core.types.AccessControlEntry],optional acl: List of access control entries and their associated roles
46+
:param cterasdk.core.enum.Role default: Default role if no match applies, defaults to `None`
47+
:param str,optional fetch: Configure identity fetching mode, defaults to `'Lazy'`
48+
"""
49+
param = Object()
50+
param._classname = 'ActiveDirectory' # pylint: disable=protected-access
51+
param.type = directory
52+
param.domain = domain
53+
param.useKerberos = krb
54+
param.useSSL = ssl
55+
param.username = username
56+
param.password = password
57+
param.ou = ou
58+
param.noMatchRole = default
59+
param.accessControlRules = None
60+
param.idMapping = None
61+
param.fetchMode = fetch
62+
63+
if domain_controllers:
64+
param.ipAddresses = Object()
65+
param.ipAddresses._classname = 'DomainControlIPAddresses' # pylint: disable=protected-access
66+
param.ipAddresses.ipAddress1 = domain_controllers.primary
67+
param.ipAddresses.ipAddress2 = domain_controllers.secondary
68+
69+
tenant = self._portal.session().user.tenant
70+
logging.getLogger().info('Connecting Portal to directory services. %s', {
71+
'tenant': tenant,
72+
'type': type,
73+
'domain': domain
74+
})
75+
self._connect_to_directory_services(param)
76+
logging.getLogger().info('Connected Portal to directory services. %s', {'tennat': tenant, 'domain': domain})
77+
78+
if mapping:
79+
self._configure_advanced_mapping(mapping)
80+
81+
if acl:
82+
self._configure_access_control(acl, default)
83+
84+
def _connect_to_directory_services(self, param):
85+
return self._portal.execute('', 'testAndSaveAD', param)
86+
87+
def get_advanced_mapping(self):
88+
"""
89+
Retrieve directory services advanced mapping configuration
90+
91+
:returns: A dictionary of domain mapping objects
92+
:rtype: dict
93+
"""
94+
return {map.domainFlatName: map for map in self._portal.get('/directoryConnector/idMapping/map')}
95+
96+
def set_advanced_mapping(self, mapping):
97+
"""
98+
Configure advanced mapping
99+
100+
:param list[cterasdk.core.types.ADDomainIDMapping] mapping: The directory services UID/GID mapping
101+
"""
102+
if self._get_configuration() is None:
103+
raise CTERAException('Failed to apply mapping. Not connected to directory services.')
104+
105+
return self._configure_advanced_mapping(mapping)
106+
107+
def _configure_advanced_mapping(self, mapping):
108+
param = Object()
109+
param._classname = 'ADIDMapping' # pylint: disable=protected-access
110+
param.map = mapping
111+
logging.getLogger().debug('Updating advanced mapping. %s', {
112+
'domains': [mapping.domainFlatName for mapping in param.map]
113+
})
114+
response = self._portal.put('/directoryConnector/idMapping', param)
115+
logging.getLogger().info('Updated advanced mapping.')
116+
return response
117+
118+
def get_access_control(self):
119+
"""
120+
Retrieve directory services access control list
121+
122+
:returns: List of access control entries
123+
:rtype: list[cterasdk.core.types.AccessControlEntry]
124+
"""
125+
acl = []
126+
for ace in self._portal.get('/directoryConnector/accessControlRules'):
127+
if ace.group.type == DirectorySearchEntityType.User:
128+
acl.append(AccessControlEntry(UserAccount(ace.group.name, ace.group.domain), ace.role))
129+
elif ace.group.type == DirectorySearchEntityType.Group:
130+
acl.append(AccessControlEntry(GroupAccount(ace.group.name, ace.group.domain), ace.role))
131+
return acl
132+
133+
def set_access_control(self, acl=None, default=None):
134+
"""
135+
Configure directory services access control
136+
137+
:param list[cterasdk.core.types.AccessControlEntry],optional acl:
138+
List of access control entries and their associated roles
139+
:param cterasdk.core.enum.Role default: Default role if no match applies, defaults to `None`
140+
"""
141+
directory_services_config = self._get_configuration()
142+
if directory_services_config is None:
143+
raise CTERAException('Failed to apply access control. Not connected to directory services.')
144+
145+
default = default if default is not None else directory_services_config.noMatchRole
146+
return self._configure_access_control(acl, default)
147+
148+
def _configure_access_control(self, acl, default=None):
149+
150+
access_control_rules = list()
151+
for ace in acl:
152+
account = None
153+
if ace.account.account_type == PortalAccountType.User:
154+
account = self._search_users(ace.account.directory, ace.account.name)
155+
elif ace.account.account_type == PortalAccountType.Group:
156+
account = self._search_groups(ace.account.directory, ace.account.name)
157+
access_control_rules.append(AccessControlRule(account, ace.role))
158+
159+
logging.getLogger().info('Updating access control rules.')
160+
response = self._portal.put('/directoryConnector/accessControlRules', access_control_rules)
161+
logging.getLogger().info('Updated access control rules.')
162+
163+
if default is not None:
164+
logging.getLogger().info('Updating default role.')
165+
response = self._portal.put('/directoryConnector/noMatchRole', default)
166+
logging.getLogger().info('Updated default role')
167+
168+
return response
169+
170+
def domains(self):
171+
"""
172+
Get domains
173+
174+
:return list(str): List of names of all discovered domains
175+
"""
176+
return self._portal.execute('', 'getADTrustedDomains', False)
177+
14178
def fetch(self, active_directory_accounts):
15179
"""
16180
Instruct the Portal to fetch the provided Active Directory Accounts
@@ -39,9 +203,7 @@ def fetch(self, active_directory_accounts):
39203
param.append(self._search_groups(active_directory_account.directory, active_directory_account.name))
40204

41205
logging.getLogger().info('Starting to fetch users and groups.')
42-
43206
response = self._portal.execute('', 'syncAD', param)
44-
45207
logging.getLogger().info('Started fetching users and groups.')
46208

47209
return response
@@ -80,9 +242,15 @@ def _search_directory_services(self, search_type, domain, name):
80242
return principal
81243

82244
raise CTERAException(
83-
'Search returned multiple results, but none that match your search criteria',
245+
'Search returned multiple results, but none matches your search criteria',
84246
None,
85247
domain=domain,
86248
type=search_type,
87249
account=name
88250
)
251+
252+
def disconnect(self):
253+
"""
254+
Disconnect a Portal tenant from directory services
255+
"""
256+
return self._portal.put('/directoryConnector', None)

cterasdk/core/enum.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,3 +504,38 @@ class Mode:
504504
"""
505505
Enabled = "enabled"
506506
Disabled = "disabled"
507+
508+
509+
class DirectoryServiceType:
510+
"""
511+
Directory Service Type
512+
513+
:ivar str Microsoft: Active Directory
514+
:ivar str LDAP: LDAP
515+
:ivar str Apple: Apple Open Directory
516+
"""
517+
Microsoft = 'ActiveDirectory'
518+
LDAP = 'LDAP'
519+
Apple = 'AppleOpenDirectory'
520+
521+
522+
class DirectoryServiceFetchMode:
523+
"""
524+
Directory Service Fetch Mode
525+
526+
:ivar str Eager: Eager
527+
:ivar str Lazy: Lazy
528+
"""
529+
Eager = 'Eager'
530+
Lazy = 'Lazy'
531+
532+
533+
class DirectorySearchEntityType:
534+
"""
535+
Directory Search Entity Type
536+
537+
:ivar str User: User
538+
:ivar str Group: Group
539+
"""
540+
User = 'user'
541+
Group = 'group'

cterasdk/core/types.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
PlatformVersion.version.__doc__ = 'The version identifier'
1818

1919

20+
AccessControlEntry = namedtuple('AccessControlEntry', ('account', 'role'))
21+
AccessControlEntry.__doc__ = 'Tuple holding a Portal account and its respective permission'
22+
AccessControlEntry.account.__doc__ = 'The Portal group or user account'
23+
AccessControlEntry.role.__doc__ = 'The group or user role'
24+
25+
2026
class PortalAccount(ABC):
2127
"""
2228
Base Class for Portal Account
@@ -393,3 +399,41 @@ def to_server_object(self):
393399
param.httpsOnly = self.https
394400
param.directUpload = self.direct
395401
return param
402+
403+
404+
class DomainControllers:
405+
406+
def __init__(self, primary=None, secondary=None):
407+
self._primary = primary
408+
self._secondary = secondary
409+
410+
@property
411+
def primary(self):
412+
return self._primary
413+
414+
@property
415+
def secondary(self):
416+
return self._secondary
417+
418+
419+
class AccessControlRule(Object):
420+
421+
def __init__(self, group, role):
422+
self._classname = 'AccessControlRule'
423+
self.group = group
424+
self.role = role
425+
426+
427+
class ADDomainIDMapping(Object):
428+
"""
429+
Base Class for Directory Service ID Mapping
430+
431+
:ivar str domain: The domain flat name
432+
:param int start: The minimum id to use for mapping
433+
:param int end: The maximum id to use for mapping
434+
"""
435+
def __init__(self, domain, start, end):
436+
self._classname = 'ADDomainIDMapping'
437+
self.domainFlatName = domain
438+
self.minID = start
439+
self.maxID = end

cterasdk/edge/directoryservice.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ def advanced_mapping(self, domain, start, end):
9292
Configure advanced mapping
9393
9494
:param str domain: The active directory domain
95-
:param str start: The minimum id to use for mapping
96-
:param str end: The maximum id to use for mapping
95+
:param int start: The minimum id to use for mapping
96+
:param int end: The maximum id to use for mapping
9797
"""
9898
mappings = self._gateway.get('/config/fileservices/cifs/idMapping/map')
9999
for mapping in mappings:

0 commit comments

Comments
 (0)