The project folders of the students are to be created on the file server FSX1 under D:\Projects.
To ensure maximum flexibility, a share Projects$ has to be created for \\FSX1\Projects.

ff

Creation of share Projects$

The $ at the end of the share name hides the share from people browsing.

The share permissions for the folder are changed to “Full Control”. On the one hand this ensures maximum flexibility, on the other hand the authorizations are restricted by NTFS permissions.

Groups

A shared resources (e.g. directories) do not give users individual permissions to access it, i.e. users are not entered individually in the security settings of a resource. Instead, for reasons of flexibility, only groups (more precisely: domain local groups) are granted access.

Global Groups

Global groups contain AD user accounts or other global groups located in the same domain.  Global groups are used to describe roles of their members, such as "Students" or "Teachers". They are known - that's why they are called global - to all resources within the overall structure.
However, resource security descriptors can only contain global groups of the same domain, so you cannot configure access to resources for members of global groups of other domains in the tree. Therefore, global groups are generally not used to assign rights and permissions - rather, a global group is made a member of a domain locale.

Domaine Local Groups

The resource permissions are only granted to local domain groups.
Local domain groups exist only within the respective domain and do not describe the role of their members, but the access rights and authorizations that they offer.  Therefore, as best practices, they do not have any users as members, but only other groups, such as global ones.

Nested Groups in AD

Example of nested groups with users

AGDLP (an abbreviation of "account, global, domain local, permission") briefly summarizes Microsoft's recommendations using nested groups in an Active Directory domain: User and computer accounts are members of global groups that represent business roles, which are members of domain local groups that describe resource permissions or user rights assignments.    

https://en.wikipedia.org/wiki/AGDLP        (18.10.2019)


1. Manual creation of Global Groups

Before programmatically creating network resources with appropriate groups and permissions, the following nested Global Groups should exist:

Global Group GL_SchoolUsers with nested global groups GL_Students and GL_Teachers

Global Group GL_SchoolUsers with nested global groups GL_Students and GL_Teachers

To find all groups easily, two OUs GL-Groups and DL-Groups are created, in which the global or domain local groups are stored. To find all groups easily, two OUs GL-Groups and DL-Groups are created, in which the global or domain local groups are stored.
For further identification, all global groups are assigned either the prefix GL_ or DL_.

Settings.py

Some additional settings are needed:

 1  
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Settings:
    domainName = "trainingX.net"
    dnSchool = "dc=trainingX, dc=net"                       # Distinguished Name of domaine
    dnStudents = "ou=Students, ou=SchoolUsers, " + dnSchool # Distinguished Name of OU Students

    dnGLGroups = "ou=GL-Groups, " + dnSchool                # Distinguished Name of OU GL- and DL-Groups
    dnDLGroups = "ou=DL-Groups, " + dnSchool
    
    nameFileserver = "FSX1"
    shareHomeStudents = "\\\\"+nameFileserver+"\\HomeStudents$"         # "\" must be escaped by another "\"
    homeDrive = "H:"
    dirProjectsStudents = "\\\\"+nameFileserver+"\\Projects$"           # student's projects base dir
    pathProjectsStudents = "D:\\Projects"
    projectDrive = "P:"


Fileserver.py

Class Fileserver has got four additional methods. One method each for creating and deleting the project directory for data exchange within a school class and one method each for creating and deleting the respective share on it:

 1  
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
from Settings import Settings
import os
import shutil
import win32security, win32netcon, win32net, ntsecuritycon, win32file

