├── .gitignore ├── CreateUserPkg.icns ├── CreateUserPkg.xcodeproj └── project.pbxproj ├── CreateUserPkg ├── CUPAppDelegate.h ├── CUPAppDelegate.m ├── CUPBestHostFinder.h ├── CUPBestHostFinder.m ├── CUPDocument.h ├── CUPDocument.m ├── CUPImageSelector.h ├── CUPImageSelector.m ├── CUPUIDFormatter.h ├── CUPUIDFormatter.m ├── CUPUUIDFormatter.h ├── CUPUUIDFormatter.m ├── CUPUserNameFormatter.h ├── CUPUserNameFormatter.m ├── CreateUserPkg-Info.plist ├── CreateUserPkg-Prefix.pch ├── CreateUserPkg.entitlements ├── create_package.py ├── dropimagehere.png ├── en.lproj │ ├── CUPDocument.xib │ ├── Credits.rtf │ ├── InfoPlist.strings │ └── MainMenu.xib ├── main.m └── read_package.py ├── Distributions └── create_dmg.sh ├── Icons ├── Add-User-Icon-16.png ├── Add-User-Icon-32.png ├── dropimagehere.psd └── psd-web-user-icons.psd ├── README.css ├── README.markdown └── Test ├── c64breadbox.png ├── test_icon.eps ├── test_icon.jpg ├── test_icon.pdf ├── test_icon.png ├── test_icon.tiff └── test_package.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.pbxuser 3 | *.mode1v3 4 | *.mode2v3 5 | *.perspectivev3 6 | *.xcuserstate 7 | project.xcworkspace/ 8 | xcuserdata/ 9 | Distributions/*.dmg 10 | Distributions/CreateUserPkg-* 11 | Screenshots/ 12 | 13 | -------------------------------------------------------------------------------- /CreateUserPkg.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/CreateUserPkg.icns -------------------------------------------------------------------------------- /CreateUserPkg.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 05524F8D159B314900741F47 /* create_package.py in Resources */ = {isa = PBXBuildFile; fileRef = 05524F8C159B314900741F47 /* create_package.py */; }; 11 | 05524F94159B33AB00741F47 /* read_package.py in Resources */ = {isa = PBXBuildFile; fileRef = 05524F8F159B32D600741F47 /* read_package.py */; }; 12 | 0596D17D159AF27800B898B4 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0596D17C159AF27800B898B4 /* Cocoa.framework */; }; 13 | 0596D187159AF27800B898B4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0596D185159AF27800B898B4 /* InfoPlist.strings */; }; 14 | 0596D189159AF27800B898B4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0596D188159AF27800B898B4 /* main.m */; }; 15 | 0596D18D159AF27800B898B4 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 0596D18B159AF27800B898B4 /* Credits.rtf */; }; 16 | 0596D190159AF27800B898B4 /* CUPDocument.m in Sources */ = {isa = PBXBuildFile; fileRef = 0596D18F159AF27800B898B4 /* CUPDocument.m */; }; 17 | 0596D193159AF27800B898B4 /* CUPDocument.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0596D191159AF27800B898B4 /* CUPDocument.xib */; }; 18 | 0596D196159AF27800B898B4 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0596D194159AF27800B898B4 /* MainMenu.xib */; }; 19 | 05CF2A17159DE745004F36C8 /* CUPImageSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = 05CF2A16159DE745004F36C8 /* CUPImageSelector.m */; }; 20 | 05CF2A1A159DEDE4004F36C8 /* dropimagehere.png in Resources */ = {isa = PBXBuildFile; fileRef = 05CF2A19159DEDE4004F36C8 /* dropimagehere.png */; }; 21 | 6605E68A159B86480048362A /* CUPBestHostFinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 6605E689159B86480048362A /* CUPBestHostFinder.m */; }; 22 | 6605E68E159B90070048362A /* CUPAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6605E68D159B90070048362A /* CUPAppDelegate.m */; }; 23 | 6605E695159B964D0048362A /* CUPUIDFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 6605E690159B964D0048362A /* CUPUIDFormatter.m */; }; 24 | 6605E696159B964D0048362A /* CUPUserNameFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 6605E692159B964D0048362A /* CUPUserNameFormatter.m */; }; 25 | 6605E697159B964D0048362A /* CUPUUIDFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 6605E694159B964D0048362A /* CUPUUIDFormatter.m */; }; 26 | 66B221D8159C4C30003FDF4E /* CreateUserPkg.icns in Resources */ = {isa = PBXBuildFile; fileRef = 66B221D7159C4C30003FDF4E /* CreateUserPkg.icns */; }; 27 | C09EAB201EF8EDFE005A8859 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C09EAB1F1EF8EDFE005A8859 /* Security.framework */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 05524F8C159B314900741F47 /* create_package.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = create_package.py; sourceTree = ""; }; 32 | 05524F8F159B32D600741F47 /* read_package.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = read_package.py; sourceTree = ""; }; 33 | 0596D178159AF27800B898B4 /* CreateUserPkg.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CreateUserPkg.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 0596D17C159AF27800B898B4 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 35 | 0596D17F159AF27800B898B4 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 36 | 0596D180159AF27800B898B4 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 37 | 0596D181159AF27800B898B4 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 38 | 0596D184159AF27800B898B4 /* CreateUserPkg-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "CreateUserPkg-Info.plist"; sourceTree = ""; }; 39 | 0596D186159AF27800B898B4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 40 | 0596D188159AF27800B898B4 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 41 | 0596D18A159AF27800B898B4 /* CreateUserPkg-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CreateUserPkg-Prefix.pch"; sourceTree = ""; }; 42 | 0596D18C159AF27800B898B4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 43 | 0596D18E159AF27800B898B4 /* CUPDocument.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CUPDocument.h; sourceTree = ""; }; 44 | 0596D18F159AF27800B898B4 /* CUPDocument.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CUPDocument.m; sourceTree = ""; }; 45 | 0596D192159AF27800B898B4 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/CUPDocument.xib; sourceTree = ""; }; 46 | 0596D195159AF27800B898B4 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; 47 | 0596D19C159AF2A200B898B4 /* CreateUserPkg.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = CreateUserPkg.entitlements; sourceTree = ""; }; 48 | 05CF2A15159DE745004F36C8 /* CUPImageSelector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CUPImageSelector.h; sourceTree = ""; }; 49 | 05CF2A16159DE745004F36C8 /* CUPImageSelector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CUPImageSelector.m; sourceTree = ""; }; 50 | 05CF2A19159DEDE4004F36C8 /* dropimagehere.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dropimagehere.png; sourceTree = ""; }; 51 | 6605E688159B86480048362A /* CUPBestHostFinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CUPBestHostFinder.h; sourceTree = ""; }; 52 | 6605E689159B86480048362A /* CUPBestHostFinder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CUPBestHostFinder.m; sourceTree = ""; }; 53 | 6605E68C159B90070048362A /* CUPAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CUPAppDelegate.h; sourceTree = ""; }; 54 | 6605E68D159B90070048362A /* CUPAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CUPAppDelegate.m; sourceTree = ""; }; 55 | 6605E68F159B964D0048362A /* CUPUIDFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CUPUIDFormatter.h; sourceTree = ""; }; 56 | 6605E690159B964D0048362A /* CUPUIDFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CUPUIDFormatter.m; sourceTree = ""; }; 57 | 6605E691159B964D0048362A /* CUPUserNameFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CUPUserNameFormatter.h; sourceTree = ""; }; 58 | 6605E692159B964D0048362A /* CUPUserNameFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CUPUserNameFormatter.m; sourceTree = ""; }; 59 | 6605E693159B964D0048362A /* CUPUUIDFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CUPUUIDFormatter.h; sourceTree = ""; }; 60 | 6605E694159B964D0048362A /* CUPUUIDFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CUPUUIDFormatter.m; sourceTree = ""; }; 61 | 66B221D7159C4C30003FDF4E /* CreateUserPkg.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = CreateUserPkg.icns; sourceTree = ""; }; 62 | C09EAB1F1EF8EDFE005A8859 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = ../../../../System/Library/Frameworks/Security.framework; sourceTree = ""; }; 63 | /* End PBXFileReference section */ 64 | 65 | /* Begin PBXFrameworksBuildPhase section */ 66 | 0596D175159AF27800B898B4 /* Frameworks */ = { 67 | isa = PBXFrameworksBuildPhase; 68 | buildActionMask = 2147483647; 69 | files = ( 70 | C09EAB201EF8EDFE005A8859 /* Security.framework in Frameworks */, 71 | 0596D17D159AF27800B898B4 /* Cocoa.framework in Frameworks */, 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | /* End PBXFrameworksBuildPhase section */ 76 | 77 | /* Begin PBXGroup section */ 78 | 0596D16D159AF27800B898B4 = { 79 | isa = PBXGroup; 80 | children = ( 81 | 66B221D7159C4C30003FDF4E /* CreateUserPkg.icns */, 82 | 0596D182159AF27800B898B4 /* CreateUserPkg */, 83 | 0596D17B159AF27800B898B4 /* Frameworks */, 84 | 0596D179159AF27800B898B4 /* Products */, 85 | ); 86 | sourceTree = ""; 87 | }; 88 | 0596D179159AF27800B898B4 /* Products */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 0596D178159AF27800B898B4 /* CreateUserPkg.app */, 92 | ); 93 | name = Products; 94 | sourceTree = ""; 95 | }; 96 | 0596D17B159AF27800B898B4 /* Frameworks */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 0596D17C159AF27800B898B4 /* Cocoa.framework */, 100 | 0596D17E159AF27800B898B4 /* Other Frameworks */, 101 | ); 102 | name = Frameworks; 103 | sourceTree = ""; 104 | }; 105 | 0596D17E159AF27800B898B4 /* Other Frameworks */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | C09EAB1F1EF8EDFE005A8859 /* Security.framework */, 109 | 0596D17F159AF27800B898B4 /* AppKit.framework */, 110 | 0596D180159AF27800B898B4 /* CoreData.framework */, 111 | 0596D181159AF27800B898B4 /* Foundation.framework */, 112 | ); 113 | name = "Other Frameworks"; 114 | sourceTree = ""; 115 | }; 116 | 0596D182159AF27800B898B4 /* CreateUserPkg */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 0596D19C159AF2A200B898B4 /* CreateUserPkg.entitlements */, 120 | 6605E68C159B90070048362A /* CUPAppDelegate.h */, 121 | 6605E68D159B90070048362A /* CUPAppDelegate.m */, 122 | 6605E688159B86480048362A /* CUPBestHostFinder.h */, 123 | 6605E689159B86480048362A /* CUPBestHostFinder.m */, 124 | 05CF2A15159DE745004F36C8 /* CUPImageSelector.h */, 125 | 05CF2A16159DE745004F36C8 /* CUPImageSelector.m */, 126 | 0596D18E159AF27800B898B4 /* CUPDocument.h */, 127 | 0596D18F159AF27800B898B4 /* CUPDocument.m */, 128 | 0596D191159AF27800B898B4 /* CUPDocument.xib */, 129 | 6605E68F159B964D0048362A /* CUPUIDFormatter.h */, 130 | 6605E690159B964D0048362A /* CUPUIDFormatter.m */, 131 | 6605E691159B964D0048362A /* CUPUserNameFormatter.h */, 132 | 6605E692159B964D0048362A /* CUPUserNameFormatter.m */, 133 | 6605E693159B964D0048362A /* CUPUUIDFormatter.h */, 134 | 6605E694159B964D0048362A /* CUPUUIDFormatter.m */, 135 | 0596D194159AF27800B898B4 /* MainMenu.xib */, 136 | 0596D183159AF27800B898B4 /* Supporting Files */, 137 | ); 138 | path = CreateUserPkg; 139 | sourceTree = ""; 140 | }; 141 | 0596D183159AF27800B898B4 /* Supporting Files */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 05CF2A19159DEDE4004F36C8 /* dropimagehere.png */, 145 | 05524F8C159B314900741F47 /* create_package.py */, 146 | 0596D184159AF27800B898B4 /* CreateUserPkg-Info.plist */, 147 | 0596D18A159AF27800B898B4 /* CreateUserPkg-Prefix.pch */, 148 | 0596D18B159AF27800B898B4 /* Credits.rtf */, 149 | 0596D185159AF27800B898B4 /* InfoPlist.strings */, 150 | 0596D188159AF27800B898B4 /* main.m */, 151 | 05524F8F159B32D600741F47 /* read_package.py */, 152 | ); 153 | name = "Supporting Files"; 154 | sourceTree = ""; 155 | }; 156 | /* End PBXGroup section */ 157 | 158 | /* Begin PBXNativeTarget section */ 159 | 0596D177159AF27800B898B4 /* CreateUserPkg */ = { 160 | isa = PBXNativeTarget; 161 | buildConfigurationList = 0596D199159AF27800B898B4 /* Build configuration list for PBXNativeTarget "CreateUserPkg" */; 162 | buildPhases = ( 163 | 0596D174159AF27800B898B4 /* Sources */, 164 | 0596D175159AF27800B898B4 /* Frameworks */, 165 | 0596D176159AF27800B898B4 /* Resources */, 166 | 6660D821159B2515003BB5E8 /* ShellScript */, 167 | 6660D823159B2528003BB5E8 /* ShellScript */, 168 | ); 169 | buildRules = ( 170 | ); 171 | dependencies = ( 172 | ); 173 | name = CreateUserPkg; 174 | productName = CreateUserPkg; 175 | productReference = 0596D178159AF27800B898B4 /* CreateUserPkg.app */; 176 | productType = "com.apple.product-type.application"; 177 | }; 178 | /* End PBXNativeTarget section */ 179 | 180 | /* Begin PBXProject section */ 181 | 0596D16F159AF27800B898B4 /* Project object */ = { 182 | isa = PBXProject; 183 | attributes = { 184 | CLASSPREFIX = CUP; 185 | LastUpgradeCheck = 0730; 186 | ORGANIZATIONNAME = "University of Gothenburg"; 187 | }; 188 | buildConfigurationList = 0596D172159AF27800B898B4 /* Build configuration list for PBXProject "CreateUserPkg" */; 189 | compatibilityVersion = "Xcode 3.2"; 190 | developmentRegion = English; 191 | hasScannedForEncodings = 0; 192 | knownRegions = ( 193 | en, 194 | ); 195 | mainGroup = 0596D16D159AF27800B898B4; 196 | productRefGroup = 0596D179159AF27800B898B4 /* Products */; 197 | projectDirPath = ""; 198 | projectRoot = ""; 199 | targets = ( 200 | 0596D177159AF27800B898B4 /* CreateUserPkg */, 201 | ); 202 | }; 203 | /* End PBXProject section */ 204 | 205 | /* Begin PBXResourcesBuildPhase section */ 206 | 0596D176159AF27800B898B4 /* Resources */ = { 207 | isa = PBXResourcesBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | 0596D187159AF27800B898B4 /* InfoPlist.strings in Resources */, 211 | 0596D18D159AF27800B898B4 /* Credits.rtf in Resources */, 212 | 0596D193159AF27800B898B4 /* CUPDocument.xib in Resources */, 213 | 0596D196159AF27800B898B4 /* MainMenu.xib in Resources */, 214 | 05524F8D159B314900741F47 /* create_package.py in Resources */, 215 | 05524F94159B33AB00741F47 /* read_package.py in Resources */, 216 | 66B221D8159C4C30003FDF4E /* CreateUserPkg.icns in Resources */, 217 | 05CF2A1A159DEDE4004F36C8 /* dropimagehere.png in Resources */, 218 | ); 219 | runOnlyForDeploymentPostprocessing = 0; 220 | }; 221 | /* End PBXResourcesBuildPhase section */ 222 | 223 | /* Begin PBXShellScriptBuildPhase section */ 224 | 6660D821159B2515003BB5E8 /* ShellScript */ = { 225 | isa = PBXShellScriptBuildPhase; 226 | buildActionMask = 2147483647; 227 | files = ( 228 | ); 229 | inputPaths = ( 230 | ); 231 | outputPaths = ( 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | shellPath = /bin/sh; 235 | shellScript = "chmod +x \"$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH\"/*.py\n"; 236 | showEnvVarsInLog = 0; 237 | }; 238 | 6660D823159B2528003BB5E8 /* ShellScript */ = { 239 | isa = PBXShellScriptBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | ); 243 | inputPaths = ( 244 | ); 245 | outputPaths = ( 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | shellPath = /bin/sh; 249 | shellScript = "#!/bin/bash\n#\n# Sets the version string to a monotonically increased string, based on\n# the number of git commits.\n\nset -o errexit\nset -o nounset\n\nVERSION=$(git --git-dir=\"$PROJECT_DIR/.git\" --work-tree=\"$PROJECT_DIR\" rev-list master | wc -l)\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $VERSION\" \"$TARGET_BUILD_DIR/$INFOPLIST_PATH\"\n"; 250 | showEnvVarsInLog = 0; 251 | }; 252 | /* End PBXShellScriptBuildPhase section */ 253 | 254 | /* Begin PBXSourcesBuildPhase section */ 255 | 0596D174159AF27800B898B4 /* Sources */ = { 256 | isa = PBXSourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | 0596D189159AF27800B898B4 /* main.m in Sources */, 260 | 0596D190159AF27800B898B4 /* CUPDocument.m in Sources */, 261 | 6605E68A159B86480048362A /* CUPBestHostFinder.m in Sources */, 262 | 6605E68E159B90070048362A /* CUPAppDelegate.m in Sources */, 263 | 6605E695159B964D0048362A /* CUPUIDFormatter.m in Sources */, 264 | 6605E696159B964D0048362A /* CUPUserNameFormatter.m in Sources */, 265 | 6605E697159B964D0048362A /* CUPUUIDFormatter.m in Sources */, 266 | 05CF2A17159DE745004F36C8 /* CUPImageSelector.m in Sources */, 267 | ); 268 | runOnlyForDeploymentPostprocessing = 0; 269 | }; 270 | /* End PBXSourcesBuildPhase section */ 271 | 272 | /* Begin PBXVariantGroup section */ 273 | 0596D185159AF27800B898B4 /* InfoPlist.strings */ = { 274 | isa = PBXVariantGroup; 275 | children = ( 276 | 0596D186159AF27800B898B4 /* en */, 277 | ); 278 | name = InfoPlist.strings; 279 | sourceTree = ""; 280 | }; 281 | 0596D18B159AF27800B898B4 /* Credits.rtf */ = { 282 | isa = PBXVariantGroup; 283 | children = ( 284 | 0596D18C159AF27800B898B4 /* en */, 285 | ); 286 | name = Credits.rtf; 287 | sourceTree = ""; 288 | }; 289 | 0596D191159AF27800B898B4 /* CUPDocument.xib */ = { 290 | isa = PBXVariantGroup; 291 | children = ( 292 | 0596D192159AF27800B898B4 /* en */, 293 | ); 294 | name = CUPDocument.xib; 295 | sourceTree = ""; 296 | }; 297 | 0596D194159AF27800B898B4 /* MainMenu.xib */ = { 298 | isa = PBXVariantGroup; 299 | children = ( 300 | 0596D195159AF27800B898B4 /* en */, 301 | ); 302 | name = MainMenu.xib; 303 | sourceTree = ""; 304 | }; 305 | /* End PBXVariantGroup section */ 306 | 307 | /* Begin XCBuildConfiguration section */ 308 | 0596D197159AF27800B898B4 /* Debug */ = { 309 | isa = XCBuildConfiguration; 310 | buildSettings = { 311 | ALWAYS_SEARCH_USER_PATHS = NO; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_CONSTANT_CONVERSION = YES; 314 | CLANG_WARN_EMPTY_BODY = YES; 315 | CLANG_WARN_ENUM_CONVERSION = YES; 316 | CLANG_WARN_INT_CONVERSION = YES; 317 | CLANG_WARN_UNREACHABLE_CODE = YES; 318 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 319 | COPY_PHASE_STRIP = NO; 320 | ENABLE_STRICT_OBJC_MSGSEND = YES; 321 | ENABLE_TESTABILITY = YES; 322 | GCC_C_LANGUAGE_STANDARD = gnu99; 323 | GCC_DYNAMIC_NO_PIC = NO; 324 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 325 | GCC_NO_COMMON_BLOCKS = YES; 326 | GCC_OPTIMIZATION_LEVEL = 0; 327 | GCC_PREPROCESSOR_DEFINITIONS = ( 328 | "DEBUG=1", 329 | "$(inherited)", 330 | ); 331 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 332 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 333 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 334 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 335 | GCC_WARN_UNDECLARED_SELECTOR = YES; 336 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 337 | GCC_WARN_UNUSED_FUNCTION = YES; 338 | GCC_WARN_UNUSED_VARIABLE = YES; 339 | MACOSX_DEPLOYMENT_TARGET = 10.7; 340 | ONLY_ACTIVE_ARCH = YES; 341 | SDKROOT = macosx; 342 | }; 343 | name = Debug; 344 | }; 345 | 0596D198159AF27800B898B4 /* Release */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ALWAYS_SEARCH_USER_PATHS = NO; 349 | CLANG_WARN_BOOL_CONVERSION = YES; 350 | CLANG_WARN_CONSTANT_CONVERSION = YES; 351 | CLANG_WARN_EMPTY_BODY = YES; 352 | CLANG_WARN_ENUM_CONVERSION = YES; 353 | CLANG_WARN_INT_CONVERSION = YES; 354 | CLANG_WARN_UNREACHABLE_CODE = YES; 355 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 356 | COPY_PHASE_STRIP = YES; 357 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 358 | ENABLE_STRICT_OBJC_MSGSEND = YES; 359 | GCC_C_LANGUAGE_STANDARD = gnu99; 360 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 361 | GCC_NO_COMMON_BLOCKS = YES; 362 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 363 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 364 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 365 | GCC_WARN_UNDECLARED_SELECTOR = YES; 366 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 367 | GCC_WARN_UNUSED_FUNCTION = YES; 368 | GCC_WARN_UNUSED_VARIABLE = YES; 369 | MACOSX_DEPLOYMENT_TARGET = 10.7; 370 | SDKROOT = macosx; 371 | }; 372 | name = Release; 373 | }; 374 | 0596D19A159AF27800B898B4 /* Debug */ = { 375 | isa = XCBuildConfiguration; 376 | buildSettings = { 377 | CLANG_ENABLE_OBJC_ARC = YES; 378 | CODE_SIGN_ENTITLEMENTS = CreateUserPkg/CreateUserPkg.entitlements; 379 | CODE_SIGN_IDENTITY = ""; 380 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = ""; 381 | COMBINE_HIDPI_IMAGES = YES; 382 | DEVELOPMENT_TEAM = ""; 383 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 384 | GCC_PREFIX_HEADER = "CreateUserPkg/CreateUserPkg-Prefix.pch"; 385 | INFOPLIST_FILE = "CreateUserPkg/CreateUserPkg-Info.plist"; 386 | MACOSX_DEPLOYMENT_TARGET = 10.8; 387 | PRODUCT_BUNDLE_IDENTIFIER = "se.gu.it.${PRODUCT_NAME:rfc1034identifier}"; 388 | PRODUCT_NAME = "$(TARGET_NAME)"; 389 | PROVISIONING_PROFILE = ""; 390 | WRAPPER_EXTENSION = app; 391 | }; 392 | name = Debug; 393 | }; 394 | 0596D19B159AF27800B898B4 /* Release */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | CLANG_ENABLE_OBJC_ARC = YES; 398 | CODE_SIGN_ENTITLEMENTS = CreateUserPkg/CreateUserPkg.entitlements; 399 | CODE_SIGN_IDENTITY = ""; 400 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = ""; 401 | COMBINE_HIDPI_IMAGES = YES; 402 | DEVELOPMENT_TEAM = ""; 403 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 404 | GCC_PREFIX_HEADER = "CreateUserPkg/CreateUserPkg-Prefix.pch"; 405 | INFOPLIST_FILE = "CreateUserPkg/CreateUserPkg-Info.plist"; 406 | MACOSX_DEPLOYMENT_TARGET = 10.8; 407 | PRODUCT_BUNDLE_IDENTIFIER = "se.gu.it.${PRODUCT_NAME:rfc1034identifier}"; 408 | PRODUCT_NAME = "$(TARGET_NAME)"; 409 | PROVISIONING_PROFILE = ""; 410 | WRAPPER_EXTENSION = app; 411 | }; 412 | name = Release; 413 | }; 414 | /* End XCBuildConfiguration section */ 415 | 416 | /* Begin XCConfigurationList section */ 417 | 0596D172159AF27800B898B4 /* Build configuration list for PBXProject "CreateUserPkg" */ = { 418 | isa = XCConfigurationList; 419 | buildConfigurations = ( 420 | 0596D197159AF27800B898B4 /* Debug */, 421 | 0596D198159AF27800B898B4 /* Release */, 422 | ); 423 | defaultConfigurationIsVisible = 0; 424 | defaultConfigurationName = Release; 425 | }; 426 | 0596D199159AF27800B898B4 /* Build configuration list for PBXNativeTarget "CreateUserPkg" */ = { 427 | isa = XCConfigurationList; 428 | buildConfigurations = ( 429 | 0596D19A159AF27800B898B4 /* Debug */, 430 | 0596D19B159AF27800B898B4 /* Release */, 431 | ); 432 | defaultConfigurationIsVisible = 0; 433 | defaultConfigurationName = Release; 434 | }; 435 | /* End XCConfigurationList section */ 436 | }; 437 | rootObject = 0596D16F159AF27800B898B4 /* Project object */; 438 | } 439 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // CUPAppDelegate.h 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-27. 6 | // Copyright (c) 2012 University of Gothenburg. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CUPAppDelegate : NSObject 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // CUPAppDelegate.m 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-27. 6 | // Copyright (c) 2012 University of Gothenburg. All rights reserved. 7 | // 8 | 9 | #import "CUPAppDelegate.h" 10 | #import "CUPBestHostFinder.h" 11 | 12 | @implementation CUPAppDelegate 13 | 14 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 15 | { 16 | [CUPBestHostFinder bestHostFinder]; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPBestHostFinder.h: -------------------------------------------------------------------------------- 1 | // 2 | // CUPBestHostFinder.h 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-27. 6 | // Copyright (c) 2012 University of Gothenburg. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CUPBestHostFinder : NSObject { 12 | NSArray *_bestHost; 13 | } 14 | 15 | @property (atomic, strong) NSArray *bestHost; 16 | 17 | + (CUPBestHostFinder *)bestHostFinder; 18 | - (void)findBestHost; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPBestHostFinder.m: -------------------------------------------------------------------------------- 1 | // 2 | // CUPBestHostFinder.m 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-27. 6 | // Copyright (c) 2012 University of Gothenburg. All rights reserved. 7 | // 8 | 9 | #import "CUPBestHostFinder.h" 10 | 11 | @implementation CUPBestHostFinder 12 | 13 | @synthesize bestHost = _bestHost; 14 | 15 | - (instancetype)init 16 | { 17 | self = [super init]; 18 | if (self) { 19 | self.bestHost = @[@"host", @"example", @"com"]; 20 | } 21 | return self; 22 | } 23 | 24 | 25 | + (CUPBestHostFinder *)bestHostFinder 26 | { 27 | static CUPBestHostFinder *sharedSingleton; 28 | @synchronized(self) { 29 | if (!sharedSingleton) { 30 | sharedSingleton = [[CUPBestHostFinder alloc] init]; 31 | [sharedSingleton performSelectorInBackground:@selector(findBestHost) withObject:nil]; 32 | } 33 | return sharedSingleton; 34 | } 35 | } 36 | 37 | - (void)findBestHost 38 | { 39 | // Try to find a full domain name for the current host. 40 | 41 | // Iterate over all host names. 42 | for (NSString *name in [[NSHost currentHost] names]) { 43 | // Split host name by ".". 44 | NSArray *splitHost = [name componentsSeparatedByString:@"."]; 45 | // Don't bother with unqualified host names. 46 | if ([splitHost count] > 1) { 47 | // Ignore .local. 48 | if (! [[splitHost lastObject] isEqualToString:@"local"]) { 49 | // Whatever we found, it's probably better than what we have. 50 | self.bestHost = splitHost; 51 | // Return as soon as we've got a result. 52 | return; 53 | } 54 | } 55 | } 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPDocument.h: -------------------------------------------------------------------------------- 1 | // 2 | // CUPDocument.h 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-27. 6 | // Copyright (c) 2012 University of Gothenburg. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "CUPBestHostFinder.h" 11 | #import "CUPImageSelector.h" 12 | 13 | 14 | @interface CUPDocument : NSDocument { 15 | NSWindow *__unsafe_unretained docWindow; 16 | 17 | CUPImageSelector *__unsafe_unretained _image; 18 | NSTextField *__unsafe_unretained _fullName; 19 | NSTextField *__unsafe_unretained _accountName; 20 | NSSecureTextField *__unsafe_unretained _password; 21 | NSSecureTextField *__unsafe_unretained _verifyPassword; 22 | NSTextField *__unsafe_unretained _userID; 23 | NSPopUpButton *__unsafe_unretained _accountType; 24 | NSTextField *__unsafe_unretained _homeDirectory; 25 | NSTextField *__unsafe_unretained _uuid; 26 | NSButton *__unsafe_unretained _automaticLogin; 27 | 28 | NSTextField *__unsafe_unretained _packageID; 29 | NSTextField *__unsafe_unretained _version; 30 | 31 | NSData *_shadowHashData; 32 | NSData *_kcPassword; 33 | NSMutableDictionary *_docState; 34 | } 35 | 36 | @property (unsafe_unretained) IBOutlet NSWindow *docWindow; 37 | 38 | @property (unsafe_unretained) IBOutlet NSTextField *fullName; 39 | @property (unsafe_unretained) IBOutlet NSTextField *accountName; 40 | @property (unsafe_unretained) IBOutlet CUPImageSelector *image; 41 | @property (unsafe_unretained) IBOutlet NSSecureTextField *password; 42 | @property (unsafe_unretained) IBOutlet NSSecureTextField *verifyPassword; 43 | @property (unsafe_unretained) IBOutlet NSTextField *userID; 44 | @property (unsafe_unretained) IBOutlet NSPopUpButton *accountType; 45 | @property (unsafe_unretained) IBOutlet NSTextField *homeDirectory; 46 | @property (unsafe_unretained) IBOutlet NSTextField *uuid; 47 | @property (unsafe_unretained) IBOutlet NSButton *automaticLogin; 48 | 49 | @property (unsafe_unretained) IBOutlet NSTextField *packageID; 50 | @property (unsafe_unretained) IBOutlet NSTextField *version; 51 | 52 | @property (strong) NSData *shadowHashData; 53 | @property (strong) NSData *kcPassword; 54 | @property (strong) NSMutableDictionary *docState; 55 | 56 | - (IBAction)didLeaveFullName:(id)sender; 57 | - (IBAction)didLeaveAccountName:(id)sender; 58 | - (IBAction)didLeavePassword:(id)sender; 59 | 60 | - (void)calculateShadowHashData:(NSString *)pwd; 61 | - (void)calculateKCPassword:(NSString *)pwd; 62 | - (void)setTextField:(NSTextField *)field withKey:(NSString *)key; 63 | - (void)setDocStateKey:(NSString *)key fromDict:(NSDictionary *)dict; 64 | - (BOOL)validateField:(NSControl *)field validator:(BOOL (^)(NSControl *))valFunc errorMsg:(NSString *)msg; 65 | - (BOOL)validateDocumentAndUpdateState; 66 | - (NSDictionary *)newDictFromScript:(NSString *)path withArgs:(NSDictionary *)argDict error:(NSError **)outError; 67 | 68 | 69 | #define PBKDF2_SALT_LEN 32 70 | #define PBKDF2_ENTROPY_LEN 128 71 | 72 | 73 | #define CUP_PASSWORD_PLACEHOLDER @"SEIPHATSXSTX$D418JMPC000" 74 | 75 | enum { 76 | ACCOUNT_TYPE_STANDARD = 0, 77 | ACCOUNT_TYPE_ADMIN 78 | }; 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPDocument.m: -------------------------------------------------------------------------------- 1 | // 2 | // CUPDocument.m 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-27. 6 | // Copyright (c) 2012 University of Gothenburg. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | #import "CUPDocument.h" 13 | #import "CUPUIDFormatter.h" 14 | #import "CUPUserNameFormatter.h" 15 | #import "CUPUUIDFormatter.h" 16 | 17 | @implementation CUPDocument 18 | 19 | @synthesize docWindow; 20 | 21 | @synthesize fullName = _fullName; 22 | @synthesize accountName = _accountName; 23 | @synthesize image = _image; 24 | @synthesize password = _password; 25 | @synthesize verifyPassword = _verifyPassword; 26 | @synthesize userID = _userID; 27 | @synthesize accountType = _accountType; 28 | @synthesize homeDirectory = _homeDirectory; 29 | @synthesize uuid = _uuid; 30 | @synthesize automaticLogin = _automaticLogin; 31 | 32 | @synthesize packageID = _packageID; 33 | @synthesize version = _version; 34 | @synthesize shadowHashData = _shadowHashData; 35 | @synthesize kcPassword = _kcPassword; 36 | @synthesize docState = _docState; 37 | 38 | 39 | - (instancetype)init 40 | { 41 | self = [super init]; 42 | if (self) { 43 | _docState = [[NSMutableDictionary alloc] init]; 44 | _shadowHashData = nil; 45 | _kcPassword = nil; 46 | } 47 | return self; 48 | } 49 | 50 | 51 | - (NSError *)cocoaError:(NSInteger)code withReason:(NSString *)reason 52 | { 53 | return [NSError errorWithDomain:NSCocoaErrorDomain code:code userInfo:@{NSLocalizedFailureReasonErrorKey: reason}]; 54 | } 55 | 56 | - (NSString *)windowNibName 57 | { 58 | return @"CUPDocument"; 59 | } 60 | 61 | - (void)showHelp:(id)sender 62 | { 63 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://github.com/MagerValp/CreateUserPkg#createuserpkg"]]; 64 | } 65 | 66 | - (IBAction)didLeaveFullName:(id)sender { 67 | // If the user has entered a fullName convert the full name to lower case, strip 68 | // illegal chars, and use that to fill the accountName. 69 | if ([[self.accountName stringValue] length] == 0 && [[self.fullName stringValue] length] != 0) { 70 | NSString *lcString = [[self.fullName stringValue] lowercaseString]; 71 | NSData *asciiData = [lcString dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; 72 | NSString *asciiString = [[NSString alloc] initWithData:asciiData encoding:NSASCIIStringEncoding]; 73 | [self.accountName setStringValue:asciiString]; 74 | } 75 | } 76 | 77 | - (IBAction)didLeaveAccountName:(id)sender { 78 | // If the user has entered an accountName fill homeDirectory and packageID. 79 | if ([[self.homeDirectory stringValue] length] == 0 && [[self.accountName stringValue] length] != 0) { 80 | [self.homeDirectory setStringValue:[NSString stringWithFormat:@"/Users/%@", [self.accountName stringValue]]]; 81 | } 82 | if ([[self.packageID stringValue] length] == 0 && [[self.accountName stringValue] length] != 0) { 83 | NSMutableArray *host = [NSMutableArray arrayWithArray:[[CUPBestHostFinder bestHostFinder] bestHost]]; 84 | [host removeObjectAtIndex:0]; 85 | 86 | NSString *reverseDomain = [[[host reverseObjectEnumerator] allObjects] componentsJoinedByString:@"."]; 87 | [self.packageID setStringValue:[NSString stringWithFormat:@"%@.create_%@.pkg", reverseDomain, [self.accountName stringValue]]]; 88 | } 89 | } 90 | 91 | - (IBAction)didLeavePassword:(id)sender { 92 | if ([[self.password stringValue] length] != 0) { 93 | if ([[self.password stringValue] isEqualToString:[self.verifyPassword stringValue]]) { 94 | if ([[self.password stringValue] isEqualToString:CUP_PASSWORD_PLACEHOLDER] == NO) { 95 | [self calculateShadowHashData:[self.password stringValue]]; 96 | [self calculateKCPassword:[self.password stringValue]]; 97 | [self.automaticLogin setEnabled:YES]; 98 | } 99 | } 100 | } 101 | } 102 | 103 | 104 | - (void)calculateShadowHashData:(NSString *)pwd 105 | { 106 | unsigned char salt[PBKDF2_SALT_LEN]; 107 | int status = SecRandomCopyBytes(kSecRandomDefault, PBKDF2_SALT_LEN, salt); 108 | if (status == 0) { 109 | unsigned char key[PBKDF2_ENTROPY_LEN]; 110 | // calculate the number of iterations to use 111 | unsigned int rounds = CCCalibratePBKDF(kCCPBKDF2, 112 | [pwd lengthOfBytesUsingEncoding:NSUTF8StringEncoding], 113 | PBKDF2_SALT_LEN, kCCPRFHmacAlgSHA512, PBKDF2_ENTROPY_LEN, 100); 114 | // derive our SALTED-SHA512-PBKDF2 key 115 | CCKeyDerivationPBKDF(kCCPBKDF2, 116 | [pwd UTF8String], 117 | (CC_LONG)[pwd lengthOfBytesUsingEncoding:NSUTF8StringEncoding], 118 | salt, PBKDF2_SALT_LEN, 119 | kCCPRFHmacAlgSHA512, rounds, key, PBKDF2_ENTROPY_LEN); 120 | // Make a dictionary containing the needed fields 121 | NSDictionary *dictionary = @{ 122 | @"SALTED-SHA512-PBKDF2" : @{ 123 | @"entropy" : [NSData dataWithBytes: key length: PBKDF2_ENTROPY_LEN], 124 | @"iterations" : [NSNumber numberWithUnsignedInt: rounds], 125 | @"salt" : [NSData dataWithBytes: salt length: PBKDF2_SALT_LEN] 126 | } 127 | }; 128 | // convert to binary plist data 129 | NSError *error = NULL; 130 | NSData *plistData = [NSPropertyListSerialization dataWithPropertyList: dictionary 131 | format: NSPropertyListBinaryFormat_v1_0 132 | options: 0 133 | error: &error]; 134 | self.shadowHashData = plistData; 135 | } 136 | } 137 | 138 | 139 | #define KCKEY_LEN 11 140 | const char kcPasswordKey[KCKEY_LEN] = {0x7D, 0x89, 0x52, 0x23, 0xD2, 0xBC, 0xDD, 0xEA, 0xA3, 0xB9, 0x1F}; 141 | 142 | - (void)calculateKCPassword:(NSString *)pwd 143 | { 144 | int i; 145 | NSUInteger pwdlen = [pwd lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; 146 | // kcpassword stores the password plus null terminator, rounded up to the nearest 12 bytes. 147 | NSUInteger rounded12len = ((pwdlen + 12) / 12) * 12; 148 | unsigned char *kcbuf = malloc(rounded12len); 149 | 150 | // Get UTF-8 encoded data and xor it with the key. 151 | const unsigned char *pwdbuf = [[pwd dataUsingEncoding:NSUTF8StringEncoding] bytes]; 152 | for (i = 0; i < pwdlen; i++) { 153 | kcbuf[i] = pwdbuf[i] ^ kcPasswordKey[i % KCKEY_LEN]; 154 | } 155 | kcbuf[i] = kcPasswordKey[i % KCKEY_LEN]; 156 | 157 | self.kcPassword = [NSData dataWithBytes:kcbuf length:rounded12len]; 158 | 159 | free(kcbuf); 160 | } 161 | 162 | - (void)setTextField:(NSTextField *)field withKey:(NSString *)key 163 | { 164 | NSString *value = (self.docState)[key]; 165 | if (key != nil && value != nil) { 166 | [field setStringValue:value]; 167 | } 168 | } 169 | 170 | #define UPDATE_TEXT_FIELD(FIELD) [self setTextField:self.FIELD withKey:@#FIELD] 171 | 172 | - (void)windowControllerDidLoadNib:(NSWindowController *)aController 173 | { 174 | [super windowControllerDidLoadNib:aController]; 175 | 176 | // Initialize form with default values. 177 | CUPUIDFormatter *uidFormatter = [[CUPUIDFormatter alloc] init]; 178 | [self.userID setFormatter:uidFormatter]; 179 | [self.userID setStringValue:@"899"]; 180 | CUPUserNameFormatter *userNameFormatter = [[CUPUserNameFormatter alloc] init]; 181 | [self.accountName setFormatter:userNameFormatter]; 182 | CUPUUIDFormatter *uuidFormatter = [[CUPUUIDFormatter alloc] init]; 183 | [self.uuid setFormatter:uuidFormatter]; 184 | CFUUIDRef theUUID = CFUUIDCreate(NULL); 185 | NSString *uuidString = (NSString *)CFBridgingRelease(CFUUIDCreateString(NULL, theUUID)); 186 | [self.uuid setStringValue:uuidString]; 187 | CFRelease(theUUID); 188 | [self.version setStringValue:@"1.0"]; 189 | 190 | [self.fullName becomeFirstResponder]; 191 | 192 | // Load values from document state. 193 | UPDATE_TEXT_FIELD(fullName); 194 | UPDATE_TEXT_FIELD(accountName); 195 | UPDATE_TEXT_FIELD(userID); 196 | UPDATE_TEXT_FIELD(homeDirectory); 197 | UPDATE_TEXT_FIELD(uuid); 198 | UPDATE_TEXT_FIELD(packageID); 199 | UPDATE_TEXT_FIELD(version); 200 | if ((self.docState)[@"shadowHashData"] != nil) { 201 | [self.password setStringValue:CUP_PASSWORD_PLACEHOLDER]; 202 | [self.verifyPassword setStringValue:CUP_PASSWORD_PLACEHOLDER]; 203 | } 204 | [self.automaticLogin setEnabled:YES]; 205 | if ((self.docState)[@"kcPassword"] != nil) { 206 | [self.automaticLogin setState:NSOnState]; 207 | } else { 208 | [self.automaticLogin setState:NSOffState]; 209 | if ([[self.password stringValue] isEqualToString:CUP_PASSWORD_PLACEHOLDER]) { 210 | [self.automaticLogin setEnabled:NO]; 211 | } 212 | } 213 | self.image.imageData = (self.docState)[@"imageData"]; 214 | self.image.imagePath = (self.docState)[@"imagePath"]; 215 | [self.image displayImageData]; 216 | if ((self.docState)[@"isAdmin"] != nil) { 217 | [self.accountType selectItemAtIndex:[(self.docState)[@"isAdmin"] boolValue] ? ACCOUNT_TYPE_ADMIN : ACCOUNT_TYPE_STANDARD]; 218 | } else { 219 | [self.accountType selectItemAtIndex:ACCOUNT_TYPE_ADMIN]; 220 | } 221 | } 222 | 223 | + (BOOL)autosavesInPlace 224 | { 225 | return NO; 226 | } 227 | 228 | - (BOOL)isDocumentEdited 229 | { 230 | return NO; 231 | } 232 | 233 | - (NSString *)displayName 234 | { 235 | // This is used to provide a default name for the save dialog. 236 | // If accountName and version are set, return create_NAME-VER instead of Untitled. 237 | NSString *orgDisplayName = [super displayName]; 238 | if ([[orgDisplayName lowercaseString] hasPrefix:@"untitled"]) { 239 | if ([[self.accountName stringValue] length] > 0 && [[self.version stringValue] length] > 0) { 240 | return [NSString stringWithFormat:@"create_%@-%@", [self.accountName stringValue], [self.version stringValue]]; 241 | } 242 | } 243 | return orgDisplayName; 244 | } 245 | 246 | - (BOOL)validateField:(NSControl *)field validator:(BOOL (^)(NSControl *))valFunc errorMsg:(NSString *)msg 247 | { 248 | if (valFunc(field) == NO) { 249 | [field becomeFirstResponder]; 250 | NSRunCriticalAlertPanel(msg, @"", @"OK", nil, nil); 251 | return NO; 252 | } 253 | return YES; 254 | } 255 | 256 | #define VALIDATE(...) if ((__VA_ARGS__) == NO) {return NO;} 257 | #define SET_DOC_STATE(KEY) [self.docState setObject:[[self KEY] stringValue] forKey:@#KEY] 258 | 259 | - (BOOL)validateDocumentAndUpdateState 260 | { 261 | BOOL (^validateNotEmpty)(NSControl *) = ^(NSControl *field){ 262 | return (BOOL)([[field stringValue] length] != 0); 263 | }; 264 | BOOL (^validateSameAsPassword)(NSControl *) = ^(NSControl *field){ 265 | return [[field stringValue] isEqualToString:[self.password stringValue]]; 266 | }; 267 | BOOL (^validateHomeDir)(NSControl *) = ^(NSControl *field){ 268 | return (BOOL)([[self.homeDirectory stringValue] length] != 0 && [[self.homeDirectory stringValue] characterAtIndex:0] == L'/'); 269 | }; 270 | BOOL (^validateUUID)(NSControl *) = ^(NSControl *field){ 271 | return (BOOL)([[field stringValue] length] == 36); 272 | }; 273 | 274 | VALIDATE([self validateField:self.fullName validator:validateNotEmpty errorMsg:@"Please enter a full name"]); 275 | VALIDATE([self validateField:self.accountName validator:validateNotEmpty errorMsg:@"Please enter an account name"]); 276 | if ([[self.password stringValue] length] == 0) { 277 | if (NSRunAlertPanel(@"Are you sure you want to create a user with an empty password?", 278 | @"Anyone will be able to log in, even remotely, without being asked for a password.", 279 | @"Cancel", 280 | @"Use Empty Password", 281 | nil) == NSAlertDefaultReturn) { 282 | [self.password becomeFirstResponder]; 283 | return NO; 284 | } 285 | } 286 | VALIDATE([self validateField:self.verifyPassword validator:validateSameAsPassword errorMsg:@"Password verification doesn't match"]); 287 | VALIDATE([self validateField:self.userID validator:validateNotEmpty errorMsg:@"Please enter a user ID"]); 288 | VALIDATE([self validateField:self.homeDirectory validator:validateHomeDir errorMsg:@"Please enter a home directory path"]); 289 | VALIDATE([self validateField:self.uuid validator:validateUUID errorMsg:@"Please enter a valid UUID"]); 290 | VALIDATE([self validateField:self.packageID validator:validateNotEmpty errorMsg:@"Please enter a package ID"]); 291 | VALIDATE([self validateField:self.version validator:validateNotEmpty errorMsg:@"Please enter a version"]); 292 | 293 | SET_DOC_STATE(fullName); 294 | SET_DOC_STATE(accountName); 295 | SET_DOC_STATE(userID); 296 | SET_DOC_STATE(homeDirectory); 297 | SET_DOC_STATE(uuid); 298 | SET_DOC_STATE(packageID); 299 | SET_DOC_STATE(version); 300 | if ([[self.password stringValue] isEqualToString:CUP_PASSWORD_PLACEHOLDER] == NO) { 301 | if (self.shadowHashData == nil) { 302 | NSLog(@"shadowHash is empty, calculating new hash"); 303 | [self calculateShadowHashData:[self.password stringValue]]; 304 | } 305 | (self.docState)[@"shadowHashData"] = [NSData dataWithData:self.shadowHashData]; 306 | } 307 | if ([self.automaticLogin state] == NSOnState) { 308 | if ([self.kcPassword length] == 0) { 309 | NSLog(@"kcPassword is empty, calculating new hash"); 310 | [self calculateKCPassword:[self.password stringValue]]; 311 | } 312 | (self.docState)[@"kcPassword"] = self.kcPassword; 313 | } 314 | if (self.image.imageData != nil) { 315 | (self.docState)[@"imageData"] = self.image.imageData; 316 | } 317 | if (self.image.imagePath != nil) { 318 | (self.docState)[@"imagePath"] = self.image.imagePath; 319 | } 320 | (self.docState)[@"isAdmin"] = [NSNumber numberWithBool:([self.accountType indexOfSelectedItem] == ACCOUNT_TYPE_ADMIN)]; 321 | 322 | return YES; 323 | } 324 | 325 | - (void)saveDocument:(id)sender 326 | { 327 | if ([self validateDocumentAndUpdateState]) { 328 | [super saveDocument:sender]; 329 | } 330 | } 331 | 332 | - (void)saveDocumentAs:(id)sender 333 | { 334 | if ([self validateDocumentAndUpdateState]) { 335 | [super saveDocumentAs:sender]; 336 | } 337 | } 338 | 339 | - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError 340 | { 341 | NSData *data; 342 | 343 | NSDictionary *result = [self newDictFromScript:[[NSBundle mainBundle] pathForResource:@"create_package" ofType:@"py"] withArgs:self.docState error:outError]; 344 | if (result == nil) { 345 | return nil; 346 | } 347 | data = result[@"data"]; 348 | return data; 349 | } 350 | 351 | - (void)setDocStateKey:(NSString *)key fromDict:(NSDictionary *)dict 352 | { 353 | NSString *value = dict[key]; 354 | if (value != nil) { 355 | (self.docState)[key] = value; 356 | } 357 | } 358 | 359 | - (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError 360 | { 361 | NSDictionary *document; 362 | NSString *tmp_plist = [NSTemporaryDirectory() stringByAppendingFormat:@"%08x.pkg", arc4random()]; 363 | NSDictionary *args = @{@"input": tmp_plist}; 364 | 365 | [data writeToFile:tmp_plist atomically:NO]; 366 | 367 | document = [self newDictFromScript:[[NSBundle mainBundle] pathForResource:@"read_package" ofType:@"py"] withArgs:args error:outError]; 368 | [[NSFileManager defaultManager] removeItemAtPath:tmp_plist error:nil]; 369 | if (document == nil) { 370 | NSLog(@"Script returned nil"); 371 | return NO; 372 | } 373 | 374 | [self.docState removeAllObjects]; 375 | [self setDocStateKey:@"fullName" fromDict:document]; 376 | [self setDocStateKey:@"accountName" fromDict:document]; 377 | [self setDocStateKey:@"userID" fromDict:document]; 378 | [self setDocStateKey:@"isAdmin" fromDict:document]; 379 | [self setDocStateKey:@"homeDirectory" fromDict:document]; 380 | [self setDocStateKey:@"uuid" fromDict:document]; 381 | [self setDocStateKey:@"packageID" fromDict:document]; 382 | [self setDocStateKey:@"version" fromDict:document]; 383 | if (document[@"shadowHashData"] != nil) { 384 | // only set this if we read ShadowHashData from the package 385 | [self setDocStateKey:@"shadowHashData" fromDict:document]; 386 | } 387 | [self setDocStateKey:@"imageData" fromDict:document]; 388 | [self setDocStateKey:@"imagePath" fromDict:document]; 389 | [self setDocStateKey:@"kcPassword" fromDict:document]; 390 | 391 | self.kcPassword = document[@"kcPassword"]; 392 | 393 | return YES; 394 | } 395 | 396 | - (NSDictionary *)newDictFromScript:(NSString *)path withArgs:(NSDictionary *)argDict error:(NSError **)outError 397 | { 398 | NSTask *task; 399 | NSPipe *stdoutPipe; 400 | NSPipe *stderrPipe; 401 | NSPipe *stdinPipe; 402 | NSFileHandle *stdoutFile; 403 | NSFileHandle *stderrFile; 404 | NSFileHandle *stdinFile; 405 | NSData *stdoutData; 406 | NSData *stderrData; 407 | NSData *stdinData; 408 | NSDictionary *result; 409 | NSString *errorMsg = nil; 410 | 411 | // Create a task to execute the script. 412 | task = [[NSTask alloc] init]; 413 | [task setLaunchPath:path]; 414 | //[task setArguments:args]; 415 | 416 | // Set up stdout/err/in pipes and get the file handles. 417 | stdoutPipe = [NSPipe pipe]; 418 | stderrPipe = [NSPipe pipe]; 419 | stdinPipe = [NSPipe pipe]; 420 | [task setStandardOutput:stdoutPipe]; 421 | [task setStandardError:stderrPipe]; 422 | [task setStandardInput:stdinPipe]; 423 | stdoutFile = [stdoutPipe fileHandleForReading]; 424 | stderrFile = [stderrPipe fileHandleForReading]; 425 | stdinFile = [stdinPipe fileHandleForWriting]; 426 | 427 | // Execute and collect output. 428 | [task launch]; 429 | 430 | stdinData = [NSPropertyListSerialization dataFromPropertyList:argDict format:NSPropertyListXMLFormat_v1_0 errorDescription:&errorMsg]; 431 | if (stdinData == nil) { 432 | if (outError != NULL) { 433 | *outError = [self cocoaError:NSFileReadUnknownError withReason:[NSString stringWithFormat:@"Couldn't create input for %@: %@", [path lastPathComponent], errorMsg]]; 434 | } 435 | } 436 | [stdinFile writeData:stdinData]; 437 | [stdinFile closeFile]; 438 | stdoutData = [stdoutFile readDataToEndOfFile]; 439 | stderrData = [stderrFile readDataToEndOfFile]; 440 | 441 | [task waitUntilExit]; 442 | int terminationStatus = [task terminationStatus]; 443 | 444 | 445 | if ([stderrData length] != 0) { 446 | NSLog(@"%@ stderr: %@", path, [[NSString alloc] initWithData:stderrData encoding:NSUTF8StringEncoding]); 447 | } 448 | 449 | if (terminationStatus == 0) { 450 | result = [NSPropertyListSerialization propertyListFromData:stdoutData 451 | mutabilityOption:0 452 | format:NULL 453 | errorDescription:&errorMsg]; 454 | if (result == nil) { 455 | if ([stdoutData length] != 0) { 456 | NSLog(@"%@ stdout: %@", path, [[NSString alloc] initWithData:stdoutData encoding:NSUTF8StringEncoding]); 457 | } 458 | if (outError != NULL) { 459 | *outError = [self cocoaError:NSFileReadUnknownError withReason:[NSString stringWithFormat:@"Couldn't read output from %@: %@", [path lastPathComponent], errorMsg]]; 460 | } 461 | return nil; 462 | } 463 | return result; 464 | } else { 465 | if ([stdoutData length] != 0) { 466 | NSLog(@"%@ stdout: %@", path, [[NSString alloc] initWithData:stdoutData encoding:NSUTF8StringEncoding]); 467 | } 468 | if (outError != NULL) { 469 | *outError = [self cocoaError:NSFileReadUnknownError withReason:[NSString stringWithFormat:@"%@ exited with return code %d", [path lastPathComponent], terminationStatus]]; 470 | } 471 | return nil; 472 | } 473 | } 474 | 475 | @end 476 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPImageSelector.h: -------------------------------------------------------------------------------- 1 | // 2 | // CUPImageSelector.h 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-29. 6 | // Copyright (c) 2012 University of Gothenburg. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CUPImageSelector : NSImageView { 12 | NSData *_imageData; 13 | NSString *_imagePath; 14 | } 15 | 16 | @property (strong) NSData *imageData; 17 | @property (strong) NSString *imagePath; 18 | 19 | - (BOOL)saveJpegData:(NSData *)data; 20 | - (void)saveUserPicturesPath:(NSURL *)url; 21 | - (void)displayImageData; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPImageSelector.m: -------------------------------------------------------------------------------- 1 | // 2 | // CUPImageSelector.m 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-29. 6 | // Copyright (c) 2012 University of Gothenburg. All rights reserved. 7 | // 8 | 9 | #import "CUPImageSelector.h" 10 | 11 | @implementation CUPImageSelector 12 | 13 | @synthesize imageData = _imageData; 14 | @synthesize imagePath = _imagePath; 15 | 16 | 17 | - (instancetype)initWithFrame:(NSRect)frame 18 | { 19 | self = [super initWithFrame:frame]; 20 | if (self) { 21 | [self registerForDraggedTypes:@[NSTIFFPboardType, NSURLPboardType]]; 22 | self.imageData = nil; 23 | self.imagePath = nil; 24 | } 25 | 26 | return self; 27 | } 28 | 29 | 30 | - (BOOL)saveJpegData:(NSData *)data 31 | { 32 | NSBitmapImageRep *imgrep = [NSBitmapImageRep imageRepWithData:data]; 33 | if (imgrep == nil) { 34 | return NO; 35 | } 36 | self.imageData = [imgrep representationUsingType:NSJPEGFileType properties:@{}]; 37 | if (self.imageData == nil) { 38 | return NO; 39 | } 40 | return YES; 41 | } 42 | 43 | - (void)saveUserPicturesPath:(NSURL *)url 44 | { 45 | if (url != nil) { 46 | if ([url isFileURL] == YES) { 47 | NSString *path = [url path]; 48 | if ([path hasPrefix:@"/Library/User Pictures/"]) { 49 | self.imagePath = path; 50 | } 51 | } 52 | } 53 | } 54 | 55 | - (void)displayImageData 56 | { 57 | if (self.imageData) { 58 | NSImage *image = [[NSImage alloc] initWithData:self.imageData]; 59 | [self setImage:image]; 60 | } 61 | [self setNeedsDisplay:YES]; 62 | } 63 | 64 | - (NSDragOperation)draggingEntered:(id )sender 65 | { 66 | if ((NSDragOperationGeneric & [sender draggingSourceOperationMask]) == NSDragOperationGeneric) { 67 | return NSDragOperationGeneric; 68 | } else { 69 | return NSDragOperationNone; 70 | } 71 | } 72 | 73 | - (BOOL)performDragOperation:(id )sender 74 | { 75 | NSPasteboard *pboard = [sender draggingPasteboard]; 76 | NSString *droppedType = [pboard availableTypeFromArray:@[NSTIFFPboardType, NSURLPboardType]]; 77 | 78 | if ([droppedType isEqualToString:NSTIFFPboardType]) { 79 | NSData *droppedData = [pboard dataForType:droppedType]; 80 | if ([self saveJpegData:droppedData] == NO) { 81 | return NO; 82 | } 83 | [self displayImageData]; 84 | self.imagePath = nil; 85 | } else if ([droppedType isEqualToString:NSURLPboardType]) { 86 | NSURL *droppedURL = [NSURL URLFromPasteboard:pboard]; 87 | if ([self saveJpegData:[NSData dataWithContentsOfURL:droppedURL]] == NO) { 88 | return NO; 89 | } 90 | [self saveUserPicturesPath:droppedURL]; 91 | } else { 92 | return NO; 93 | } 94 | 95 | [self setNeedsDisplay:YES]; 96 | return YES; 97 | } 98 | 99 | @end 100 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPUIDFormatter.h: -------------------------------------------------------------------------------- 1 | // 2 | // CUPNumberOnly.h 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-19. 6 | // Copyright (c) 2012 Per Olofsson. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CUPUIDFormatter : NSNumberFormatter 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPUIDFormatter.m: -------------------------------------------------------------------------------- 1 | // 2 | // CUPNumberOnly.m 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-19. 6 | // Copyright (c) 2012 Per Olofsson. All rights reserved. 7 | // 8 | 9 | #import "CUPUIDFormatter.h" 10 | 11 | @implementation CUPUIDFormatter 12 | 13 | - (BOOL)isPartialStringValid:(NSString *)partialString 14 | newEditingString:(NSString **)newString 15 | errorDescription:(NSString **)error 16 | { 17 | NSRange inRange; 18 | NSCharacterSet *allowedChars = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet]; 19 | inRange = [partialString rangeOfCharacterFromSet:allowedChars]; 20 | 21 | if(inRange.location != NSNotFound) 22 | { 23 | *error = @"Only numbers are allowed."; 24 | NSBeep(); 25 | return NO; 26 | } 27 | 28 | *newString = partialString; 29 | return YES; 30 | } 31 | 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPUUIDFormatter.h: -------------------------------------------------------------------------------- 1 | // 2 | // CUPUUIDFormatter.h 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-19. 6 | // Copyright (c) 2012 Per Olofsson. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CUPUUIDFormatter : NSFormatter { 12 | NSMutableCharacterSet *controlSet; 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPUUIDFormatter.m: -------------------------------------------------------------------------------- 1 | // 2 | // CUPUUIDFormatter.m 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-19. 6 | // Copyright (c) 2012 Per Olofsson. All rights reserved. 7 | // 8 | 9 | #import "CUPUUIDFormatter.h" 10 | 11 | @implementation CUPUUIDFormatter 12 | 13 | - (instancetype)init 14 | { 15 | self = [super init]; 16 | if (self) { 17 | controlSet = [[NSMutableCharacterSet alloc] init]; 18 | 19 | NSRange upperCaseAlphaRange; 20 | upperCaseAlphaRange.location = (unsigned int)'A'; 21 | upperCaseAlphaRange.length = 6; 22 | [controlSet addCharactersInRange:upperCaseAlphaRange]; 23 | 24 | NSRange digitsRange; 25 | digitsRange.location = (unsigned int)'0'; 26 | digitsRange.length = 10; 27 | [controlSet addCharactersInRange:digitsRange]; 28 | 29 | [controlSet addCharactersInString:@"-"]; 30 | 31 | [controlSet invert]; 32 | } 33 | return self; 34 | } 35 | 36 | - (NSString *)stringForObjectValue:(id)anObject { 37 | if (![anObject isKindOfClass:[NSString class]]) { 38 | return nil; 39 | } 40 | return anObject; 41 | } 42 | 43 | - (BOOL)getObjectValue:(id *)obj forString:(NSString *)string errorDescription:(NSString **)error { 44 | NSString *trimmedReplacement = [[string componentsSeparatedByCharactersInSet:controlSet] componentsJoinedByString:@""]; 45 | *obj = trimmedReplacement; 46 | return YES; 47 | } 48 | 49 | - (BOOL)isPartialStringValid:(NSString *)partialString 50 | newEditingString:(NSString **)newString 51 | errorDescription:(NSString **)error 52 | { 53 | NSRange inRange = [[partialString uppercaseString] rangeOfCharacterFromSet:controlSet]; 54 | 55 | if(inRange.location != NSNotFound) 56 | { 57 | *error = @"Illegal value."; 58 | NSBeep(); 59 | return NO; 60 | } 61 | 62 | *newString = [partialString uppercaseString]; 63 | return YES; 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPUserNameFormatter.h: -------------------------------------------------------------------------------- 1 | // 2 | // CUPUserNameFormatter.h 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-19. 6 | // Copyright (c) 2012 Per Olofsson. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CUPUserNameFormatter : NSFormatter { 12 | NSMutableCharacterSet *controlSet; 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /CreateUserPkg/CUPUserNameFormatter.m: -------------------------------------------------------------------------------- 1 | // 2 | // CUPUserNameFormatter.m 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-19. 6 | // Copyright (c) 2012 Per Olofsson. All rights reserved. 7 | // 8 | 9 | #import "CUPUserNameFormatter.h" 10 | 11 | @implementation CUPUserNameFormatter 12 | 13 | - (instancetype)init 14 | { 15 | self = [super init]; 16 | if (self) { 17 | controlSet = [[NSMutableCharacterSet alloc] init]; 18 | 19 | NSRange lowerCaseAlphaRange; 20 | lowerCaseAlphaRange.location = (unsigned int)'a'; 21 | lowerCaseAlphaRange.length = 26; 22 | [controlSet addCharactersInRange:lowerCaseAlphaRange]; 23 | 24 | NSRange digitsRange; 25 | digitsRange.location = (unsigned int)'0'; 26 | digitsRange.length = 10; 27 | [controlSet addCharactersInRange:digitsRange]; 28 | 29 | [controlSet addCharactersInString:@"-._"]; 30 | 31 | [controlSet invert]; 32 | } 33 | return self; 34 | } 35 | 36 | - (NSString *)stringForObjectValue:(id)anObject { 37 | if (![anObject isKindOfClass:[NSString class]]) { 38 | return nil; 39 | } 40 | return anObject; 41 | } 42 | 43 | - (BOOL)getObjectValue:(id *)obj forString:(NSString *)string errorDescription:(NSString **)error { 44 | NSString *trimmedReplacement = [[string componentsSeparatedByCharactersInSet:controlSet] componentsJoinedByString:@""]; 45 | *obj = trimmedReplacement; 46 | return YES; 47 | } 48 | 49 | - (BOOL)isPartialStringValid:(NSString *)partialString 50 | newEditingString:(NSString **)newString 51 | errorDescription:(NSString **)error 52 | { 53 | NSRange inRange = [partialString rangeOfCharacterFromSet:controlSet]; 54 | 55 | if(inRange.location != NSNotFound) 56 | { 57 | *error = @"Illegal value."; 58 | NSBeep(); 59 | return NO; 60 | } 61 | 62 | *newString = partialString; 63 | return YES; 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /CreateUserPkg/CreateUserPkg-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeExtensions 11 | 12 | pkg 13 | 14 | CFBundleTypeIconFile 15 | 16 | CFBundleTypeName 17 | DocumentType 18 | CFBundleTypeOSTypes 19 | 20 | ???? 21 | 22 | CFBundleTypeRole 23 | Editor 24 | NSDocumentClass 25 | CUPDocument 26 | 27 | 28 | CFBundleExecutable 29 | ${EXECUTABLE_NAME} 30 | CFBundleIconFile 31 | CreateUserPkg.icns 32 | CFBundleIdentifier 33 | $(PRODUCT_BUNDLE_IDENTIFIER) 34 | CFBundleInfoDictionaryVersion 35 | 6.0 36 | CFBundleName 37 | ${PRODUCT_NAME} 38 | CFBundlePackageType 39 | APPL 40 | CFBundleShortVersionString 41 | 1.5 42 | CFBundleSignature 43 | ???? 44 | CFBundleVersion 45 | 1 46 | LSApplicationCategoryType 47 | public.app-category.utilities 48 | LSMinimumSystemVersion 49 | ${MACOSX_DEPLOYMENT_TARGET} 50 | NSHumanReadableCopyright 51 | Copyright © 2012-2013 Per Olofsson, University of Gothenburg 52 | NSMainNibFile 53 | MainMenu 54 | NSPrincipalClass 55 | NSApplication 56 | 57 | 58 | -------------------------------------------------------------------------------- /CreateUserPkg/CreateUserPkg-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'CreateUserPkg' target in the 'CreateUserPkg' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /CreateUserPkg/CreateUserPkg.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CreateUserPkg/create_package.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # create_package.py 4 | # CreateUserPkg 5 | # 6 | # Created by Per Olofsson on 2012-06-20. 7 | # Copyright (c) 2012 Per Olofsson. All rights reserved. 8 | 9 | 10 | import os 11 | import sys 12 | import plistlib 13 | import hashlib 14 | import tempfile 15 | import subprocess 16 | import shutil 17 | import stat 18 | import gzip 19 | 20 | 21 | REQUIRED_KEYS = set(( 22 | u"fullName", 23 | u"accountName", 24 | u"shadowHashData", 25 | u"userID", 26 | u"isAdmin", 27 | u"homeDirectory", 28 | u"uuid", 29 | u"packageID", 30 | u"version", 31 | )) 32 | 33 | POSTINSTALL_TEMPLATE = """#!/bin/bash 34 | # 35 | # postinstall for local account install 36 | 37 | _POSTINSTALL_REQUIREMENTS_ 38 | _POSTINSTALL_ACTIONS_ 39 | if [ "$3" == "/" ]; then 40 | # we're operating on the boot volume 41 | _POSTINSTALL_LIVE_ACTIONS_ 42 | fi 43 | 44 | exit 0 45 | """ 46 | 47 | PI_REQ_PLIST_FUNCS = """ 48 | PlistArrayAdd() { 49 | # Add $value to $array_name in $plist_path, creating if necessary 50 | local plist_path="$1" 51 | local array_name="$2" 52 | local value="$3" 53 | local old_values 54 | local item 55 | 56 | old_values=$(/usr/libexec/PlistBuddy -c "Print :$array_name" "$plist_path" 2>/dev/null) 57 | if [[ $? == 1 ]]; then 58 | # Array doesn't exist, create it 59 | /usr/libexec/PlistBuddy -c "Add :$array_name array" "$plist_path" 60 | else 61 | # Array already exists, check if array already contains value 62 | IFS=$'\\012' 63 | for item in $old_values; do 64 | unset IFS 65 | if [[ "$item" =~ ^\\ *$value$ ]]; then 66 | # Array already contains value 67 | return 0 68 | fi 69 | done 70 | unset IFS 71 | fi 72 | # Add item to array 73 | /usr/libexec/PlistBuddy -c "Add :$array_name: string \\"$value\\"" "$plist_path" 74 | } 75 | """ 76 | 77 | PI_ADD_ADMIN_GROUPS = """ 78 | ACCOUNT_TYPE=ADMIN # Used by read_package.py. 79 | PlistArrayAdd "$3/private/var/db/dslocal/nodes/Default/groups/admin.plist" users "_USERNAME_" && \\ 80 | PlistArrayAdd "$3/private/var/db/dslocal/nodes/Default/groups/admin.plist" groupmembers "_UUID_" 81 | """ 82 | 83 | PI_ENABLE_AUTOLOGIN = """ 84 | if [ "$3" == "/" ] ; then 85 | # work around path issue with 'defaults' 86 | /usr/bin/defaults write "/Library/Preferences/com.apple.loginwindow" autoLoginUser "_USERNAME_" 87 | else 88 | /usr/bin/defaults write "$3/Library/Preferences/com.apple.loginwindow" autoLoginUser "_USERNAME_" 89 | fi 90 | /bin/chmod 644 "$3/Library/Preferences/com.apple.loginwindow.plist" 91 | """ 92 | 93 | PI_LIVE_KILLDS = """ 94 | # kill local directory service so it will see our local 95 | # file changes -- it will automatically restart 96 | /usr/bin/killall DirectoryService 2>/dev/null || /usr/bin/killall opendirectoryd 2>/dev/null 97 | """ 98 | 99 | PACKAGE_INFO = """ 100 | 101 | 102 | 103 | 104 | 105 | """ 106 | 107 | 108 | ODC_HDR_SIZES = (6, 6, 6, 6, 6, 6, 6, 6, 11, 6, 11) 109 | 110 | def fix_cpio_owners(f, new_uid=lambda x: 0, new_gid=lambda x: 0): 111 | """ 112 | Change ownership of items in a cpio archive. new_uid and new_gid are 113 | functions that take an archive path as the argument, and return the 114 | new uid and gid. By default they are set to 0 (root:wheel). Returns 115 | a list of strings suitable for writelines(). 116 | """ 117 | output = list() 118 | while True: 119 | # Read and decode ODC header 120 | header = f.read(sum(ODC_HDR_SIZES)) 121 | values = list() 122 | offset = 0 123 | for size in ODC_HDR_SIZES: 124 | values.append(int(header[offset:offset+size], 8)) 125 | offset += size 126 | (magic, 127 | dev, 128 | ino, 129 | mode, 130 | uid, 131 | gid, 132 | nlink, 133 | rdev, 134 | mtime, 135 | namesize, 136 | filesize) = values 137 | # Read name and data 138 | name = f.read(namesize) 139 | data = f.read(filesize) 140 | # Generate a new record, replacing uid and gid 141 | output.append("%06o%06o%06o%06o%06o%06o%06o%06o%011o%06o%011o" % ( 142 | magic, 143 | dev, 144 | ino, 145 | mode, 146 | new_uid(name[:-1]), 147 | new_gid(name[:-1]), 148 | nlink, 149 | rdev, 150 | mtime, 151 | namesize, 152 | filesize)) 153 | output.append(name) 154 | output.append(data) 155 | # TRAILER!!! indicates the end of the archive 156 | if name == "TRAILER!!!\x00": 157 | break 158 | # Append padding 159 | output.append(f.read()) 160 | return output 161 | 162 | 163 | def get_bom_info(path, new_uid=lambda x: 0, new_gid=lambda x: 0): 164 | st = os.lstat(path) 165 | info = [ 166 | path, 167 | "%o" % st.st_mode, 168 | "%d/%d" % (new_uid(path), new_gid(path)), 169 | ] 170 | if not stat.S_ISDIR(st.st_mode): 171 | info.append("%d" % st.st_size) 172 | p = subprocess.Popen(["/usr/bin/cksum", path.encode("utf-8")], 173 | stdout=subprocess.PIPE) 174 | out, _ = p.communicate() 175 | cksum, space, rest = out.partition(" ") 176 | info.append(cksum) 177 | return info 178 | 179 | 180 | def generate_bom_lines(root_path): 181 | bom = list() 182 | old_cd = os.getcwd() 183 | os.chdir(root_path) 184 | bom.append("\t".join(get_bom_info("."))) 185 | for (dirpath, dirnames, filenames) in os.walk("."): 186 | for path in [os.path.join(dirpath, p) for p in sorted(dirnames + filenames)]: 187 | bom.append("\t".join(get_bom_info(path))) 188 | os.chdir(old_cd) 189 | return bom 190 | 191 | 192 | def get_size_and_nfiles(root): 193 | kbytes = 0 194 | nfiles = 0 195 | for dirpath, dirnames, filenames in os.walk(root): 196 | nfiles += len(dirnames) + len(filenames) 197 | for filename in filenames: 198 | path = os.path.join(dirpath, filename) 199 | info = os.lstat(path) 200 | nblocks = (info.st_size + 4095) / 4096 201 | kbytes += nblocks * 4 202 | return kbytes, nfiles + 1 203 | 204 | 205 | def shell(*args): 206 | sys.stdout.flush() 207 | return subprocess.call(args) 208 | 209 | 210 | def main(argv): 211 | 212 | input_data = plistlib.readPlist(sys.stdin) 213 | 214 | # Ensure all required keys are given on the command line. 215 | for key in REQUIRED_KEYS: 216 | if key not in input_data: 217 | print >>sys.stderr, "Missing key: %s" % repr(key) 218 | return 1 219 | 220 | # Create a dictionary with user attributes. 221 | user_plist = dict() 222 | user_plist[u"authentication_authority"] = [u";ShadowHash;HASHLIST:"] 223 | user_plist[u"ShadowHashData"] = [input_data[u"shadowHashData"]] 224 | user_plist[u"generateduid"] = [input_data[u"uuid"]] 225 | user_plist[u"gid"] = [u"20"] 226 | user_plist[u"home"] = [input_data[u"homeDirectory"]] 227 | user_plist[u"name"] = [input_data[u"accountName"]] 228 | user_plist[u"passwd"] = [u"********"] 229 | user_plist[u"realname"] = [input_data[u"fullName"]] 230 | user_plist[u"shell"] = [u"/bin/bash"] 231 | user_plist[u"uid"] = [input_data[u"userID"]] 232 | user_plist[u"_writers_hint"] = [input_data[u"accountName"]] 233 | user_plist[u"_writers_jpegphoto"] = [input_data[u"accountName"]] 234 | user_plist[u"_writers_passwd"] = [input_data[u"accountName"]] 235 | user_plist[u"_writers_picture"] = [input_data[u"accountName"]] 236 | user_plist[u"_writers_realname"] = [input_data[u"accountName"]] 237 | user_plist[u"_writers_UserCertificate"] = [input_data[u"accountName"]] 238 | if u"imagePath" in input_data: 239 | user_plist[u"picture"] = [input_data[u"imagePath"]] 240 | if u"imageData" in input_data: 241 | user_plist[u"jpegphoto"] = [input_data[u"imageData"]] 242 | 243 | # Get name, version, package ID, and kcpassword. 244 | utf8_username = input_data[u"accountName"].encode("utf-8") 245 | pkg_version = input_data[u"version"] 246 | pkg_name = "create_%s-%s" % (utf8_username, pkg_version) 247 | pkg_id = input_data[u"packageID"] 248 | if u"kcPassword" in input_data: 249 | kcpassword = input_data[u"kcPassword"].data 250 | else: 251 | kcpassword = None 252 | is_admin = input_data.get(u"isAdmin", False) 253 | 254 | # Create a package with the plist for our user. 255 | tmp_path = tempfile.mkdtemp() 256 | try: 257 | # Create a root for the package. 258 | pkg_root_path = os.path.join(tmp_path, "create_user") 259 | os.mkdir(pkg_root_path) 260 | # Create package structure inside root. 261 | os.makedirs(os.path.join(pkg_root_path, "private/var/db/dslocal/nodes"), 0755) 262 | os.makedirs(os.path.join(pkg_root_path, "private/var/db/dslocal/nodes/Default/users"), 0700) 263 | if kcpassword: 264 | os.makedirs(os.path.join(pkg_root_path, "private/etc"), 0755) 265 | # Save user plist. 266 | user_plist_name = "%s.plist" % utf8_username 267 | user_plist_path = os.path.join(pkg_root_path, 268 | "private/var/db/dslocal/nodes/Default/users", 269 | user_plist_name) 270 | plistlib.writePlist(user_plist, user_plist_path) 271 | os.chmod(user_plist_path, 0600) 272 | # Save kcpassword. 273 | if kcpassword: 274 | kcpassword_path = os.path.join(pkg_root_path, "private/etc/kcpassword") 275 | f = open(kcpassword_path, "w") 276 | f.write(kcpassword) 277 | f.close() 278 | os.chmod(kcpassword_path, 0600) 279 | # Create a flat package structure. 280 | flat_pkg_path = os.path.join(tmp_path, pkg_name + "_pkg") 281 | scripts_path = os.path.join(flat_pkg_path, "Scripts") 282 | os.makedirs(scripts_path, 0755) 283 | # Create postinstall script. 284 | pi_reqs = set() 285 | pi_actions = set() 286 | pi_live_actions = set() 287 | pi_live_actions.add(PI_LIVE_KILLDS) 288 | if is_admin: 289 | pi_actions.add(PI_ADD_ADMIN_GROUPS) 290 | pi_reqs.add(PI_REQ_PLIST_FUNCS) 291 | if kcpassword: 292 | pi_actions.add(PI_ENABLE_AUTOLOGIN) 293 | postinstall = POSTINSTALL_TEMPLATE 294 | postinstall = postinstall.replace("_POSTINSTALL_REQUIREMENTS_", "\n".join(pi_reqs)) 295 | postinstall = postinstall.replace("_POSTINSTALL_ACTIONS_", "\n".join(pi_actions)) 296 | postinstall = postinstall.replace("_POSTINSTALL_LIVE_ACTIONS_", "\n".join(pi_live_actions)) 297 | postinstall = postinstall.replace("_USERNAME_", utf8_username) 298 | postinstall = postinstall.replace("_UUID_", input_data[u"uuid"]) 299 | postinstall_path = os.path.join(scripts_path, "postinstall") 300 | f = open(postinstall_path, "w") 301 | f.write(postinstall) 302 | f.close() 303 | os.chmod(postinstall_path, 0755) 304 | # Create Bom. 305 | tmp_bom_path = os.path.join(tmp_path, "Bom.txt") 306 | f = open(tmp_bom_path, "w") 307 | f.write("\n".join(generate_bom_lines(pkg_root_path))) 308 | f.write("\n") 309 | f.close() 310 | bom_path = os.path.join(flat_pkg_path, "Bom") 311 | if shell("/usr/bin/mkbom", "-i", tmp_bom_path, bom_path) != 0: 312 | return 2 313 | # Create Payload. 314 | tmp_payload_path = os.path.join(tmp_path, "Payload") 315 | if shell("/usr/bin/ditto", "-cz", pkg_root_path, tmp_payload_path) != 0: 316 | return 2 317 | payload_path = os.path.join(flat_pkg_path, "Payload") 318 | user_payload_f = gzip.open(tmp_payload_path) 319 | root_payload_f = gzip.open(payload_path, "wb") 320 | root_payload_f.writelines(fix_cpio_owners(user_payload_f)) 321 | root_payload_f.close() 322 | user_payload_f.close() 323 | # Calculate size and number of files in payload. 324 | kbytes, nfiles = get_size_and_nfiles(pkg_root_path) 325 | # Create PackageInfo 326 | package_info_path = os.path.join(flat_pkg_path, "PackageInfo") 327 | package_info = PACKAGE_INFO 328 | package_info = package_info.replace("_BUNDLE_ID_", pkg_id) 329 | package_info = package_info.replace("_VERSION_", pkg_version) 330 | package_info = package_info.replace("_KBYTES_", "%d" % kbytes) 331 | package_info = package_info.replace("_NFILES_", "%d" % nfiles) 332 | f = open(package_info_path, "w") 333 | f.write(package_info) 334 | f.close() 335 | # Flatten package with pkgutil. 336 | pkg_path = os.path.join(tmp_path, pkg_name + ".pkg") 337 | if shell("/usr/sbin/pkgutil", "--flatten", flat_pkg_path, pkg_path) != 0: 338 | return 2 339 | # Write the flattened pkg to stdout as a plist. 340 | f = open(pkg_path) 341 | output_data = { 342 | u"data": plistlib.Data(f.read()) 343 | } 344 | f.close() 345 | plistlib.writePlist(output_data, sys.stdout) 346 | 347 | except (OSError, IOError), e: 348 | print >>sys.stderr, "Package creation failed: %s" % e 349 | return 2 350 | finally: 351 | shutil.rmtree(tmp_path, ignore_errors=True) 352 | 353 | return 0 354 | 355 | 356 | if __name__ == '__main__': 357 | sys.exit(main(sys.argv)) 358 | 359 | -------------------------------------------------------------------------------- /CreateUserPkg/dropimagehere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/CreateUserPkg/dropimagehere.png -------------------------------------------------------------------------------- /CreateUserPkg/en.lproj/CUPDocument.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1070 5 | 12A269 6 | 2549 7 | 1187 8 | 624.00 9 | 10 | com.apple.InterfaceBuilder.CocoaPlugin 11 | 2549 12 | 13 | 14 | NSBox 15 | NSButton 16 | NSButtonCell 17 | NSCustomObject 18 | NSImageCell 19 | NSImageView 20 | NSMenu 21 | NSMenuItem 22 | NSPopUpButton 23 | NSPopUpButtonCell 24 | NSTextField 25 | NSTextFieldCell 26 | NSView 27 | NSWindowTemplate 28 | 29 | 30 | com.apple.InterfaceBuilder.CocoaPlugin 31 | 32 | 33 | PluginDependencyRecalculationVersion 34 | 35 | 36 | 37 | 38 | CUPDocument 39 | 40 | 41 | FirstResponder 42 | 43 | 44 | 15 45 | 2 46 | {{1030, 450}, {534, 459}} 47 | 1886912512 48 | Window 49 | NSWindow 50 | View 51 | 52 | {534, 459} 53 | {534, 459} 54 | 55 | 56 | 256 57 | 58 | 59 | 60 | 12 61 | 62 | 63 | 64 | 274 65 | 66 | 67 | 68 | 268 69 | {{143, 312}, {357, 22}} 70 | 71 | 72 | 73 | _NS:9 74 | YES 75 | 76 | -1804599231 77 | 272630784 78 | 79 | 80 | LucidaGrande 81 | 13 82 | 1044 83 | 84 | _NS:9 85 | 86 | YES 87 | 88 | 6 89 | System 90 | textBackgroundColor 91 | 92 | 3 93 | MQA 94 | 95 | 96 | 97 | 6 98 | System 99 | textColor 100 | 101 | 3 102 | MAA 103 | 104 | 105 | 106 | NO 107 | 108 | 109 | 110 | 268 111 | {{31, 314}, {107, 17}} 112 | 113 | 114 | 115 | _NS:1505 116 | YES 117 | 118 | 68157504 119 | 71304192 120 | Full Name: 121 | 122 | _NS:1505 123 | 124 | 125 | 6 126 | System 127 | controlColor 128 | 129 | 3 130 | MC42NjY2NjY2NjY3AA 131 | 132 | 133 | 134 | 6 135 | System 136 | controlTextColor 137 | 138 | 139 | 140 | NO 141 | 142 | 143 | 144 | 268 145 | {{143, 270}, {229, 22}} 146 | 147 | 148 | 149 | _NS:9 150 | YES 151 | 152 | -1804599231 153 | 272630784 154 | 155 | 156 | _NS:9 157 | 158 | YES 159 | 160 | 161 | 162 | NO 163 | 164 | 165 | 166 | 268 167 | {{143, 228}, {229, 22}} 168 | 169 | 170 | 171 | _NS:9 172 | YES 173 | 174 | -1804599231 175 | 272630784 176 | 177 | 178 | _NS:9 179 | 180 | YES 181 | 182 | 183 | 184 | NO 185 | 186 | 187 | 188 | 268 189 | {{143, 186}, {229, 22}} 190 | 191 | 192 | 193 | _NS:9 194 | YES 195 | 196 | -1804599231 197 | 272630784 198 | 199 | 200 | _NS:9 201 | 202 | YES 203 | 204 | 205 | 206 | NO 207 | 208 | 209 | 210 | 268 211 | {{31, 272}, {107, 17}} 212 | 213 | 214 | 215 | _NS:1505 216 | YES 217 | 218 | 68157504 219 | 71304192 220 | Account name: 221 | 222 | _NS:1505 223 | 224 | 225 | 226 | 227 | NO 228 | 229 | 230 | 231 | 268 232 | {{31, 230}, {107, 17}} 233 | 234 | 235 | 236 | _NS:1505 237 | YES 238 | 239 | 68157504 240 | 71304192 241 | Password: 242 | 243 | _NS:1505 244 | 245 | 246 | 247 | 248 | NO 249 | 250 | 251 | 252 | 268 253 | {{31, 188}, {107, 17}} 254 | 255 | 256 | 257 | _NS:1505 258 | YES 259 | 260 | 68157504 261 | 71304192 262 | Verify: 263 | 264 | _NS:1505 265 | 266 | 267 | 268 | 269 | NO 270 | 271 | 272 | 273 | 268 274 | {{143, 144}, {70, 22}} 275 | 276 | 277 | 278 | _NS:9 279 | YES 280 | 281 | -1804599231 282 | 272630784 283 | 284 | 285 | _NS:9 286 | 287 | YES 288 | 289 | 290 | 291 | NO 292 | 293 | 294 | 295 | 268 296 | {{31, 146}, {107, 17}} 297 | 298 | 299 | 300 | _NS:1505 301 | YES 302 | 303 | 68157504 304 | 71304192 305 | User ID: 306 | 307 | _NS:1505 308 | 309 | 310 | 311 | 312 | NO 313 | 314 | 315 | 316 | 268 317 | {{218, 146}, {96, 17}} 318 | 319 | 320 | 321 | _NS:1505 322 | YES 323 | 324 | 68157504 325 | 71304192 326 | Account type: 327 | 328 | _NS:1505 329 | 330 | 331 | 332 | 333 | NO 334 | 335 | 336 | 337 | 268 338 | {{143, 102}, {357, 22}} 339 | 340 | 341 | 342 | _NS:9 343 | YES 344 | 345 | -1804599231 346 | 272630784 347 | 348 | 349 | _NS:9 350 | 351 | YES 352 | 353 | 354 | 355 | NO 356 | 357 | 358 | 359 | 268 360 | {{31, 104}, {107, 17}} 361 | 362 | 363 | 364 | _NS:1505 365 | YES 366 | 367 | 68157504 368 | 71304192 369 | Home directory: 370 | 371 | _NS:1505 372 | 373 | 374 | 375 | 376 | NO 377 | 378 | 379 | 380 | 268 381 | {{143, 60}, {357, 22}} 382 | 383 | 384 | 385 | _NS:9 386 | YES 387 | 388 | -1804599231 389 | 272630784 390 | 391 | 392 | _NS:9 393 | 394 | YES 395 | 396 | 397 | 398 | NO 399 | 400 | 401 | 402 | 268 403 | {{31, 62}, {107, 17}} 404 | 405 | 406 | 407 | _NS:1505 408 | YES 409 | 410 | 68157504 411 | 71304192 412 | UUID: 413 | 414 | _NS:1505 415 | 416 | 417 | 418 | 419 | NO 420 | 421 | 422 | 423 | 268 424 | 425 | Apple PDF pasteboard type 426 | Apple PICT pasteboard type 427 | Apple PNG pasteboard type 428 | NSFilenamesPboardType 429 | NeXT Encapsulated PostScript v1.2 pasteboard type 430 | NeXT TIFF v4.0 pasteboard type 431 | 432 | {{391, 183}, {112, 112}} 433 | 434 | 435 | 436 | _NS:9 437 | YES 438 | 439 | 134217728 440 | 33554432 441 | 442 | NSImage 443 | dropimagehere 444 | 445 | _NS:9 446 | 0 447 | 0 448 | 2 449 | NO 450 | 451 | NO 452 | YES 453 | 454 | 455 | 456 | 268 457 | {{196, 19}, {123, 18}} 458 | 459 | 460 | 461 | _NS:9 462 | YES 463 | 464 | 67108864 465 | 268435456 466 | Automatic login 467 | 468 | _NS:9 469 | 470 | 1211912448 471 | 2 472 | 473 | NSImage 474 | NSSwitch 475 | 476 | 477 | NSSwitch 478 | 479 | 480 | 481 | 200 482 | 25 483 | 484 | NO 485 | 486 | 487 | 488 | 268 489 | {{316, 141}, {187, 26}} 490 | 491 | 492 | 493 | _NS:9 494 | YES 495 | 496 | -2076180416 497 | 2048 498 | 499 | _NS:9 500 | 501 | 109199360 502 | 129 503 | 504 | 505 | 400 506 | 75 507 | 508 | 509 | Administrator 510 | 511 | 1048576 512 | 2147483647 513 | 1 514 | 515 | NSImage 516 | NSMenuCheckmark 517 | 518 | 519 | NSImage 520 | NSMenuMixedState 521 | 522 | _popUpItemAction: 523 | 524 | 525 | YES 526 | 527 | OtherViews 528 | 529 | 530 | 531 | Standard 532 | 533 | 1048576 534 | 2147483647 535 | 536 | 537 | _popUpItemAction: 538 | 539 | 540 | 541 | 542 | 543 | 544 | 1 545 | 1 546 | YES 547 | YES 548 | 2 549 | 550 | NO 551 | 552 | 553 | {{1, 1}, {518, 348}} 554 | 555 | 556 | 557 | _NS:11 558 | 559 | 560 | {{7, 98}, {520, 364}} 561 | 562 | 563 | 564 | _NS:9 565 | {250, 250} 566 | {0, 0} 567 | 568 | 67108864 569 | 0 570 | 571 | 572 | LucidaGrande 573 | 11 574 | 3100 575 | 576 | 577 | 578 | 3 579 | MCAwLjgwMDAwMDAxMTkAA 580 | 581 | 582 | 583 | 1 584 | 0 585 | 2 586 | NO 587 | 588 | 589 | 590 | 268 591 | {{400, 13}, {124, 32}} 592 | 593 | 594 | _NS:9 595 | YES 596 | 597 | 67108864 598 | 134217728 599 | Save Package 600 | 601 | _NS:9 602 | 603 | -2038284288 604 | 129 605 | 606 | 607 | 200 608 | 25 609 | 610 | NO 611 | 612 | 613 | 614 | 268 615 | {{101, 61}, {285, 22}} 616 | 617 | 618 | 619 | _NS:9 620 | YES 621 | 622 | -1804599231 623 | 272630784 624 | 625 | 626 | _NS:9 627 | 628 | YES 629 | 630 | 631 | 632 | NO 633 | 634 | 635 | 636 | 268 637 | {{7, 63}, {92, 17}} 638 | 639 | 640 | 641 | _NS:1505 642 | YES 643 | 644 | 68157504 645 | 71304192 646 | Package ID: 647 | 648 | _NS:1505 649 | 650 | 651 | 652 | 653 | NO 654 | 655 | 656 | 657 | 268 658 | {{458, 61}, {60, 22}} 659 | 660 | 661 | 662 | _NS:9 663 | YES 664 | 665 | -1804599231 666 | 272630784 667 | 668 | 669 | _NS:9 670 | 671 | YES 672 | 673 | 674 | 675 | NO 676 | 677 | 678 | 679 | 268 680 | {{391, 63}, {62, 17}} 681 | 682 | 683 | 684 | _NS:1505 685 | YES 686 | 687 | 68157504 688 | 71304192 689 | Version: 690 | 691 | _NS:1505 692 | 693 | 694 | 695 | 696 | NO 697 | 698 | 699 | {534, 459} 700 | 701 | 702 | 703 | 704 | {{0, 0}, {1680, 1028}} 705 | {534, 481} 706 | {534, 481} 707 | YES 708 | 709 | 710 | NSApplication 711 | 712 | 713 | 714 | 715 | 716 | 717 | window 718 | 719 | 720 | 721 | 18 722 | 723 | 724 | 725 | fullName 726 | 727 | 728 | 729 | 100239 730 | 731 | 732 | 733 | accountName 734 | 735 | 736 | 737 | 100240 738 | 739 | 740 | 741 | password 742 | 743 | 744 | 745 | 100241 746 | 747 | 748 | 749 | verifyPassword 750 | 751 | 752 | 753 | 100242 754 | 755 | 756 | 757 | userID 758 | 759 | 760 | 761 | 100243 762 | 763 | 764 | 765 | homeDirectory 766 | 767 | 768 | 769 | 100245 770 | 771 | 772 | 773 | uuid 774 | 775 | 776 | 777 | 100246 778 | 779 | 780 | 781 | packageID 782 | 783 | 784 | 785 | 100247 786 | 787 | 788 | 789 | version 790 | 791 | 792 | 793 | 100248 794 | 795 | 796 | 797 | docWindow 798 | 799 | 800 | 801 | 100252 802 | 803 | 804 | 805 | didLeaveFullName: 806 | 807 | 808 | 809 | 100253 810 | 811 | 812 | 813 | didLeaveAccountName: 814 | 815 | 816 | 817 | 100254 818 | 819 | 820 | 821 | didLeavePassword: 822 | 823 | 824 | 825 | 100255 826 | 827 | 828 | 829 | didLeavePassword: 830 | 831 | 832 | 833 | 100256 834 | 835 | 836 | 837 | image 838 | 839 | 840 | 841 | 100260 842 | 843 | 844 | 845 | automaticLogin 846 | 847 | 848 | 849 | 100273 850 | 851 | 852 | 853 | accountType 854 | 855 | 856 | 857 | 100280 858 | 859 | 860 | 861 | saveDocumentAs: 862 | 863 | 864 | 865 | 100257 866 | 867 | 868 | 869 | delegate 870 | 871 | 872 | 873 | 17 874 | 875 | 876 | 877 | 878 | 879 | 0 880 | 881 | 882 | 883 | 884 | 885 | -2 886 | 887 | 888 | File's Owner 889 | 890 | 891 | -1 892 | 893 | 894 | First Responder 895 | 896 | 897 | 5 898 | 899 | 900 | 901 | 902 | 903 | Window 904 | 905 | 906 | 6 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | -3 920 | 921 | 922 | Application 923 | 924 | 925 | 100028 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 100029 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 100030 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 100031 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 100032 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 100033 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 100035 991 | 992 | 993 | 994 | 995 | 100036 996 | 997 | 998 | 999 | 1000 | 100037 1001 | 1002 | 1003 | 1004 | 1005 | 100038 1006 | 1007 | 1008 | 1009 | 1010 | 100039 1011 | 1012 | 1013 | 1014 | 1015 | 100040 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 100041 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 100042 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 100043 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 100044 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 100045 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 100046 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 100047 1072 | 1073 | 1074 | 1075 | 1076 | 1077 | 1078 | 1079 | 100049 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 100050 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1095 | 100051 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | 100052 1104 | 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1111 | 100053 1112 | 1113 | 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 100054 1120 | 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 100055 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | 1134 | 1135 | 100056 1136 | 1137 | 1138 | 1139 | 1140 | 100057 1141 | 1142 | 1143 | 1144 | 1145 | 100058 1146 | 1147 | 1148 | 1149 | 1150 | 100059 1151 | 1152 | 1153 | 1154 | 1155 | 100060 1156 | 1157 | 1158 | 1159 | 1160 | 100061 1161 | 1162 | 1163 | 1164 | 1165 | 100062 1166 | 1167 | 1168 | 1169 | 1170 | 100064 1171 | 1172 | 1173 | 1174 | 1175 | 100065 1176 | 1177 | 1178 | 1179 | 1180 | 100066 1181 | 1182 | 1183 | 1184 | 1185 | 100067 1186 | 1187 | 1188 | 1189 | 1190 | 100068 1191 | 1192 | 1193 | 1194 | 1195 | 100069 1196 | 1197 | 1198 | 1199 | 1200 | 100070 1201 | 1202 | 1203 | 1204 | 1205 | 100071 1206 | 1207 | 1208 | 1209 | 1210 | 100258 1211 | 1212 | 1213 | 1214 | 1215 | 1216 | 1217 | 1218 | 100259 1219 | 1220 | 1221 | 1222 | 1223 | 100271 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 100272 1232 | 1233 | 1234 | 1235 | 1236 | 100274 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | 1244 | 100275 1245 | 1246 | 1247 | 1248 | 1249 | 1250 | 1251 | 1252 | 100276 1253 | 1254 | 1255 | 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | 100277 1262 | 1263 | 1264 | 1265 | 1266 | 100278 1267 | 1268 | 1269 | 1270 | 1271 | 1272 | 1273 | com.apple.InterfaceBuilder.CocoaPlugin 1274 | com.apple.InterfaceBuilder.CocoaPlugin 1275 | com.apple.InterfaceBuilder.CocoaPlugin 1276 | com.apple.InterfaceBuilder.CocoaPlugin 1277 | com.apple.InterfaceBuilder.CocoaPlugin 1278 | com.apple.InterfaceBuilder.CocoaPlugin 1279 | com.apple.InterfaceBuilder.CocoaPlugin 1280 | com.apple.InterfaceBuilder.CocoaPlugin 1281 | com.apple.InterfaceBuilder.CocoaPlugin 1282 | com.apple.InterfaceBuilder.CocoaPlugin 1283 | com.apple.InterfaceBuilder.CocoaPlugin 1284 | com.apple.InterfaceBuilder.CocoaPlugin 1285 | com.apple.InterfaceBuilder.CocoaPlugin 1286 | com.apple.InterfaceBuilder.CocoaPlugin 1287 | com.apple.InterfaceBuilder.CocoaPlugin 1288 | NSSecureTextField 1289 | com.apple.InterfaceBuilder.CocoaPlugin 1290 | NSSecureTextField 1291 | com.apple.InterfaceBuilder.CocoaPlugin 1292 | com.apple.InterfaceBuilder.CocoaPlugin 1293 | com.apple.InterfaceBuilder.CocoaPlugin 1294 | com.apple.InterfaceBuilder.CocoaPlugin 1295 | com.apple.InterfaceBuilder.CocoaPlugin 1296 | com.apple.InterfaceBuilder.CocoaPlugin 1297 | com.apple.InterfaceBuilder.CocoaPlugin 1298 | com.apple.InterfaceBuilder.CocoaPlugin 1299 | com.apple.InterfaceBuilder.CocoaPlugin 1300 | com.apple.InterfaceBuilder.CocoaPlugin 1301 | com.apple.InterfaceBuilder.CocoaPlugin 1302 | com.apple.InterfaceBuilder.CocoaPlugin 1303 | com.apple.InterfaceBuilder.CocoaPlugin 1304 | com.apple.InterfaceBuilder.CocoaPlugin 1305 | com.apple.InterfaceBuilder.CocoaPlugin 1306 | com.apple.InterfaceBuilder.CocoaPlugin 1307 | com.apple.InterfaceBuilder.CocoaPlugin 1308 | com.apple.InterfaceBuilder.CocoaPlugin 1309 | com.apple.InterfaceBuilder.CocoaPlugin 1310 | com.apple.InterfaceBuilder.CocoaPlugin 1311 | com.apple.InterfaceBuilder.CocoaPlugin 1312 | com.apple.InterfaceBuilder.CocoaPlugin 1313 | com.apple.InterfaceBuilder.CocoaPlugin 1314 | com.apple.InterfaceBuilder.CocoaPlugin 1315 | com.apple.InterfaceBuilder.CocoaPlugin 1316 | com.apple.InterfaceBuilder.CocoaPlugin 1317 | com.apple.InterfaceBuilder.CocoaPlugin 1318 | com.apple.InterfaceBuilder.CocoaPlugin 1319 | CUPImageSelector 1320 | com.apple.InterfaceBuilder.CocoaPlugin 1321 | com.apple.InterfaceBuilder.CocoaPlugin 1322 | com.apple.InterfaceBuilder.CocoaPlugin 1323 | com.apple.InterfaceBuilder.CocoaPlugin 1324 | com.apple.InterfaceBuilder.CocoaPlugin 1325 | com.apple.InterfaceBuilder.CocoaPlugin 1326 | com.apple.InterfaceBuilder.CocoaPlugin 1327 | com.apple.InterfaceBuilder.CocoaPlugin 1328 | com.apple.InterfaceBuilder.CocoaPlugin 1329 | 1330 | 1331 | com.apple.InterfaceBuilder.CocoaPlugin 1332 | {{133, 170}, {507, 413}} 1333 | com.apple.InterfaceBuilder.CocoaPlugin 1334 | 1335 | 1336 | 1337 | 1338 | 1339 | 100280 1340 | 1341 | 1342 | 1343 | 1344 | CUPDocument 1345 | NSDocument 1346 | 1347 | id 1348 | id 1349 | id 1350 | 1351 | 1352 | 1353 | didLeaveAccountName: 1354 | id 1355 | 1356 | 1357 | didLeaveFullName: 1358 | id 1359 | 1360 | 1361 | didLeavePassword: 1362 | id 1363 | 1364 | 1365 | 1366 | NSTextField 1367 | NSPopUpButton 1368 | NSButton 1369 | NSWindow 1370 | NSTextField 1371 | NSTextField 1372 | CUPImageSelector 1373 | NSTextField 1374 | NSSecureTextField 1375 | NSTextField 1376 | NSTextField 1377 | NSSecureTextField 1378 | NSTextField 1379 | 1380 | 1381 | 1382 | accountName 1383 | NSTextField 1384 | 1385 | 1386 | accountType 1387 | NSPopUpButton 1388 | 1389 | 1390 | automaticLogin 1391 | NSButton 1392 | 1393 | 1394 | docWindow 1395 | NSWindow 1396 | 1397 | 1398 | fullName 1399 | NSTextField 1400 | 1401 | 1402 | homeDirectory 1403 | NSTextField 1404 | 1405 | 1406 | image 1407 | CUPImageSelector 1408 | 1409 | 1410 | packageID 1411 | NSTextField 1412 | 1413 | 1414 | password 1415 | NSSecureTextField 1416 | 1417 | 1418 | userID 1419 | NSTextField 1420 | 1421 | 1422 | uuid 1423 | NSTextField 1424 | 1425 | 1426 | verifyPassword 1427 | NSSecureTextField 1428 | 1429 | 1430 | version 1431 | NSTextField 1432 | 1433 | 1434 | 1435 | IBProjectSource 1436 | ./Classes/CUPDocument.h 1437 | 1438 | 1439 | 1440 | CUPImageSelector 1441 | NSImageView 1442 | 1443 | IBProjectSource 1444 | ./Classes/CUPImageSelector.h 1445 | 1446 | 1447 | 1448 | NSDocument 1449 | 1450 | id 1451 | id 1452 | id 1453 | id 1454 | id 1455 | id 1456 | 1457 | 1458 | 1459 | printDocument: 1460 | id 1461 | 1462 | 1463 | revertDocumentToSaved: 1464 | id 1465 | 1466 | 1467 | runPageLayout: 1468 | id 1469 | 1470 | 1471 | saveDocument: 1472 | id 1473 | 1474 | 1475 | saveDocumentAs: 1476 | id 1477 | 1478 | 1479 | saveDocumentTo: 1480 | id 1481 | 1482 | 1483 | 1484 | IBProjectSource 1485 | ./Classes/NSDocument.h 1486 | 1487 | 1488 | 1489 | 1490 | 0 1491 | IBCocoaFramework 1492 | 1493 | com.apple.InterfaceBuilder.CocoaPlugin.macosx 1494 | 1495 | 1496 | YES 1497 | 3 1498 | 1499 | {11, 11} 1500 | {10, 3} 1501 | {15, 15} 1502 | {64, 64} 1503 | 1504 | 1505 | 1506 | -------------------------------------------------------------------------------- /CreateUserPkg/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 2 | {\fonttbl\f0\fnil\fcharset0 LucidaGrande;} 3 | {\colortbl;\red255\green255\blue255;} 4 | \paperw8400\paperh11900\vieww9600\viewh8400\viewkind0 5 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc 6 | 7 | \f0\fs22 \cf0 Developed by Per Olofsson.\ 8 | \ 9 | Account creation through plist deployment developed by Greg Neagle.\ 10 | Bash plist modification code by Michael Lynn\ 11 | \ 12 | Icon by psdGraphics.com\ 13 | } -------------------------------------------------------------------------------- /CreateUserPkg/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /CreateUserPkg/en.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 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 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /CreateUserPkg/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // CreateUserPkg 4 | // 5 | // Created by Per Olofsson on 2012-06-27. 6 | // Copyright (c) 2012 University of Gothenburg. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | return NSApplicationMain(argc, (const char **)argv); 14 | } 15 | -------------------------------------------------------------------------------- /CreateUserPkg/read_package.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # read_package.py 4 | # CreateUserPkg 5 | # 6 | # Created by Per Olofsson on 2012-06-27. 7 | # Copyright (c) 2012 University of Gothenburg. All rights reserved. 8 | 9 | 10 | import os 11 | import sys 12 | import plistlib 13 | import subprocess 14 | import tempfile 15 | import shutil 16 | import glob 17 | from xml.etree import ElementTree 18 | 19 | 20 | REQUIRED_KEYS = set((u"input",)) 21 | 22 | 23 | def shell(*args): 24 | sys.stdout.flush() 25 | return subprocess.call(args) 26 | 27 | 28 | def main(argv): 29 | 30 | input = plistlib.readPlist(sys.stdin) 31 | 32 | # Decode arguments as --key=value to a dictionary. 33 | #args = dict() 34 | #for arg in [a.decode("utf-8") for a in argv[1:]]: 35 | # if not arg.startswith(u"--"): 36 | # print >>sys.stderr, "Invalid argument: %s" % repr(arg) 37 | # return 1 38 | # (key, equal, value) = arg[2:].partition(u"=") 39 | # if not equal: 40 | # print >>sys.stderr, "Invalid argument: %s" % repr(arg) 41 | # return 1 42 | # args[key] = value 43 | 44 | # Ensure all required keys are given on the command line. 45 | for key in REQUIRED_KEYS: 46 | if key not in input: 47 | print >>sys.stderr, "Missing key: %s" % repr(key) 48 | return 1 49 | 50 | # Create a temporary work directory, which is cleaned up in a finally clause. 51 | tmp_path = tempfile.mkdtemp() 52 | try: 53 | # Expand pkg with pkgutil. 54 | expanded_pkg_path = os.path.join(tmp_path, "expanded_pkg") 55 | if shell("/usr/sbin/pkgutil", "--expand", input[u"input"], expanded_pkg_path) != 0: 56 | return 2 57 | # Unpack the payload using ditto. 58 | compressed_payload_path = os.path.join(expanded_pkg_path, "Payload") 59 | payload_path = os.path.join(tmp_path, "payload") 60 | if shell("/usr/bin/ditto", "-x", compressed_payload_path, payload_path) != 0: 61 | return 2 62 | # Find the user plist inside. 63 | users_dir = os.path.join(payload_path, "private/var/db/dslocal/nodes/Default/users") 64 | user_plists = glob.glob(os.path.join(users_dir, "*.plist")) 65 | if len(user_plists) != 1: 66 | print >>sys.stderr, "Incompatible package" 67 | return 2 68 | user_plist_path = user_plists[0] 69 | # Read the plist. 70 | user = plistlib.readPlist(user_plist_path) 71 | # Read the PackageInfo. 72 | pkginfo_path = os.path.join(expanded_pkg_path, "PackageInfo") 73 | et = ElementTree.parse(pkginfo_path) 74 | pkg_info = et.getroot() 75 | # Read the shadow hash. 76 | #shadow_hash_path = os.path.join(payload_path, "private/var/db/shadow/hash", user[u"generateduid"][0]) 77 | #f = open(shadow_hash_path) 78 | #shadow_hash = f.read() 79 | #f.close() 80 | # Read kcpassword. 81 | kcpassword_path = os.path.join(payload_path, "private/etc/kcpassword") 82 | try: 83 | f = open(kcpassword_path) 84 | kcpassword = f.read() 85 | f.close() 86 | except IOError: 87 | kcpassword = None 88 | # Check if user is admin. 89 | is_admin = False 90 | if int(user[u"gid"][0]) == 80: 91 | is_admin = True 92 | try: 93 | f = open(os.path.join(expanded_pkg_path, "Scripts/postinstall")) 94 | postinstall = f.read() 95 | f.close() 96 | except IOError: 97 | pass 98 | if "ACCOUNT_TYPE=ADMIN" in postinstall: 99 | is_admin = True 100 | # Write the extracted document data to stdout as a plist. 101 | output_data = { 102 | u"fullName": user[u"realname"][0], 103 | u"accountName": user[u"name"][0], 104 | u"userID": user[u"uid"][0], 105 | u"isAdmin": is_admin, 106 | u"homeDirectory": user[u"home"][0], 107 | u"uuid": user[u"generateduid"][0], 108 | u"packageID": pkg_info.get("identifier"), 109 | u"version": pkg_info.get("version"), 110 | } 111 | if u"ShadowHashData" in user: 112 | output_data[u"shadowHashData"] = user[u"ShadowHashData"][0] 113 | if u"picture" in user and len(user[u"picture"]): 114 | output_data[u"imagePath"] = user[u"picture"][0] 115 | if u"jpegphoto" in user and len(user[u"jpegphoto"]): 116 | output_data[u"imageData"] = user[u"jpegphoto"][0] 117 | if kcpassword: 118 | output_data[u"kcPassword"] = plistlib.Data(kcpassword) 119 | plistlib.writePlist(output_data, sys.stdout) 120 | 121 | finally: 122 | shutil.rmtree(tmp_path, ignore_errors=True) 123 | 124 | return 0 125 | 126 | 127 | if __name__ == '__main__': 128 | sys.exit(main(sys.argv)) 129 | 130 | -------------------------------------------------------------------------------- /Distributions/create_dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | srcdir="$1" 4 | 5 | if [ -z "$srcdir" -o ! -d "$srcdir" ]; then 6 | echo "Usage: $0 srcdir" 7 | exit 1 8 | fi 9 | 10 | name=`basename "$srcdir"` 11 | 12 | echo "Cleaning" 13 | find "$srcdir" -name .DS_Store -print0 | xargs -0 rm -f 14 | 15 | echo "Adding documentation" 16 | markdown ../README.markdown | cat ../README.css - | tidy -q -i > "$srcdir/$name.html" 17 | 18 | echo "Creating image" 19 | dmg_fname="$srcdir.dmg" 20 | sudo -p "Password for %p@%h: " hdiutil create -srcfolder "$srcdir" -uid 0 -gid 0 -ov "$dmg_fname" 21 | sudo -p "Password for %p@%h: " chown ${UID} "$dmg_fname" 22 | -------------------------------------------------------------------------------- /Icons/Add-User-Icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/Icons/Add-User-Icon-16.png -------------------------------------------------------------------------------- /Icons/Add-User-Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/Icons/Add-User-Icon-32.png -------------------------------------------------------------------------------- /Icons/dropimagehere.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/Icons/dropimagehere.psd -------------------------------------------------------------------------------- /Icons/psd-web-user-icons.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/Icons/psd-web-user-icons.psd -------------------------------------------------------------------------------- /README.css: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | This utility creates packages that create local user accounts when installed. The packages can create users on 10.8+, and they are compatible with all workflows that can install standard installer pkgs. For the details on how the packages work, see Greg Neagle's article in the [May 2012 issue of MacTech](http://www.mactech.com/issue-TOCs-2012). 5 | 6 | 7 | Security Notes 8 | -------------- 9 | 10 | Packages created using this utility install a user account plist with ShadowHashData contaning a "SALTED-SHA512-PBKDF2" key derived from the provided password. This is the standard mechanism for storing user authentication data in macOS 10.8+. 11 | 12 | If you enable automatic login the password is stored in a kcpassword file, which is merely obfuscated and not encrypted - extracting the password (no matter how strong) is trivial. 13 | 14 | 15 | Credits 16 | ------- 17 | 18 | * Code by Per Olofsson, 19 | * User deployment method by Greg Neagle 20 | * Bash plist modification code by Michael Lynn 21 | 22 | 23 | Version History 24 | --------------- 25 | 26 | * 1.2.5 (in beta) 27 | * Fixed automatic login on 10.9+ (thanks to Greg Neagle). 28 | * 1.2.4 29 | * Fixed permissions for users to change their name, password, picture, etc (thanks to Greg Collen). 30 | * 1.2.3 31 | * Allow packages with empty password (thanks to Dan Keller). 32 | * 1.2.2 33 | * Fixed automatic logins that only worked on 2nd boot (thanks to Joseph Chilcote). 34 | * 1.2.1 35 | * Fixed empty password hash when you clicked Save without leaving the Password/Verify field (thanks to ih84ds). 36 | * 1.2 37 | * Added automatic login using kcpassword. 38 | * Package now adds users to admin group instead of using primary group 80 (thanks to Michael Lynn, Jason Bush, Greg Neagle). Primary group is always 20. 39 | * 1.1 40 | * create_user.pkg files can now be opened for editing. 41 | * Added user picture. 42 | * App is now sandboxed. 43 | * 1.0.3 44 | * Fixed Package ID and Version being set incorrectly. 45 | * 1.0.2 46 | * Fixed ownership of items in package Payload. 47 | * Changed salted sha1 shadow hash to upper case which fixes authentication on 10.5 and 10.6 (thanks to Allister Banks). 48 | * 1.0.1 49 | * Fixed postinstall script for 10.6 (thanks to Allister Banks). 50 | * 1.0 51 | * Initial release. 52 | 53 | 54 | License 55 | ------- 56 | 57 | Copyright 2012 Per Olofsson 58 | 59 | Licensed under the Apache License, Version 2.0 (the "License"); 60 | you may not use this file except in compliance with the License. 61 | You may obtain a copy of the License at 62 | 63 | http://www.apache.org/licenses/LICENSE-2.0 64 | 65 | Unless required by applicable law or agreed to in writing, software 66 | distributed under the License is distributed on an "AS IS" BASIS, 67 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 68 | See the License for the specific language governing permissions and 69 | limitations under the License. 70 | -------------------------------------------------------------------------------- /Test/c64breadbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/Test/c64breadbox.png -------------------------------------------------------------------------------- /Test/test_icon.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/Test/test_icon.eps -------------------------------------------------------------------------------- /Test/test_icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/Test/test_icon.jpg -------------------------------------------------------------------------------- /Test/test_icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/Test/test_icon.pdf -------------------------------------------------------------------------------- /Test/test_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/Test/test_icon.png -------------------------------------------------------------------------------- /Test/test_icon.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregneagle/CreateUserPkg/6cfb3d7c6d4b0fb2696891586c0ddde0db5b8972/Test/test_icon.tiff -------------------------------------------------------------------------------- /Test/test_package.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # test_package.py 4 | # CreateUserPkg 5 | # 6 | # Created by Per Olofsson on 2012-08-02. 7 | # Copyright (c) 2012 Per Olofsson. All rights reserved. 8 | 9 | 10 | ### FIXME: replace this with a proper unit test 11 | 12 | 13 | import os 14 | import sys 15 | import plistlib 16 | import optparse 17 | import subprocess 18 | import tempfile 19 | import shutil 20 | import glob 21 | import hashlib 22 | import binascii 23 | from xml.etree import ElementTree 24 | 25 | 26 | def shell(*args): 27 | sys.stdout.flush() 28 | return subprocess.call(args) 29 | 30 | 31 | def decode_kcpassword(pwd): 32 | key = (0x7D, 0x89, 0x52, 0x23, 0xD2, 0xBC, 0xDD, 0xEA, 0xA3, 0xB9, 0x1F) 33 | decoded = [] 34 | for i, c in enumerate(pwd): 35 | n = ord(c) 36 | k = key[i % len(key)] 37 | if n ^ k == 0: 38 | break 39 | decoded.append(chr(n ^k)) 40 | return "".join(decoded) 41 | 42 | 43 | def verify_shadowhash(shadowhash, pwd): 44 | salted_offset = 64 + 40 + 64 45 | salted_sha1_str = shadowhash[salted_offset:salted_offset + 48] 46 | salt_str = salted_sha1_str[:8] 47 | hash_str = salted_sha1_str[8:] 48 | hashed_pwd = hashlib.sha1(binascii.unhexlify(salt_str) + pwd).hexdigest().upper() 49 | return hashed_pwd == hash_str 50 | 51 | 52 | def main(argv): 53 | p = optparse.OptionParser() 54 | p.set_usage("""Usage: %prog [options] -p password createuserpackage.pkg""") 55 | p.add_option("-v", "--verbose", action="store_true", help="Verbose output.") 56 | p.add_option("-p", "--password", action="store", help="Password to verify against (required).") 57 | options, argv = p.parse_args(argv) 58 | if len(argv) != 2: 59 | print >>sys.stderr, p.get_usage() 60 | return 1 61 | if not options.password: 62 | print >>sys.stderr, p.get_usage() 63 | return 1 64 | 65 | pkg_path = argv[1] 66 | 67 | # Create a temporary work directory, which is cleaned up in a finally clause. 68 | tmp_path = tempfile.mkdtemp() 69 | try: 70 | # Expand pkg with pkgutil. 71 | expanded_pkg_path = os.path.join(tmp_path, "expanded_pkg") 72 | if shell("/usr/sbin/pkgutil", "--expand", pkg_path, expanded_pkg_path) != 0: 73 | return 2 74 | # Unpack the payload using ditto. 75 | compressed_payload_path = os.path.join(expanded_pkg_path, "Payload") 76 | payload_path = os.path.join(tmp_path, "payload") 77 | if shell("/usr/bin/ditto", "-x", compressed_payload_path, payload_path) != 0: 78 | return 2 79 | # Find the user plist inside. 80 | users_dir = os.path.join(payload_path, "private/var/db/dslocal/nodes/Default/users") 81 | user_plists = glob.glob(os.path.join(users_dir, "*.plist")) 82 | if len(user_plists) != 1: 83 | print >>sys.stderr, "Incompatible package" 84 | return 2 85 | user_plist_path = user_plists[0] 86 | # Read the plist. 87 | user = plistlib.readPlist(user_plist_path) 88 | if options.verbose: 89 | print "Account:" 90 | print " Full Name: %s" % user["realname"][0] 91 | print " Account name: %s" % user["name"][0] 92 | print " Password: %s" % user["passwd"][0] 93 | print " User ID: %s" % user["uid"][0] 94 | print " Group ID: %s" % user["gid"][0] 95 | print " Home directory: %s" % user["realname"][0] 96 | print " UUID: %s" % user["generateduid"][0] 97 | print 98 | # Read the PackageInfo. 99 | pkginfo_path = os.path.join(expanded_pkg_path, "PackageInfo") 100 | et = ElementTree.parse(pkginfo_path) 101 | pkg_info = et.getroot() 102 | if options.verbose: 103 | print "Package:" 104 | print " Package ID: %s" % pkg_info.get("identifier") 105 | print " Version: %s" % pkg_info.get("version") 106 | print 107 | 108 | # Verify the payload. 109 | try: 110 | # Verify the shadow hash. 111 | shadow_hash_path = os.path.join(payload_path, "private/var/db/shadow/hash", user[u"generateduid"][0]) 112 | f = open(shadow_hash_path) 113 | shadow_hash = f.read() 114 | f.close() 115 | if verify_shadowhash(shadow_hash, options.password): 116 | if options.verbose: 117 | print "shadowhash: OK" 118 | else: 119 | print "shadowhash: FAILED" 120 | return 2 121 | # Verify kcpassword. 122 | kcpassword_path = os.path.join(payload_path, "private/etc/kcpassword") 123 | try: 124 | f = open(kcpassword_path) 125 | kcpassword = f.read() 126 | f.close() 127 | if options.password == decode_kcpassword(kcpassword): 128 | if options.verbose: 129 | print "kcpassword: OK" 130 | else: 131 | print "kcpassword: FAILED" 132 | return 2 133 | except IOError: 134 | kcpassword = None 135 | except BaseException, e: 136 | print >>sys.stderr, "Exception when verifying payload: %s" % e 137 | return 2 138 | 139 | finally: 140 | shutil.rmtree(tmp_path, ignore_errors=True) 141 | 142 | return 0 143 | 144 | 145 | if __name__ == '__main__': 146 | sys.exit(main(sys.argv)) 147 | 148 | --------------------------------------------------------------------------------