diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml index 1c67a95..4ca6626 100644 --- a/.github/workflows/swiftlint.yml +++ b/.github/workflows/swiftlint.yml @@ -11,7 +11,7 @@ jobs: SwiftLint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Swiftlint verification uses: norio-nomura/action-swiftlint@3.2.1 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 382d149..2904691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] -N/A +### Added +- Connection to Jamf Pro can now use client credentials with Jamf Pro v10.49+ ([Issue #120](https://github.com/jamf/PPPC-Utility/issues/120)) [@macblazer](https://github.com/macblazer). + +### Changed +- PPPC Utility now requires macOS 11+ to run. It can still produce profiles usable on older versions of macOS. ## [1.5.0] - 2022-10-04 diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj index f205514..3403476 100644 --- a/PPPC Utility.xcodeproj/project.pbxproj +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -36,7 +36,6 @@ 6EC40A12214DF8FE00BE4F17 /* SecurityWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A11214DF8FE00BE4F17 /* SecurityWrapper.swift */; }; 6EC40A14214DFB5800BE4F17 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A13214DFB5800BE4F17 /* Model.swift */; }; 6EC40A16214ECF1E00BE4F17 /* SaveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A15214ECF1E00BE4F17 /* SaveViewController.swift */; }; - 6EC40A18214ECF2C00BE4F17 /* UploadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A17214ECF2C00BE4F17 /* UploadViewController.swift */; }; 6EC40A1C214EF87800BE4F17 /* SigningIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A1B214EF87800BE4F17 /* SigningIdentity.swift */; }; 71061E54246106C800822D35 /* LoadExecutableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71061E53246106C800822D35 /* LoadExecutableError.swift */; }; B5E09548250BCCFC00A40409 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E09547250BCCFC00A40409 /* Alert.swift */; }; @@ -44,8 +43,12 @@ C03270BA28636330008B38E0 /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03270B928636330008B38E0 /* SemanticVersion.swift */; }; C03270C028636397008B38E0 /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03270BE28636397008B38E0 /* Networking.swift */; }; C03270C128636397008B38E0 /* JamfProAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03270BF28636397008B38E0 /* JamfProAPIClient.swift */; }; + C05844B82AD4512D00141353 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05844B72AD4512D00141353 /* Token.swift */; }; + C05844BE2AD45F7900141353 /* UploadInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05844BD2AD45F7900141353 /* UploadInfoView.swift */; }; C07961E228749A36007B98A7 /* TokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07961E128749A36007B98A7 /* TokenTests.swift */; }; C07961E428749A51007B98A7 /* NetworkAuthManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07961E328749A51007B98A7 /* NetworkAuthManagerTests.swift */; }; + C07B1FB82AF596D80075E38B /* UploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07B1FB72AF596D80075E38B /* UploadManager.swift */; }; + C0A2B5422B1A5D5C0007F510 /* JamfProAPIClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A2B5412B1A5D5C0007F510 /* JamfProAPIClientTests.swift */; }; C0A85DB5279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig in Resources */ = {isa = PBXBuildFile; fileRef = C0A85DB4279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig */; }; C0E0383F27A30C7100A23FA2 /* PPPCServiceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E0383D27A30C7100A23FA2 /* PPPCServiceInfo.swift */; }; C0E0384027A30C7100A23FA2 /* PPPCServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E0383E27A30C7100A23FA2 /* PPPCServicesManager.swift */; }; @@ -103,7 +106,6 @@ 6EC40A11214DF8FE00BE4F17 /* SecurityWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityWrapper.swift; sourceTree = ""; }; 6EC40A13214DFB5800BE4F17 /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; 6EC40A15214ECF1E00BE4F17 /* SaveViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveViewController.swift; sourceTree = ""; }; - 6EC40A17214ECF2C00BE4F17 /* UploadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadViewController.swift; sourceTree = ""; }; 6EC40A1B214EF87800BE4F17 /* SigningIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningIdentity.swift; sourceTree = ""; }; 71061E53246106C800822D35 /* LoadExecutableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadExecutableError.swift; sourceTree = ""; }; 97227C6726248CD7000F26C1 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; @@ -112,8 +114,12 @@ C03270B928636330008B38E0 /* SemanticVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SemanticVersion.swift; sourceTree = ""; }; C03270BE28636397008B38E0 /* Networking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = ""; }; C03270BF28636397008B38E0 /* JamfProAPIClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JamfProAPIClient.swift; sourceTree = ""; }; + C05844B72AD4512D00141353 /* Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; + C05844BD2AD45F7900141353 /* UploadInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadInfoView.swift; sourceTree = ""; }; C07961E128749A36007B98A7 /* TokenTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenTests.swift; sourceTree = ""; }; C07961E328749A51007B98A7 /* NetworkAuthManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkAuthManagerTests.swift; sourceTree = ""; }; + C07B1FB72AF596D80075E38B /* UploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadManager.swift; sourceTree = ""; }; + C0A2B5412B1A5D5C0007F510 /* JamfProAPIClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JamfProAPIClientTests.swift; sourceTree = ""; }; C0A85DB4279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "TestTCCUnsignedProfile-allLower.mobileconfig"; sourceTree = ""; }; C0E0383D27A30C7100A23FA2 /* PPPCServiceInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PPPCServiceInfo.swift; sourceTree = ""; }; C0E0383E27A30C7100A23FA2 /* PPPCServicesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PPPCServicesManager.swift; sourceTree = ""; }; @@ -257,6 +263,7 @@ 6EC40A11214DF8FE00BE4F17 /* SecurityWrapper.swift */, 6E651CC623143969001CC974 /* Views */, 6EC40A1E214EF89600BE4F17 /* View Controllers */, + C05844BC2AD45F7900141353 /* SwiftUI */, 6EC40A1D214EF87E00BE4F17 /* Model */, C03270BD28636397008B38E0 /* Networking */, 5F95AE0B23158AB5002E0A22 /* TCCProfileImporter */, @@ -298,7 +305,6 @@ children = ( 6EC409F2214D8FFA00BE4F17 /* TCCProfileViewController.swift */, 6EC40A15214ECF1E00BE4F17 /* SaveViewController.swift */, - 6EC40A17214ECF2C00BE4F17 /* UploadViewController.swift */, 6E6216F8215321CE0043DF18 /* OpenViewController.swift */, ); path = "View Controllers"; @@ -309,16 +315,27 @@ children = ( C0EE9A802863BE2B00738B6B /* NetworkAuthManager.swift */, C03270BE28636397008B38E0 /* Networking.swift */, + C07B1FB72AF596D80075E38B /* UploadManager.swift */, C03270BF28636397008B38E0 /* JamfProAPIClient.swift */, C0EE9A7E2863BDE300738B6B /* JamfProAPITypes.swift */, + C05844B72AD4512D00141353 /* Token.swift */, C0EE9A822863BEEB00738B6B /* URLSessionAsyncCompatibility.swift */, ); path = Networking; sourceTree = ""; }; + C05844BC2AD45F7900141353 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + C05844BD2AD45F7900141353 /* UploadInfoView.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; C07961E028749A36007B98A7 /* NetworkingTests */ = { isa = PBXGroup; children = ( + C0A2B5412B1A5D5C0007F510 /* JamfProAPIClientTests.swift */, C07961E328749A51007B98A7 /* NetworkAuthManagerTests.swift */, C07961E128749A36007B98A7 /* TokenTests.swift */, ); @@ -441,6 +458,7 @@ /* Begin PBXShellScriptBuildPhase section */ 49DB95D624991AA800F433CA /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -466,6 +484,7 @@ files = ( C07961E228749A36007B98A7 /* TokenTests.swift in Sources */, 34DED4D423FDCAFD00C53FB9 /* SwiftyCMSDecoder.swift in Sources */, + C0A2B5422B1A5D5C0007F510 /* JamfProAPIClientTests.swift in Sources */, C07961E428749A51007B98A7 /* NetworkAuthManagerTests.swift in Sources */, 5F95AE262315A7CB002E0A22 /* TCCProfileImporterTests.swift in Sources */, C01BEDBA28636F57001B0B3B /* SemanticVersionTests.swift in Sources */, @@ -492,9 +511,9 @@ C0E0383F27A30C7100A23FA2 /* PPPCServiceInfo.swift in Sources */, C0EE9A7F2863BDE300738B6B /* JamfProAPITypes.swift in Sources */, 6EC40A1C214EF87800BE4F17 /* SigningIdentity.swift in Sources */, - 6EC40A18214ECF2C00BE4F17 /* UploadViewController.swift in Sources */, C03270C128636397008B38E0 /* JamfProAPIClient.swift in Sources */, C0E0384027A30C7100A23FA2 /* PPPCServicesManager.swift in Sources */, + C07B1FB82AF596D80075E38B /* UploadManager.swift in Sources */, 6E651CCA231439CE001CC974 /* InfoButton.swift in Sources */, 6EC409F5214D95D200BE4F17 /* TCCProfile.swift in Sources */, 6EC40A12214DF8FE00BE4F17 /* SecurityWrapper.swift in Sources */, @@ -502,10 +521,12 @@ 345B01D623FDBF55008838B6 /* TCCProfileExtensions.swift in Sources */, 6EC409F3214D8FFA00BE4F17 /* TCCProfileViewController.swift in Sources */, 6E6216F9215321CE0043DF18 /* OpenViewController.swift in Sources */, + C05844B82AD4512D00141353 /* Token.swift in Sources */, 6EC40A10214DE3B200BE4F17 /* Executable.swift in Sources */, C0EE9A832863BEEB00738B6B /* URLSessionAsyncCompatibility.swift in Sources */, C0EE9A812863BE2B00738B6B /* NetworkAuthManager.swift in Sources */, 6EC40A16214ECF1E00BE4F17 /* SaveViewController.swift in Sources */, + C05844BE2AD45F7900141353 /* UploadInfoView.swift in Sources */, 6EB45830214FFCCB00BE5749 /* AppleEventRule.swift in Sources */, 5F90EBDF2319970000738D09 /* TCCProfileImportError.swift in Sources */, C03270BA28636330008B38E0 /* SemanticVersion.swift in Sources */, @@ -634,7 +655,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IBSC_NOTICES = NO; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -690,7 +711,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IBSC_NOTICES = NO; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/PPPC UtilityTests/ModelTests/ModelTests.swift b/PPPC UtilityTests/ModelTests/ModelTests.swift index 57548ae..a07cb54 100644 --- a/PPPC UtilityTests/ModelTests/ModelTests.swift +++ b/PPPC UtilityTests/ModelTests/ModelTests.swift @@ -185,7 +185,6 @@ class ModelTests: XCTestCase { } } - // swiftlint:disable:next function_body_length func testExportProfileWithAppleEventsAndLegacyAllowed() { // given let exe1 = Executable(identifier: "one", codeRequirement: "oneReq") diff --git a/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift b/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift new file mode 100644 index 0000000..308decd --- /dev/null +++ b/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift @@ -0,0 +1,27 @@ +// +// JamfProAPIClientTests.swift +// PPPC UtilityTests +// +// SPDX-License-Identifier: MIT +// Copyright (c) 2023 Jamf Software + +import Foundation +import XCTest + +@testable import PPPC_Utility + +class JamfProAPIClientTests: XCTestCase { + func testOAuthTokenRequest() throws { + // given + let authManager = NetworkAuthManager(username: "", password: "") + let apiClient = JamfProAPIClient(serverUrlString: "https://something", tokenManager: authManager) + + // when + let request = try apiClient.oauthTokenRequest(clientId: "mine&yours", clientSecret: "foo bar") + + // then + let body = try XCTUnwrap(request.httpBody) + let bodyString = String(data: body, encoding: .utf8) + XCTAssertEqual(bodyString, "grant_type=client_credentials&client_id=mine%26yours&client_secret=foo%20bar") + } +} diff --git a/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift b/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift index 33e2047..d54ddd4 100644 --- a/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift +++ b/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift @@ -38,12 +38,16 @@ class MockNetworking: Networking { super.init(serverUrlString: "https://example.com", tokenManager: tokenManager) } - override func getBearerToken() async throws -> Token { + override func getBearerToken(authInfo: AuthenticationInfo) async throws -> Token { if let error = errorToThrow { throw error } - return Token(value: "xyz", expireTime: "2950-06-22T22:05:58.81Z") + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let expiration = try XCTUnwrap(formatter.date(from: "2950-06-22T22:05:58.81Z")) + + return Token(value: "xyz", expiresAt: expiration) } } diff --git a/PPPC UtilityTests/NetworkingTests/TokenTests.swift b/PPPC UtilityTests/NetworkingTests/TokenTests.swift index 3ad32c3..973e1d5 100644 --- a/PPPC UtilityTests/NetworkingTests/TokenTests.swift +++ b/PPPC UtilityTests/NetworkingTests/TokenTests.swift @@ -31,9 +31,12 @@ import XCTest @testable import PPPC_Utility class TokenTests: XCTestCase { - func testPastIsNotValid() { + func testPastIsNotValid() throws { // given - let token = Token(value: "abc", expireTime: "2021-06-22T22:05:58.81Z") + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let expiration = try XCTUnwrap(formatter.date(from: "2021-06-22T22:05:58.81Z")) + let token = Token(value: "abc", expiresAt: expiration) // when let valid = token.isValid @@ -42,9 +45,12 @@ class TokenTests: XCTestCase { XCTAssertFalse(valid) } - func testFutureIsValid() { + func testFutureIsValid() throws { // given - let token = Token(value: "abc", expireTime: "2750-06-22T22:05:58.81Z") + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let expiration = try XCTUnwrap(formatter.date(from: "2750-06-22T22:05:58.81Z")) + let token = Token(value: "abc", expiresAt: expiration) // when let valid = token.isValid @@ -52,4 +58,68 @@ class TokenTests: XCTestCase { // then XCTAssertTrue(valid) } + + // MARK: - Decoding + + func testDecodeBasicAuthToken() throws { + // given + let jsonText = """ + { + "token": "abc", + "expires": "2750-06-22T22:05:58.81Z" + } + """ + let jsonData = try XCTUnwrap(jsonText.data(using: .utf8)) + let decoder = JSONDecoder() + + // when + let actual = try decoder.decode(Token.self, from: jsonData) + + // then + XCTAssertEqual(actual.value, "abc") + XCTAssertNotNil(actual.expiresAt) + XCTAssertTrue(actual.isValid) + } + + func testDecodeExpiredBasicAuthToken() throws { + // given + let jsonText = """ + { + "token": "abc", + "expires": "1970-10-24T22:05:58.81Z" + } + """ + let jsonData = try XCTUnwrap(jsonText.data(using: .utf8)) + let decoder = JSONDecoder() + + // when + let actual = try decoder.decode(Token.self, from: jsonData) + + // then + XCTAssertEqual(actual.value, "abc") + XCTAssertNotNil(actual.expiresAt) + XCTAssertFalse(actual.isValid) + } + + func testDecodeClientCredentialsAuthToken() throws { + // given + let jsonText = """ + { + "access_token": "abc", + "scope": "api-role:2", + "token_type": "Bearer", + "expires_in": 599 + } + """ + let jsonData = try XCTUnwrap(jsonText.data(using: .utf8)) + let decoder = JSONDecoder() + + // when + let actual = try decoder.decode(Token.self, from: jsonData) + + // then + XCTAssertEqual(actual.value, "abc") + XCTAssertNotNil(actual.expiresAt) + XCTAssertTrue(actual.isValid) + } } diff --git a/README.md b/README.md index 79b07ad..8354018 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,18 @@ Profiles can be saved locally either signed or unsigned. ## Upload to Jamf Pro +PPPC Utility can use bearer token authentication (or basic authentication as a fallback for versions of Jamf Pro older than v10.34) to any supported +Jamf Pro version using the username and password of a Jamf Pro user account. The user account at minimum needs the two privileges indicated below. + +Jamf Pro 10.49 and higher can use OAuth client credentials to access the API. The client ID and client secret generated by Jamf Pro in the +"API Roles and clients" settings are used during the PPPC Utility upload process. When setting up the API Role, these are the permissions that +PPPC Utility requires to upload the profiles. + +#### Required API Permissions + +- "Create macOS Configuration Profiles" - primary permission to upload profiles; each upload from PPPC Utility creates a new profile. +- "Read Activation Code" - needed to retrieve the organization name that is placed in the profile. + ### Jamf Pro 10.7.1 and newer Starting in Jamf Pro 10.7.1 the Privacy Preferences Policy Control Payload can be uploaded to the API without being signed before uploading. diff --git a/Resources/Base.lproj/Main.storyboard b/Resources/Base.lproj/Main.storyboard index a50430a..7b15d80 100644 --- a/Resources/Base.lproj/Main.storyboard +++ b/Resources/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -723,7 +723,7 @@ - + @@ -797,13 +797,13 @@ - + - + @@ -811,7 +811,7 @@ - + @@ -861,7 +861,7 @@ - + @@ -869,7 +869,7 @@ - + @@ -919,7 +919,7 @@ - + @@ -927,7 +927,7 @@ - + @@ -979,7 +979,7 @@ - + @@ -987,7 +987,7 @@ - + @@ -1036,7 +1036,7 @@ - + @@ -1044,10 +1044,7 @@ - - - - + @@ -1059,6 +1056,9 @@ + + + @@ -1098,7 +1098,7 @@ - + @@ -1106,7 +1106,7 @@ - + @@ -1155,7 +1155,7 @@ - + @@ -1163,7 +1163,7 @@ - + @@ -1212,7 +1212,7 @@ - + @@ -1220,7 +1220,7 @@ - + @@ -1269,7 +1269,7 @@ - + @@ -1277,7 +1277,7 @@ - + @@ -1326,7 +1326,7 @@ - + @@ -1334,7 +1334,7 @@ - + @@ -1384,7 +1384,7 @@ - + @@ -1392,7 +1392,7 @@ - + @@ -1441,7 +1441,7 @@ - + @@ -1449,7 +1449,7 @@ - + @@ -1498,7 +1498,7 @@ - + @@ -1506,7 +1506,7 @@ - + @@ -1555,7 +1555,7 @@ - + @@ -1563,7 +1563,7 @@ - + @@ -1612,7 +1612,7 @@ - + @@ -1620,7 +1620,7 @@ - + @@ -1670,7 +1670,7 @@ - + @@ -1678,7 +1678,7 @@ - + @@ -1728,7 +1728,7 @@ - + @@ -1736,7 +1736,7 @@ - + @@ -1786,7 +1786,7 @@ - + @@ -1794,7 +1794,7 @@ - + @@ -1843,7 +1843,7 @@ - + @@ -1851,7 +1851,7 @@ - + @@ -1900,7 +1900,7 @@ - + @@ -1908,7 +1908,7 @@ - + @@ -2070,7 +2070,7 @@ - + @@ -2096,7 +2096,7 @@ - + @@ -2140,10 +2140,7 @@ - - - - + @@ -2154,6 +2151,9 @@ + + + @@ -2204,18 +2204,18 @@ - + @@ -2398,7 +2398,7 @@ - + @@ -2412,7 +2412,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NSIsNil - - - - - - [Required] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NSIsNil - - - - - - [Required] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NSIsNil - - - - - - [Required] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NSIsNil - - - - - - [Optional] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NSIsNil - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Id - - - - - - - - - - - - - - - - Name - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -3275,7 +2764,7 @@ Gw - + @@ -3290,7 +2779,7 @@ Gw - + @@ -3300,7 +2789,7 @@ Gw - + @@ -3318,7 +2807,7 @@ Gw - + @@ -3328,7 +2817,7 @@ Gw - + @@ -3346,7 +2835,7 @@ Gw - + @@ -3356,7 +2845,7 @@ Gw - + @@ -3374,7 +2863,7 @@ Gw - + @@ -3384,7 +2873,7 @@ Gw - + @@ -3402,7 +2891,7 @@ Gw - + @@ -3412,7 +2901,7 @@ Gw - + @@ -3433,7 +2922,7 @@ Gw