class Fileserver():
    def createHomeDirStudent(self, username):
        homeDir = Settings.shareHomeStudents + "\\" + username
        if os.path.exists(homeDir) == False:    # if home dir does not exist
            os.mkdir(homeDir)                   # make dir

            # How to set DACLs:
            # https://stackoverflow.com/questions/26465546/how-to-authorize-deny-write-access-to-a-directory-on-windows-using-python
            
            user, domain, type = win32security.LookupAccountName("", Settings.domainName + "\\" + username) # Find the SIDs for user of home dir
            sd = win32security.GetFileSecurity(homeDir, win32security.DACL_SECURITY_INFORMATION) # Find the DACL part of the Security Descriptor for the folder
            
            dacl = sd.GetSecurityDescriptorDacl()                                                # get security descriptor DACL 
            dacl.AddAccessAllowedAceEx(win32security.ACL_REVISION, 3, 2032127, user)             # add full control to folder, subfolder and files ACE to DACL

            sd.SetSecurityDescriptorDacl(1, dacl, 0)                                             # Put new (extended) DACL into the Security Descriptor,
            win32security.SetFileSecurity(homeDir, win32security.DACL_SECURITY_INFORMATION, sd)  # update the folder with the updated Security Descriptor
            

    def deleteHomeDirStudent(self, username):
        homeDir = Settings.shareHomeStudents + "\\" + username
        if os.path.exists(homeDir) == True:             # if home dir exists
            #os.system('rmdir /S /Q "{}"'.format(homeDir))
            shutil.rmtree(homeDir, ignore_errors=True)  # remove dir with content recursively            

    def createProjectDirStudent(self, nameSchoolClass):
        projectDir = Settings.dirProjectsStudents + "\\" + nameSchoolClass
        if os.path.exists(projectDir) == False:    # if project dir does not exist
            os.mkdir(projectDir)                   # make dir

            # Find the SID of group which uses project dir
            group, domain, type = win32security.LookupAccountName("", Settings.domainName + "\\DL_" + nameSchoolClass + "_F")
            # Find the DACL part of the Security Descriptor for the folder
            sd = win32security.GetFileSecurity(projectDir, win32security.DACL_SECURITY_INFORMATION) 
            # get security descriptor DACL 
            dacl = sd.GetSecurityDescriptorDacl()
            # add full access of group to folder, subfolder and files ACE to DACL
            dacl.AddAccessAllowedAceEx(win32security.ACL_REVISION, 3, 2032127, group)   
            # put new (extended) DACL into the Security Descriptor
            sd.SetSecurityDescriptorDacl(1, dacl, 0)
            # update the folder with the updated Security Descriptor
            win32security.SetFileSecurity(projectDir, win32security.DACL_SECURITY_INFORMATION, sd)  

    def deleteProjectDirStudent(self, nameSchoolClass):
        projectDir = Settings.dirProjectsStudents + "\\" + nameSchoolClass
        if os.path.exists(projectDir) == True:             # if project dir exists
            shutil.rmtree(projectDir, ignore_errors=True)  # remove dir with content recursively         

    def addShareProjectDirStudent(self, nameSchoolClass):
        sd = win32security.SECURITY_DESCRIPTOR()
        # get the "well known" SID for the administrators group
        subAuths = ntsecuritycon.SECURITY_BUILTIN_DOMAIN_RID, ntsecuritycon.DOMAIN_ALIAS_RID_ADMINS
        sidAdmins = win32security.SID(ntsecuritycon.SECURITY_NT_AUTHORITY, subAuths)
        # Find the SID of dl group which uses share to project dir
        group, domain, type = win32security.LookupAccountName("", Settings.domainName + "\\DL_" + nameSchoolClass + "_F")
        # Set ACL, giving DL group of school class and admin full access
        dacl = win32security.ACL(128)
        dacl.AddAccessAllowedAce(win32file.FILE_ALL_ACCESS, sidAdmins)
        dacl.AddAccessAllowedAce(win32file.FILE_ALL_ACCESS, group)
        sd.SetSecurityDescriptorDacl(1, dacl, 0)
        
        sharename = nameSchoolClass + "$"   # hide share
        shinfo={}                           # shinfo struct
        shinfo['netname'] = sharename
        shinfo['type'] = win32netcon.STYPE_DISKTREE
        shinfo['remark'] = 'Project share for schoolclass %s' % (sharename,)
        shinfo['permissions'] = 0
        shinfo['max_uses'] = -1             # unlimited users
        shinfo['security_descriptor'] = sd
        shinfo['current_uses'] = 0
        shinfo['path'] = Settings.pathProjectsStudents + "\\" + nameSchoolClass
        shinfo['passwd'] = ''
        win32net.NetShareAdd("\\\\" + Settings.nameFileserver,502,shinfo) # add share
        
    def deleteShareProjectDirStudent(self, nameSchoolClass):
        win32net.NetShareDel("\\\\" + Settings.nameFileserver, nameSchoolClass + "$")



Ldap.py

In the createSchoolClass() method, the necessary groups, the project directory and the share of a school class are now additionally created and linked and in the method createStudent() this user is added to the respective global group of its school class.

deleteSchoolClass() clears everything up again:

  1  
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
from abc import ABC, abstractmethod     # import Abstract Base Classes (ABCs)

class AbstractLDAP(ABC):
 
    def __init__(self, dnStudents):
        self._dnStudents = dnStudents
        super().__init__()
    
    @abstractmethod
    def createStudent(self, username, nameSchoolClass):
        pass
    @abstractmethod
    def deleteStudent(self, username, nameSchoolClass):
        pass
    @abstractmethod
    def createSchoolClass(self, nameSchoolClass):
        pass
    @abstractmethod
    def deleteSchoolClass(self, nameSchoolClass):
        pass

from pyad import *
from Settings import Settings
from Fileserver import *

class PyAD(AbstractLDAP):
    def createStudent(self, username, surname, firstname, password, dateOfBirth, nameSchoolClass):
        query = adquery.ADQuery()
        cnUnique = False
        numChar = 0
        while(cnUnique == False):
            query.execute_query(attributes = ["cn"],            # query: does cn already exist
                where_clause=("cn = '" + username + "'"),
                base_dn = Settings.dnSchool)
            if query.get_row_count() > 0:                       # yes: add character
                numChar = numChar + 1
                if(numChar <= len(firstname)):                  # if the character set of the first name is large enough
                    username = surname + firstname[:numChar]    # add number of characters from beginning of first ame
                else:                                           # if the character set of the first name is used up
                    username = surname + firstname + (numChar - len(firstname))  * 'x'  # added number of 'x'
            else:                                               # cn is unique => create user
                cnUnique = True
                ouSchoolClass = pyad.adcontainer.ADContainer("ou="+nameSchoolClass+", "+self._dnStudents, adsi_ldap_com_object=None, options={})
                student = aduser.ADUser.create(username
                                     , ouSchoolClass
                                     , password=password
                                     , upn_suffix=None
                                     , enable=True
                                     , optional_attributes={"sn":surname
                                                            , "givenName":firstname
                                                            , "employeeID":dateOfBirth  # using existing employeeId attribute is easier than extending AD scheme
                                                            , "description":"Student"
                                                            , "pwdLastSet":0            # password must be changed on first login
                                                            , "homeDrive":Settings.homeDrive                                   # set home drive
                                                            , "homeDirectory":Settings.shareHomeStudents + "\\" + username})   # set home directory
                # put new student in global group of its school class
                gl_groupSchoolClass = pyad.from_dn("cn=GL_" + nameSchoolClass + ", " + Settings.dnGLGroups)   
                gl_groupSchoolClass.add_members([student])           
        return username    # return possibly modified username     

    def deleteStudent(self, surname, firstname, dateOfBirth, nameSchoolClass):
        # print("delete ", surname, firstname, dateOfBirth, nameSchoolClass)
        dnSchoolClass = "ou="+nameSchoolClass + ", " + self._dnStudents
        query = adquery.ADQuery()
        query.execute_query(attributes = ["cn"],
                where_clause=("sn = '" + surname + "' and givenName = '" + firstname + "' and employeeID = '" + dateOfBirth + "'"),
                base_dn = dnSchoolClass)
        if query.get_row_count() >= 1:                              # if student exists => delete it
            ouSchoolClass = pyad.adcontainer.ADContainer(dnSchoolClass, adsi_ldap_com_object=None, options={})
            fs = Fileserver()
            for row in query.get_results():
                ouSchoolClass.from_cn(row["cn"]).delete()
                fs.deleteHomeDirStudent(row["cn"])
   
    def createSchoolClass(self, nameSchoolClass):
        query = adquery.ADQuery()
        query.execute_query(attributes = ["distinguishedName"],
                where_clause=("ou = '" +nameSchoolClass + "'"),
                base_dn = self._dnStudents)
        if query.get_row_count() == 0:                              # if school class not yet exists => create
            ouStudents = pyad.adcontainer.ADContainer(self._dnStudents, adsi_ldap_com_object=None, options={})
            ouStudents.create_container(nameSchoolClass)
            # create a new global group for students of school class
            ouGLgroups = pyad.adcontainer.ADContainer.from_dn(Settings.dnGLGroups)
            gl_groupSchoolClass = ouGLgroups.create_group("GL_" + nameSchoolClass
                                                 , security_enabled=True
                                                 , scope='GLOBAL'
                                                 , optional_attributes = {"description":"Students of school class " + nameSchoolClass})
            # create a new domaine local for project dir of students
            ouDLgroups = pyad.adcontainer.ADContainer.from_dn(Settings.dnDLGroups)
            dl_groupSchoolClass = ouDLgroups.create_group("DL_" + nameSchoolClass + "_F" 
                                            , security_enabled=True
                                            , scope='LOCAL'
                                            , optional_attributes = {"description":"Full access for project dir of students of school class " + nameSchoolClass})
            
            gl_groupStudents = pyad.from_dn("cn=GL_Students, " + Settings.dnGLGroups) # put global group of school class
            gl_groupStudents.add_members([gl_groupSchoolClass])             # into global group of all students "GL_Students" and
            dl_groupSchoolClass.add_members([gl_groupSchoolClass])          # into domaine local group of students of a school class
            # create folder and share
            fs = Fileserver()
            fs.createProjectDirStudent(nameSchoolClass)
            fs.addShareProjectDirStudent(nameSchoolClass)
 
    def deleteSchoolClass(self, nameSchoolClass):
        dnSchoolClass = "ou="+nameSchoolClass+", "+ self._dnStudents
        query = adquery.ADQuery()
        query.execute_query(attributes = ["distinguishedName"],
                where_clause=("ou = '" +nameSchoolClass + "'"),
                base_dn = self._dnStudents)
        if query.get_row_count() == 1:                          # if school class exists
            query = adquery.ADQuery()
            query.execute_query(attributes = ["sn", "givenName", "employeeID"],  # employeeID holds date of birth
                where_clause=("cn = '*'"),
                base_dn = dnSchoolClass)
            if query.get_row_count() > 0:                       # if school class not empty 
                for row in query.get_results():                 # delete students
                    self.deleteStudent(row["sn"], row["givenName"], row["employeeID"],  nameSchoolClass)
            ouSchoolClass = pyad.adcontainer.ADContainer(dnSchoolClass, adsi_ldap_com_object=None, options={})
            ouSchoolClass.delete()                              # delete school class ou
            # delete global and domaine local groups of school class
            gl_groupSchoolClass = pyad.from_dn("cn=GL_" + nameSchoolClass + ", " + Settings.dnGLGroups) 
            gl_groupSchoolClass.delete()
            dl_groupSchoolClass = pyad.from_dn("cn=DL_" + nameSchoolClass + "_F" + ", " + Settings.dnDLGroups) 
            dl_groupSchoolClass.delete()
            # delete folder and share
            fs = Fileserver()
            fs.deleteProjectDirStudent(nameSchoolClass)
            fs.deleteShareProjectDirStudent(nameSchoolClass)


The above listed code in the method createSchoolClass()

  1. creates a global group GL_nameOfSchoolClass in the ou GL-Groups,
  2. puts this global group of school class into the global group GL_Students,
  3. creates a domain local group DL_nameOfSchoolClass in the ou DL-Groups,
  4. puts the global group GL_nameOfSchoolClass into the domain local group DL_nameOfSchoolClass
  5. creates a project directory \\FSX1\Projects\nameOfSchoolClass on the fileserver and
  6. creates a share \\FSX1\nameOfSchoolClass$ on the fileserver, which gets the appropriate permissions.

testExchangeDrive.py

demonstrates the use of the classes:

 1  
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from School import *                        
from User import *

schoolClass = SchoolClass("school_class1",[])         
schoolClass2 = SchoolClass("school_class2",[])       
student = Student("Miller", "Sam", "11.11.2000", schoolClass)
student = Student("Meyer", "Sam", "11.11.2000", schoolClass2)

schoolClass.addToLDAP()
schoolClass2.addToLDAP()

#schoolClass.deleteFromLDAP()
#schoolClass2.deleteFromLDAP()




studentsLogon.vbs

Logon VBScript for mapping exchange drive P: to project directory

Mapping the drive of the programmatically created share of the project directory of the school class itself is not a problem (see last command below).
The main problem is to find the school class name of a student, because the share is called like the school class. For this purpose:

  1. All groups in which the student is located are determined.
  2. The list of groups is run through and the prefix GL_ is searched in the name of the groups until the global group of the school class GL_NameOfSchoolClass where the logging student is located is found.
  3. The prefix GL_ is removed, resulting in the name of the share.
 1  
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
'********************************************************************
'Login script for students:    studentsLogon.vbs
'Version 1.0		
'Wolfhard Eick	10/2019


'--------------------------------------------------------------------
'Settings
'
fileserverStudents = "FSX1"						'File server of students
domain = "trainingX.net"


'--------------------------------------------------------------------
'get school class of student
'
Set WshNetwork = WScript.CreateObject("WScript.Network")

Do While wshNetwork.username = "" 					'wait until username of student
   WScript.Sleep 250 								'is available 
Loop 

username =  WshNetwork.UserName						'get actual username 
Set groups = GetObject("WinNT://" & domain & "/" & username)  		'get group memberships of student

For Each group In groups.Groups						'iterate over groups
	If left(group.Name,3) = "GL_" Then				'get gl-group of school class
		strLength = len(group.Name)
		schoolclass = mid(group.Name,4,strLength)	'extract name of school class
	end if
Next

'MsgBox username & " is a member of school class " & schoolclass, vbOkOnly, "Membership of school class"



'--------------------------------------------------------------------
'map drive
'
On Error Resume Next 
wshNetwork.RemoveNetworkDrive "P:", True, True 				'delete an eventually existing mapping

wshNetwork.MapNetworkDrive "P:", "\\" & fileserverStudents & "\" & schoolclass & "$"	'map drive



Configure the GPO for Windows logon scripts to run

  1. Open the Group Policy Management console
  2. From the Server Manager, navigate to Tools > Group Policy Management.
  3. Expand the Domains tree, right-click a OU Students, and select "Create a GPO in this domain and Link it here ..."
  4. In the New GPO dialog box, give the GPO the descriptive name StudentsLogonPolicy.
  5. Locate the new GPO in the Domains tree (under the domain or OU that you selected above), right-click it, and select "Edit".
  6. If a pop-up message appears when you click on the GPO name, click OK.
  7. In the Group Policy Management Editor, navigate to User Configuration > Policies > Windows Settings > Scripts (Logon/Logoff), then double-click "Logon in the right pane".
  8. In the Logon Properties window, click "Show Files".
  9. A folder whose name ends in "User\Scripts\Logon\" is displayed.
  10. Copy your logon script files into this folder.
  11. In the Logon Properties window, click Add.

    GPO for running logon script of students

  12. Click "Browse to open the logon script directory", then select your logon script file and click "OK".
  13. Verify that the logon script now appears in the list on the Logon Properties window.
  14. Close the Group Policy Management Editor window for your GPO, then close the Group Policy Management window.

Source: https://www.websense.com/content/support/library/web/v78/logon_agent/la_configure_scripts.aspx    (21.10.2019)


The administrator can access Miller's home directory via the projects$ share:

Project directory of school classes

Project directorie of school classes




Last modified: Sunday, 16 April 2023, 9:22 PM