├── .circleci └── config.yml ├── .docker ├── Dockerfile ├── entry.sh ├── nginx.conf ├── openssl.cnf ├── settings.cfg.docker ├── supervisord.conf ├── uwsgi-commandment.ini └── uwsgi.ini ├── .dockerignore ├── .gitignore ├── .gitlab-ci.yml ├── .idea ├── blade.xml ├── encodings.xml ├── jsLibraryMappings.xml ├── misc.xml ├── modules.xml ├── vcs.xml └── watcherTasks.xml ├── LICENSE.txt ├── Pipfile ├── Pipfile.lock ├── README.rst ├── alembic.ini ├── assets ├── logo.afdesign └── logo.png ├── commandment ├── __init__.py ├── ac2 │ ├── __init__.py │ └── ac2_app.py ├── alembic │ ├── __init__.py │ ├── disabled_versions │ │ ├── 072fba4a2256_create_ad_payload_table.py │ │ ├── 18412434fb57_create_energy_saver_payload_table.py │ │ ├── 323a90039a6a_create_email_payload_table.py │ │ ├── 4eddbcb30464_create_mdm_payload_table.py │ │ ├── 8186b8ecf0fc_create_ad_cert_payload_table.py │ │ ├── 9dd4e48235e3_create_vpn_payload_table.py │ │ ├── d65049bf4b91_create_wifi_payload_table.py │ │ ├── da52b64b865f_create_apps_table.py │ │ ├── e47e29a9537c_create_certificate_payload_table.py │ │ └── fc0c134cbb2e_create_password_policy_payload_table.py │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 0201b96ab856_add_ios_available_os_updates_fields.py │ │ ├── 0ab46b2f6d8c_create_users_table.py │ │ ├── 0c4c448f4daf_create_device_users_table.py │ │ ├── 0e5babc5b9ee_create_vpp_licenses.py │ │ ├── 1005dc7dea01_os_update_settings.py │ │ ├── 13358fb3846b_create_subject_alternative_names_table.py │ │ ├── 1532dff16984_drop_device_groups.py │ │ ├── 2808deb9fc62_create_dep_configurations.py │ │ ├── 2f1507bf6dc1_create_application_manifests_table.py │ │ ├── 3061e56045eb_create_certificate_authority.py │ │ ├── 3dbf6db7f9eb_application_tags.py │ │ ├── 3fb4a904979c_general_cleanup.py │ │ ├── 50188ffaf0cd_create_devices_table.py │ │ ├── 5b98cc4af6c9_create_profiles_table.py │ │ ├── 6675e981817e_create_available_os_updates_table.py │ │ ├── 70ff84113e8f_create_tags.py │ │ ├── 71818e983100_create_application_sources_table.py │ │ ├── 71ecf957301a_create_commands_table.py │ │ ├── 7ab500f58a76_create_installed_payloads.py │ │ ├── 7cf5787a089e_add_dep_profile_relationships.py │ │ ├── 7d578eb75092_create_device_groups_table.py │ │ ├── 80fa1767c7e2_create_oauth_server_models.py │ │ ├── 875dcce0bf8b_create_vpp_users.py │ │ ├── 8c866896f76e_create_dep_join_tables.py │ │ ├── __init__.py │ │ ├── a1d5ffaa2092_create_installed_applications_table.py │ │ ├── a2e0af380181_create_dep_profiles.py │ │ ├── a35eeb5a216e_create_installed_profiles_table.py │ │ ├── a3ddaad5c358_add_dep_device_columns.py │ │ ├── af4ba256efde_create_certificates_table.py │ │ ├── b231394ab475_add_scep_config_source_types.py │ │ ├── b74ca08cfd9a_create_applications_tables.py │ │ ├── ba4849d8c8ad_create_device_group_devices_table.py │ │ ├── d5b32b5cc74e_add_dep_profile_id_to_device.py │ │ ├── dd74229d17b9_create_payload_dependencies_table.py │ │ ├── e16577adc4fd_create_installed_certificates_table.py │ │ ├── e5840df9a88a_create_scep_payload_table.py │ │ ├── e58afdc17baa_create_rsa_private_keys_table.py │ │ ├── e78274be170e_create_organizations_table.py │ │ ├── e947cdf82307_add_ios_installed_application_fields.py │ │ ├── e9b0a4f7b595_create_payloads_table.py │ │ ├── ea34ae3f1e7e_create_profile_payloads_table.py │ │ ├── f029ac1af3f0_create_vpp_accounts.py │ │ ├── f5237c7e2374_create_scep_config_table.py │ │ ├── f8eb70b3aa2b_create_application_manifests.py │ │ └── fa4d91c6aacf_create_managed_applications_table.py ├── api │ ├── __init__.py │ ├── app_json.py │ ├── app_jsonapi.py │ ├── configuration.py │ ├── resources.py │ └── schema.py ├── apns │ ├── __init__.py │ ├── app.py │ ├── mdmcert.py │ ├── push.py │ ├── schema.py │ └── threads.py ├── app.py ├── apps │ ├── __init__.py │ ├── app_jsonapi.py │ ├── models.py │ ├── resources.py │ └── schema.py ├── auth │ ├── __init__.py │ ├── app.py │ ├── models.py │ └── oauth2.py ├── cli.py ├── cms │ ├── __init__.py │ └── decorators.py ├── dbtypes.py ├── decorators.py ├── default_settings.py ├── dep │ ├── __init__.py │ ├── app.py │ ├── apple_schema.py │ ├── cli.py │ ├── dep.py │ ├── errors.py │ ├── models.py │ ├── resources.py │ ├── schema.py │ ├── smime.py │ └── threads.py ├── deprecated │ ├── models.py │ └── schema.py ├── enroll │ ├── __init__.py │ ├── app.py │ ├── profiles.py │ └── util.py ├── errors.py ├── inventory │ ├── __init__.py │ ├── api.py │ ├── models.py │ ├── resources.py │ └── schema.py ├── mdm │ ├── __init__.py │ ├── api.py │ ├── app.py │ ├── commands.py │ ├── decorators.py │ ├── handlers.py │ ├── models.py │ ├── resources.py │ ├── response_schema.py │ ├── routers.py │ ├── schema.py │ └── util.py ├── models.py ├── mutablelist.py ├── omdm │ ├── __init__.py │ └── models.py ├── pkg │ ├── __init__.py │ ├── appmanifest.py │ ├── manifest.py │ ├── old_app_manifest.py │ └── schema.py ├── pki │ ├── ca.py │ ├── models.py │ ├── openssl.py │ ├── ormutils.py │ ├── serialization.py │ └── ssl.py ├── plistutil │ ├── __init__.py │ └── nonewriter.py ├── profiles │ ├── __init__.py │ ├── ad.py │ ├── api.py │ ├── certificates.py │ ├── eap.py │ ├── email.py │ ├── energy.py │ ├── models.py │ ├── plist_schema.py │ ├── resources.py │ ├── schema.py │ ├── vpn.py │ └── wifi.py ├── signals.py ├── static │ ├── .gitignore │ ├── index.dev.html │ └── index.html ├── storage │ └── .gitignore ├── templates │ └── index.html ├── threads │ ├── __init__.py │ ├── startup_thread.py │ └── vpp_thread.py ├── utils.py └── vpp │ ├── __init__.py │ ├── app.py │ ├── cli.py │ ├── decorators.py │ ├── enum.py │ ├── errors.py │ ├── models.py │ ├── schema.py │ └── vpp.py ├── doc ├── .gitignore ├── Makefile ├── _static │ ├── config │ │ ├── nginx-commandment.conf │ │ └── uwsgi-commandment.ini │ ├── images │ │ └── asm │ │ │ └── upload-key.png │ └── uml │ │ ├── checkin.puml │ │ ├── commandqueue.puml │ │ └── models │ │ ├── Certificate.plantuml │ │ ├── Command.plantuml │ │ ├── InstalledApplication.plantuml │ │ ├── InstalledCertificate.plantuml │ │ └── InstalledProfile.plantuml ├── about-mdm.rst ├── api │ ├── certificates.rst │ ├── commands.rst │ ├── dep.rst │ ├── devices.rst │ ├── index.rst │ └── organization.rst ├── conf.py ├── dev │ └── MUSINGS.rst ├── developer │ ├── guide │ │ ├── architecture.rst │ │ ├── building.rst │ │ ├── index.rst │ │ └── running.rst │ ├── index.rst │ └── microservices.rst ├── guides │ ├── INSTALL.md │ ├── nginx.rst │ └── scep.rst ├── index.rst ├── installing │ ├── index.rst │ ├── install.rst │ ├── macos.rst │ └── ubuntu-server.rst ├── internal │ ├── api │ │ ├── api.rst │ │ ├── index.rst │ │ └── json-api.rst │ ├── cms │ │ ├── decorators.rst │ │ └── index.rst │ ├── core │ │ ├── index.rst │ │ ├── models │ │ │ ├── certificate.rst │ │ │ ├── certificate_request.rst │ │ │ ├── command.rst │ │ │ ├── device.rst │ │ │ ├── index.rst │ │ │ ├── installed_application.rst │ │ │ ├── installed_certificate.rst │ │ │ ├── installed_profile.rst │ │ │ ├── organization.rst │ │ │ ├── profile.rst │ │ │ └── rsa_private_key.rst │ │ └── signals.rst │ ├── decorators.rst │ ├── dep │ │ ├── dep.rst │ │ ├── index.rst │ │ ├── models.rst │ │ └── types.rst │ ├── enroll │ │ ├── app.rst │ │ └── index.rst │ ├── flask │ │ ├── configuration.rst │ │ └── index.rst │ ├── index.rst │ ├── mdm │ │ ├── app.rst │ │ ├── handlers.rst │ │ ├── index.rst │ │ └── types.rst │ ├── push.rst │ ├── vpp │ │ ├── decorators.rst │ │ ├── enum.rst │ │ ├── errors.rst │ │ ├── index.rst │ │ ├── operations.rst │ │ └── vpp.rst │ └── workers │ │ ├── index.rst │ │ └── runner.rst ├── make.bat ├── sadisplay │ └── models.py └── user │ ├── configuration.rst │ ├── dep.rst │ └── index.rst ├── docker-compose.yml ├── mypy.ini ├── pytest.ini ├── settings.cfg.example ├── setup.cfg ├── setup.py ├── simulators ├── depsim │ ├── .gitignore │ ├── Dockerfile │ ├── config.json │ ├── config_failures.json │ └── docker-compose.yml └── vppsim │ ├── .gitignore │ ├── Dockerfile │ ├── config.json │ └── docker-compose.yml ├── testdata ├── Authenticate │ ├── 10.11.x.xml │ ├── 10.12.2.xml │ ├── IOS-11.3.1.xml │ ├── IOS-9.x.xml │ └── iOS-11.3.1-cell.xml ├── AvailableOSUpdates │ ├── 10.12.5.xml │ ├── iOS-11.3.1.xml │ └── macOS-10.13.1.xml ├── CertificateList │ ├── 10.11.x.xml │ └── iOS-11.3.1.xml ├── CheckOut │ ├── 10.11.x.xml │ └── iOS-11.3.1.xml ├── DeviceInformation │ ├── 10.11.x.xml │ ├── iOS-11.3.1.xml │ └── macOS-10.13.1.xml ├── DeviceLock │ └── iOS-11.3.1.xml ├── Errors │ ├── 10.12.5-invalid-command.xml │ ├── 10.13.6-invalid-command.xml │ ├── error_invalid_request_type.plist │ ├── iOS-11.3.1-AvailableOSUpdatesFailure.xml │ ├── iOS-11.3.1-CommandFormatError.xml │ └── iOS-11.3.1-RemoveProfile-Unmanaged.xml ├── InstallApplication │ ├── iOS-11.3.1-alreadyprompting.xml │ ├── iOS-12.1-prompting.xml │ └── manifests │ │ ├── Microsoft_AutoUpdate-3.11.17101000.plist │ │ ├── OneDrive-17.3.7078.1101.plist │ │ ├── SkypeForBusinessInstaller-16.12.0.77.plist │ │ ├── dotnet-sdk-2.0.2-osx-x64.plist │ │ └── munkitools-3.1.0.3430.plist ├── InstalledApplicationList │ ├── 10.11.x.xml │ ├── iOS-11.3.1.xml │ └── iOS-12.1.xml ├── ManagedApplicationList │ ├── iOS-11.3.1-Failed.xml │ ├── iOS-12.1-Failed.xml │ ├── iOS-12.1-Installing.xml │ ├── iOS-12.1-Managed.xml │ └── iOS-12.1-RejectedPrompting.xml ├── NotNow │ └── iOS-11.3.1.xml ├── ProfileList │ ├── 10.11.x.xml │ └── iOS-11.3.1.xml ├── README.rst ├── SecurityInfo │ ├── 10.11.x.xml │ ├── IOS-9.x.xml │ ├── iOS-11.3.1.xml │ └── macOS-10.13.1.xml ├── TokenUpdate │ ├── 10.11.x-user.plist │ ├── 10.11.x.plist │ ├── 10.12.2-user.xml │ ├── 10.12.2.xml │ └── iOS-11.3.1.xml ├── decrypt_dep_token.sh ├── dep │ └── profile.xml ├── itunes │ ├── ios-search-slack.json │ └── mas-search-slack.json └── mdmclient-PKIOperation.der ├── tests ├── __init__.py ├── alembic_test.ini ├── api │ ├── __init__.py │ ├── conftest.py │ └── test_devices.py ├── client.py ├── conftest.py ├── dep │ ├── __init__.py │ ├── conftest.py │ ├── test_dep.py │ ├── test_dep_app.py │ ├── test_dep_failures.py │ ├── test_dep_live.py │ └── test_smime.py ├── mdm │ ├── __init__.py │ ├── conftest.py │ ├── test_available_os_updates.py │ ├── test_certificate_list.py │ ├── test_checkin.py │ ├── test_device_information.py │ ├── test_installed_application_list.py │ ├── test_profile_list.py │ └── test_security_info.py ├── pkg │ └── __init__.py ├── pki │ ├── __init__.py │ ├── conftest.py │ ├── test_ca.py │ ├── test_models.py │ ├── test_openssl.py │ └── test_ormutils.py ├── test_api_flat.py ├── test_mdmcert.py ├── threads │ ├── __init__.py │ └── test_startup_thread.py └── vpp │ ├── __init__.py │ ├── conftest.py │ └── vpp_test.py ├── travis-ci-settings.cfg └── ui ├── .eslintrc.js ├── .gitignore ├── .storybook ├── config.js ├── preview-head.html └── webpack.config.js ├── _deprecated ├── AssistantPage.tsx ├── DeviceGroupPage.tsx ├── DeviceGroupsPage.tsx ├── InternalCAPage.tsx ├── MDMPage.tsx ├── SCEPConfigurationForm.tsx ├── SSLPage.tsx └── assistant │ ├── APNSConfiguration.tsx │ ├── FinalStep.tsx │ ├── SCEPConfiguration.tsx │ └── SSLConfiguration.tsx ├── babel.config.js ├── package.json ├── sass ├── _dropzone.scss ├── _helper.scss ├── _nav.scss ├── _settings.scss ├── _upload.scss └── app.scss ├── src ├── @types │ ├── byte-size │ │ └── index.d.ts │ └── redux-api-middleware │ │ └── index.d.ts ├── components │ ├── ActionMenu.tsx │ ├── App.tsx │ ├── BareLayout.tsx │ ├── CertificateTypeIcon.tsx │ ├── CheckListItem.tsx │ ├── DeviceActions.tsx │ ├── Navigation.scss │ ├── Navigation.tsx │ ├── NavigationLayout.tsx │ ├── NavigationVertical.tsx │ ├── ProtectedRoute.tsx │ ├── RSAAApiErrorMessage.tsx │ ├── SearchInput.tsx │ ├── TagDropdown.tsx │ ├── devices │ │ ├── DEPDeviceDetail.tsx │ │ ├── IOSDeviceDetail.tsx │ │ ├── MacOSDeviceDetail.scss │ │ ├── MacOSDeviceDetail.tsx │ │ └── ModelIcon.tsx │ ├── errors │ │ └── ApiError.tsx │ ├── formik │ │ └── FormikCheckbox.tsx │ ├── forms │ │ ├── DEPAccountForm.tsx │ │ ├── DEPProfileForm.tsx │ │ ├── DeviceAuthForm.tsx │ │ └── OrganizationForm.tsx │ ├── itunes │ │ └── MASResult.tsx │ ├── modals │ │ ├── DeviceRenameModal.tsx │ │ └── ProfileUploadModal.tsx │ ├── react-table │ │ ├── AppName.tsx │ │ ├── ApplicationType.tsx │ │ ├── ByteSize.tsx │ │ ├── CommandStatus.tsx │ │ ├── DEPAccountServerName.tsx │ │ ├── DEPProfileName.tsx │ │ ├── DeviceName.tsx │ │ ├── ObjectLink.tsx │ │ ├── ProfileName.tsx │ │ ├── RelativeToNow.tsx │ │ └── SUISelectionTools.tsx │ ├── react-tables │ │ ├── AppDeployStatusTable.tsx │ │ ├── ApplicationsTable.tsx │ │ ├── DEPAccountsTable.tsx │ │ ├── DEPProfilesTable.tsx │ │ ├── DeviceApplicationsTable.tsx │ │ ├── DeviceCertificatesTable.tsx │ │ ├── DeviceCommandsTable.tsx │ │ ├── DeviceProfilesTable.tsx │ │ ├── DeviceUpdatesTable.tsx │ │ ├── DevicesTable.tsx │ │ └── ProfilesTable.tsx │ ├── semantic-ui │ │ ├── ButtonLink.tsx │ │ └── MenuItemLink.tsx │ └── vpp │ │ └── VPPAccountDetail.tsx ├── constants.ts ├── containers │ ├── AppStorePage.tsx │ ├── ApplicationPage.tsx │ ├── ApplicationsPage.tsx │ ├── DEPAccountPage.tsx │ ├── DEPProfilePage.tsx │ ├── DashboardPage.tsx │ ├── DevicePage.tsx │ ├── DeviceRename.tsx │ ├── DevicesPage.tsx │ ├── LoginPage.tsx │ ├── LogoutPage.tsx │ ├── ProfilePage.tsx │ ├── ProfileUpload.tsx │ ├── ProfilesPage.tsx │ ├── SettingsPage.tsx │ ├── applications │ │ ├── ApplicationDeviceStatus.tsx │ │ └── MacOSEntApplicationPage.tsx │ ├── config │ │ ├── DeviceAuthPage.tsx │ │ └── OrganizationPage.tsx │ ├── devices │ │ ├── DeviceApplications.tsx │ │ ├── DeviceCertificates.tsx │ │ ├── DeviceCommands.tsx │ │ ├── DeviceDetail.tsx │ │ ├── DeviceOSUpdates.tsx │ │ └── DeviceProfiles.tsx │ └── settings │ │ ├── APNSPage.tsx │ │ ├── DEPAccountSetupPage.tsx │ │ ├── DEPAccountsPage.tsx │ │ └── VPPAccountsPage.tsx ├── entry.tsx ├── flask-rest-jsonapi.ts ├── forms │ ├── ApplicationForm.tsx │ └── DeviceGroupForm.tsx ├── guards.ts ├── hooks │ └── useForm.ts ├── json-api-v1.ts ├── models.ts ├── reducers │ ├── index.ts │ └── interfaces.ts ├── selectors │ └── device.ts ├── store │ ├── applications │ │ ├── actions.ts │ │ ├── itunes.ts │ │ ├── list_reducer.ts │ │ ├── managed.ts │ │ ├── managed_reducer.ts │ │ ├── reducer.ts │ │ └── types.ts │ ├── assistant │ │ ├── actions.ts │ │ └── reducer.ts │ ├── auth │ │ ├── actions.ts │ │ ├── reducer.ts │ │ └── types.ts │ ├── certificates │ │ ├── actions.ts │ │ ├── ca_actions.ts │ │ ├── ca_reducer.ts │ │ ├── push_actions.ts │ │ ├── push_reducer.ts │ │ ├── reducer.ts │ │ ├── ssl_actions.ts │ │ ├── ssl_reducer.ts │ │ └── types.ts │ ├── commands │ │ ├── actions.ts │ │ └── reducer.ts │ ├── configuration │ │ ├── apns_reducer.ts │ │ ├── mdmcert_actions.ts │ │ ├── reducer.ts │ │ ├── scep_actions.ts │ │ ├── scep_reducer.ts │ │ ├── types.ts │ │ ├── vpp.ts │ │ └── vpp_reducer.ts │ ├── configureStore.ts │ ├── constants.ts │ ├── dep │ │ ├── account_reducer.ts │ │ ├── accounts_reducer.ts │ │ ├── actions.ts │ │ ├── profile_reducer.ts │ │ ├── profiles_reducer.ts │ │ ├── reducer.ts │ │ └── types.ts │ ├── device │ │ ├── actions.ts │ │ ├── applications.ts │ │ ├── available_os_updates_reducer.ts │ │ ├── certificates.ts │ │ ├── commands_reducer.ts │ │ ├── installed_applications_reducer.ts │ │ ├── installed_certificates_reducer.ts │ │ ├── installed_profiles_reducer.ts │ │ ├── profiles.ts │ │ ├── reducer.ts │ │ ├── types.ts │ │ └── updates.ts │ ├── device_groups │ │ ├── actions.ts │ │ ├── reducer.ts │ │ └── types.ts │ ├── devices │ │ ├── actions.ts │ │ └── devices.ts │ ├── json-api.ts │ ├── mdm.ts │ ├── organization │ │ ├── actions.ts │ │ ├── reducer.ts │ │ └── types.ts │ ├── pki │ │ ├── actions.ts │ │ └── types.ts │ ├── profile │ │ └── reducer.ts │ ├── profiles │ │ ├── actions.ts │ │ ├── reducer.ts │ │ └── types.ts │ ├── redux-api-middleware.ts │ ├── table │ │ ├── actions.ts │ │ ├── reducer.ts │ │ └── types.ts │ └── tags │ │ ├── actions.ts │ │ ├── reducer.ts │ │ └── types.ts └── stories │ ├── DEPProfileForm.tsx │ ├── index.ts │ └── redux.tsx ├── tsconfig.json ├── tslint.json ├── webpack.config.hmr.js ├── webpack.config.js └── webpack.config.prod.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/ruby:2.4.1 6 | steps: 7 | - checkout 8 | - run: echo "A first hello" -------------------------------------------------------------------------------- /.docker/openssl.cnf: -------------------------------------------------------------------------------- 1 | [req] 2 | distinguished_name = req_distinguished_name 3 | req_extensions = v3_req 4 | 5 | [req_distinguished_name] 6 | countryName = Country Name (2 letter code) 7 | countryName_default = AU 8 | stateOrProvinceName = State or Province Name (full name) 9 | stateOrProvinceName_default = New South Wales 10 | localityName = Locality Name (eg, city) 11 | localityName_default = Sydney 12 | organizationalUnitName = Organizational Unit Name (eg, section) 13 | organizationalUnitName_default = Domain Control Validated 14 | commonName = commandment.test 15 | commonName_max = 64 16 | 17 | [ v3_req ] 18 | # Extensions to add to a certificate request 19 | basicConstraints = CA:FALSE 20 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 21 | subjectAltName = @alt_names 22 | 23 | [alt_names] 24 | DNS.1 = localhost 25 | DNS.2 = mac.local 26 | -------------------------------------------------------------------------------- /.docker/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:uwsgi] 5 | command=/usr/local/bin/uwsgi --ini /etc/uwsgi/uwsgi.ini --ini /etc/uwsgi/uwsgi-commandment.ini --die-on-term --need-app 6 | stdout_logfile=/dev/stdout 7 | stdout_logfile_maxbytes=0 8 | stderr_logfile=/dev/stderr 9 | stderr_logfile_maxbytes=0 10 | 11 | [program:nginx] 12 | command=/usr/sbin/nginx 13 | stdout_logfile=/dev/stdout 14 | stdout_logfile_maxbytes=0 15 | stderr_logfile=/dev/stderr 16 | stderr_logfile_maxbytes=0 17 | # Graceful stop, see http://nginx.org/en/docs/control.html 18 | stopsignal=QUIT 19 | -------------------------------------------------------------------------------- /.docker/uwsgi-commandment.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | base = /commandment 3 | 4 | pythonpath = %(base) 5 | module = commandment:create_app() 6 | plugins = python3 7 | 8 | env = COMMANDMENT_SETTINGS=/settings.cfg 9 | 10 | master = true 11 | processes = 4 12 | enable-threads = true 13 | die-on-term = true 14 | -------------------------------------------------------------------------------- /.docker/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket = /tmp/uwsgi.sock 3 | chown-socket = nginx:nginx 4 | chmod-socket = 664 5 | # Graceful shutdown on SIGTERM, see https://github.com/unbit/uwsgi/issues/849#issuecomment-118869386 6 | hook-master-start = unix_signal:15 gracefully_kill_them_all 7 | -------------------------------------------------------------------------------- /.idea/blade.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jesse Peterson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Commandment Open Source MDM 3 | =========================== 4 | 5 | .. image:: https://travis-ci.org/cmdmnt/commandment.svg?branch=master 6 | :target: https://travis-ci.org/cmdmnt/commandment 7 | 8 | Commandment is an Open Source Apple MDM with support for managing iOS and macOS devices. 9 | 10 | The source code is available under an `MIT license `_. 11 | 12 | ------------ 13 | Requirements 14 | ------------ 15 | 16 | * Apple MDM Push Certificate and private key (in PEM format) 17 | * Obtain a free Push Certificate from `mdmcert.download `_. 18 | * Alternatively requires an Apple Enterprise Developer account (US$300/year) with the MDM vendor option enabled. 19 | * A trusted TLS certificate for the MDM. 20 | * `Python 3.6+ `_ 21 | 22 | ------------- 23 | Documentation 24 | ------------- 25 | 26 | The user, developer and API documentation is available at: 27 | 28 | http://cmdmnt.github.io/commandment/ 29 | 30 | ------------------ 31 | Bugs, issues, etc. 32 | ------------------ 33 | 34 | Please report any issues, bugs, suggestions, feedback, etc. 35 | to the `issue tracker `_ of this project. 36 | 37 | Also for discussion, and support, join us in the #commandment channel in the `MacAdmins Slack `_ ! 38 | 39 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | [alembic] 2 | # path to migration scripts 3 | script_location = %(here)s/commandment/alembic 4 | 5 | sqlalchemy.url = sqlite:///commandment.db 6 | 7 | # Logging configuration 8 | [loggers] 9 | keys = root,sqlalchemy,alembic 10 | 11 | [handlers] 12 | keys = console 13 | 14 | [formatters] 15 | keys = generic 16 | 17 | [logger_root] 18 | level = DEBUG 19 | handlers = console 20 | qualname = 21 | 22 | [logger_sqlalchemy] 23 | level = INFO 24 | handlers = 25 | qualname = sqlalchemy.engine 26 | 27 | [logger_alembic] 28 | level = DEBUG 29 | handlers = 30 | qualname = alembic 31 | 32 | [handler_console] 33 | class = StreamHandler 34 | args = (sys.stderr,) 35 | level = NOTSET 36 | formatter = generic 37 | 38 | [formatter_generic] 39 | format = %(levelname)-5.5s [%(name)s] %(message)s 40 | datefmt = %H:%M:%S 41 | -------------------------------------------------------------------------------- /assets/logo.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/assets/logo.afdesign -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/assets/logo.png -------------------------------------------------------------------------------- /commandment/ac2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/commandment/ac2/__init__.py -------------------------------------------------------------------------------- /commandment/ac2/ac2_app.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, current_app 2 | 3 | ac2_app = Blueprint('ac2_app', __name__) 4 | 5 | 6 | @ac2_app.route('/MDMServiceConfig') 7 | def mdm_service_config(): 8 | """Apple Configurator 2 checks this route to figure out which enrollment profile it should use.""" 9 | public_hostname = current_app.config.get('PUBLIC_HOSTNAME', 'localhost') 10 | port = current_app.config.get('PORT', 443) 11 | 12 | return jsonify({ 13 | 'dep_enrollment_url': 'https://{}:{}/dep/profile'.format(public_hostname, port), 14 | 'dep_anchor_certs_url': 'https://{}:{}/dep/anchor_certs'.format(public_hostname, port), 15 | 'trust_profile_url': 'https://{}:{}/enroll/trust.mobileconfig'.format(public_hostname, port) 16 | }) 17 | -------------------------------------------------------------------------------- /commandment/alembic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/commandment/alembic/__init__.py -------------------------------------------------------------------------------- /commandment/alembic/disabled_versions/9dd4e48235e3_create_vpn_payload_table.py: -------------------------------------------------------------------------------- 1 | """Create vpn_payload table 2 | 3 | Revision ID: 9dd4e48235e3 4 | Revises: e5840df9a88a 5 | Create Date: 2017-05-19 19:59:55.582629 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '9dd4e48235e3' 14 | down_revision = 'e5840df9a88a' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('vpn_payload', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('user_defined_name', sa.String(), nullable=True), 23 | sa.Column('override_primary', sa.Boolean(), nullable=True), 24 | sa.Column('vpn_type', sa.Enum('L2TP', 'PPTP', 'IPSec', 'IKEv2', 'AlwaysOn', 'VPN', name='vpntype'), nullable=False), 25 | sa.Column('vpn_sub_type', sa.String(), nullable=True), 26 | sa.Column('provider_bundle_identifier', sa.String(), nullable=True), 27 | sa.Column('on_demand_enabled', sa.Integer(), nullable=True), 28 | sa.ForeignKeyConstraint(['id'], ['payloads.id'], ), 29 | sa.PrimaryKeyConstraint('id') 30 | ) 31 | 32 | 33 | def downgrade(): 34 | op.drop_table('vpn_payload') 35 | -------------------------------------------------------------------------------- /commandment/alembic/disabled_versions/da52b64b865f_create_apps_table.py: -------------------------------------------------------------------------------- 1 | """Create apps table 2 | 3 | Revision ID: da52b64b865f 4 | Revises: 5 | Create Date: 2017-05-18 22:27:44.830159 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'da52b64b865f' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('apps', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('filename', sa.String(), nullable=False), 23 | sa.Column('filesize', sa.Integer(), nullable=False), 24 | sa.Column('md5_hash', sa.String(length=32), nullable=False), 25 | sa.Column('md5_chunk_size', sa.Integer(), nullable=False), 26 | sa.Column('md5_chunk_hashes', sa.Text(), nullable=True), 27 | sa.Column('bundle_ids_json', sa.Text(), nullable=True), 28 | sa.Column('pkg_ids_json', sa.Text(), nullable=True), 29 | sa.PrimaryKeyConstraint('id'), 30 | sa.UniqueConstraint('filename') 31 | ) 32 | 33 | 34 | def downgrade(): 35 | op.drop_table('app') 36 | 37 | -------------------------------------------------------------------------------- /commandment/alembic/disabled_versions/e47e29a9537c_create_certificate_payload_table.py: -------------------------------------------------------------------------------- 1 | """Create certificate_payload table 2 | 3 | Revision ID: e47e29a9537c 4 | Revises: 072fba4a2256 5 | Create Date: 2017-05-19 19:51:20.672688 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e47e29a9537c' 14 | down_revision = '072fba4a2256' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('certificate_payload', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('certificate_file_name', sa.String(), nullable=True), 23 | sa.Column('payload_content', sa.LargeBinary(), nullable=True), 24 | sa.Column('password', sa.String(), nullable=True), 25 | sa.ForeignKeyConstraint(['id'], ['payloads.id'], ), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | 29 | 30 | def downgrade(): 31 | op.drop_table('certificate_payload') 32 | -------------------------------------------------------------------------------- /commandment/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements 10 | 11 | from alembic import op 12 | import sqlalchemy as sa 13 | import commandment.dbtypes 14 | ${imports if imports else ""} 15 | 16 | from alembic import context 17 | 18 | # revision identifiers, used by Alembic. 19 | revision = ${repr(up_revision)} 20 | down_revision = ${repr(down_revision)} 21 | branch_labels = ${repr(branch_labels)} 22 | depends_on = ${repr(depends_on)} 23 | 24 | 25 | def upgrade(): 26 | schema_upgrades() 27 | if context.get_x_argument(as_dictionary=True).get('data', None): 28 | data_upgrades() 29 | 30 | 31 | def downgrade(): 32 | if context.get_x_argument(as_dictionary=True).get('data', None): 33 | data_downgrades() 34 | schema_downgrades() 35 | 36 | 37 | def schema_upgrades(): 38 | """schema upgrade migrations go here.""" 39 | ${upgrades if upgrades else "pass"} 40 | 41 | 42 | def schema_downgrades(): 43 | """schema downgrade migrations go here.""" 44 | ${downgrades if downgrades else "pass"} 45 | 46 | 47 | def data_upgrades(): 48 | """Add any optional data upgrade migrations here!""" 49 | pass 50 | 51 | 52 | def data_downgrades(): 53 | """Add any optional data downgrade migrations here!""" 54 | pass 55 | -------------------------------------------------------------------------------- /commandment/alembic/versions/0ab46b2f6d8c_create_users_table.py: -------------------------------------------------------------------------------- 1 | """Create users table 2 | 3 | Revision ID: 0ab46b2f6d8c 4 | Revises: f5237c7e2374 5 | Create Date: 2017-05-19 19:35:12.126022 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '0ab46b2f6d8c' 14 | down_revision = 'f5237c7e2374' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('users', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('name', sa.String(), nullable=True), 23 | sa.Column('fullname', sa.String(), nullable=True), 24 | sa.Column('password', sa.String(), nullable=True), 25 | sa.PrimaryKeyConstraint('id') 26 | ) 27 | 28 | 29 | def downgrade(): 30 | op.drop_table('users') 31 | -------------------------------------------------------------------------------- /commandment/alembic/versions/0c4c448f4daf_create_device_users_table.py: -------------------------------------------------------------------------------- 1 | """Create device_users table 2 | 3 | Revision ID: 0c4c448f4daf 4 | Revises: 7d578eb75092 5 | Create Date: 2017-05-18 22:32:52.087025 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import commandment.dbtypes 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '0c4c448f4daf' 15 | down_revision = '7d578eb75092' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | op.create_table('device_users', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('udid', commandment.dbtypes.GUID(), nullable=False), 24 | sa.Column('user_id', commandment.dbtypes.GUID(), nullable=False), 25 | sa.Column('long_name', sa.String(), nullable=True), 26 | sa.Column('short_name', sa.String(), nullable=True), 27 | sa.Column('need_sync_response', sa.Boolean(), nullable=True), 28 | sa.Column('user_configuration', sa.Boolean(), nullable=True), 29 | sa.Column('digest_challenge', sa.String(), nullable=True), 30 | sa.Column('auth_token', sa.String(), nullable=True), 31 | sa.PrimaryKeyConstraint('id') 32 | ) 33 | 34 | 35 | def downgrade(): 36 | op.drop_table('device_users') 37 | -------------------------------------------------------------------------------- /commandment/alembic/versions/13358fb3846b_create_subject_alternative_names_table.py: -------------------------------------------------------------------------------- 1 | """Create subject_alternative_names table 2 | 3 | Revision ID: 13358fb3846b 4 | Revises: ea34ae3f1e7e 5 | Create Date: 2017-05-19 19:48:09.977131 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '13358fb3846b' 14 | down_revision = 'ea34ae3f1e7e' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('subject_alternative_names', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('discriminator', sa.Enum('RFC822Name', 'DNSName', 'UniformResourceIdentifier', 'DirectoryName', 'RegisteredID', 'IPAddress', 'OtherName', name='subjectalternativenametype'), nullable=False), 23 | sa.Column('str_value', sa.String(), nullable=True), 24 | sa.Column('octet_value', sa.LargeBinary(), nullable=True), 25 | sa.PrimaryKeyConstraint('id') 26 | ) 27 | 28 | 29 | def downgrade(): 30 | op.drop_table('subject_alternative_names') 31 | -------------------------------------------------------------------------------- /commandment/alembic/versions/71818e983100_create_application_sources_table.py: -------------------------------------------------------------------------------- 1 | """Create application_sources table 2 | 3 | Revision ID: 71818e983100 4 | Revises: da52b64b865f 5 | Create Date: 2017-05-18 22:29:40.036227 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '71818e983100' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('application_sources', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('name', sa.String(), nullable=True), 23 | sa.Column('source_type', sa.Enum('S3', 'Munki', name='appsourcetype'), nullable=True), 24 | sa.Column('endpoint', sa.String(), nullable=True), 25 | sa.Column('mount_uri', sa.String(), nullable=True), 26 | sa.Column('use_ssl', sa.Boolean(), nullable=True), 27 | sa.Column('access_key', sa.String(), nullable=True), 28 | sa.Column('secret_key', sa.String(), nullable=True), 29 | sa.Column('bucket', sa.String(), nullable=True), 30 | sa.PrimaryKeyConstraint('id') 31 | ) 32 | 33 | 34 | def downgrade(): 35 | op.drop_table('application_sources') 36 | -------------------------------------------------------------------------------- /commandment/alembic/versions/7d578eb75092_create_device_groups_table.py: -------------------------------------------------------------------------------- 1 | """Create device_groups table 2 | 3 | Revision ID: 7d578eb75092 4 | Revises: 71818e983100 5 | Create Date: 2017-05-18 22:31:16.686848 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '7d578eb75092' 14 | down_revision = '71818e983100' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('device_groups', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('name', sa.String(), nullable=False), 23 | sa.PrimaryKeyConstraint('id') 24 | ) 25 | 26 | 27 | def downgrade(): 28 | op.drop_table('device_groups') 29 | -------------------------------------------------------------------------------- /commandment/alembic/versions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/commandment/alembic/versions/__init__.py -------------------------------------------------------------------------------- /commandment/alembic/versions/b231394ab475_add_scep_config_source_types.py: -------------------------------------------------------------------------------- 1 | """add scep_config source types 2 | 3 | Revision ID: b231394ab475 4 | Revises: a3ddaad5c358 5 | Create Date: 2018-09-07 07:50:10.467330 6 | 7 | """ 8 | 9 | # From: http://alembic.zzzcomputing.com/en/latest/cookbook.html#conditional-migration-elements 10 | 11 | from alembic import op 12 | import sqlalchemy as sa 13 | import commandment.dbtypes 14 | 15 | 16 | from alembic import context 17 | 18 | # revision identifiers, used by Alembic. 19 | revision = 'b231394ab475' 20 | down_revision = 'a3ddaad5c358' 21 | branch_labels = None 22 | depends_on = None 23 | 24 | 25 | def upgrade(): 26 | schema_upgrades() 27 | 28 | 29 | def downgrade(): 30 | schema_downgrades() 31 | 32 | 33 | def schema_upgrades(): 34 | op.add_column('scep_config', sa.Column('source_type', sa.Enum('InternalPKCS12', 'InternalSCEP', 'ExternalSCEP', name='deviceidentitysources'), nullable=True)) 35 | 36 | 37 | def schema_downgrades(): 38 | op.drop_column('scep_config', 'source_type') 39 | 40 | -------------------------------------------------------------------------------- /commandment/alembic/versions/ba4849d8c8ad_create_device_group_devices_table.py: -------------------------------------------------------------------------------- 1 | """Create device_group_devices table 2 | 3 | Revision ID: ba4849d8c8ad 4 | Revises: a1d5ffaa2092 5 | Create Date: 2017-05-19 19:44:37.403554 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ba4849d8c8ad' 14 | down_revision = 'a1d5ffaa2092' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('device_group_devices', 21 | sa.Column('device_group_id', sa.Integer(), nullable=False), 22 | sa.Column('device_id', sa.Integer(), nullable=False), 23 | sa.ForeignKeyConstraint(['device_group_id'], ['device_groups.id'], ), 24 | sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ), 25 | sa.PrimaryKeyConstraint('device_group_id', 'device_id') 26 | ) 27 | 28 | 29 | def downgrade(): 30 | op.drop_table('device_group_devices') 31 | -------------------------------------------------------------------------------- /commandment/alembic/versions/dd74229d17b9_create_payload_dependencies_table.py: -------------------------------------------------------------------------------- 1 | """Create payload_dependencies table 2 | 3 | Revision ID: dd74229d17b9 4 | Revises: d65049bf4b91 5 | Create Date: 2017-05-19 20:02:17.116286 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import commandment.dbtypes 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'dd74229d17b9' 14 | down_revision = 'e5840df9a88a' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('payload_dependencies', 21 | sa.Column('payload_uuid', commandment.dbtypes.GUID(), nullable=True), 22 | sa.Column('depends_on_payload_uuid', commandment.dbtypes.GUID(), nullable=True), 23 | sa.ForeignKeyConstraint(['depends_on_payload_uuid'], ['payloads.uuid'], ), 24 | sa.ForeignKeyConstraint(['payload_uuid'], ['payloads.uuid'], ) 25 | ) 26 | 27 | 28 | def downgrade(): 29 | op.drop_table('payload_dependencies') 30 | -------------------------------------------------------------------------------- /commandment/alembic/versions/e58afdc17baa_create_rsa_private_keys_table.py: -------------------------------------------------------------------------------- 1 | """Create rsa_private_keys table 2 | 3 | Revision ID: e58afdc17baa 4 | Revises: 5b98cc4af6c9 5 | Create Date: 2017-05-19 19:32:28.454940 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e58afdc17baa' 14 | down_revision = '5b98cc4af6c9' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('rsa_private_keys', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('pem_data', sa.Text(), nullable=False), 23 | sa.PrimaryKeyConstraint('id') 24 | ) 25 | 26 | 27 | def downgrade(): 28 | op.drop_table('rsa_private_keys') 29 | -------------------------------------------------------------------------------- /commandment/alembic/versions/ea34ae3f1e7e_create_profile_payloads_table.py: -------------------------------------------------------------------------------- 1 | """Create profile_payloads table 2 | 3 | Revision ID: ea34ae3f1e7e 4 | Revises: ba4849d8c8ad 5 | Create Date: 2017-05-19 19:45:34.375475 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ea34ae3f1e7e' 14 | down_revision = 'ba4849d8c8ad' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table('profile_payloads', 21 | sa.Column('profile_id', sa.Integer(), nullable=True), 22 | sa.Column('payload_id', sa.Integer(), nullable=True), 23 | sa.ForeignKeyConstraint(['payload_id'], ['payloads.id'], ), 24 | sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], ) 25 | ) 26 | 27 | 28 | def downgrade(): 29 | op.drop_table('profile_payloads') 30 | -------------------------------------------------------------------------------- /commandment/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/commandment/api/__init__.py -------------------------------------------------------------------------------- /commandment/apns/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/commandment/apns/__init__.py -------------------------------------------------------------------------------- /commandment/apns/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class PushResponseFlatSchema(Schema): 5 | """This structure mimics the fields of an APNS2 service reply.""" 6 | apns_id = fields.Integer() 7 | status_code = fields.Integer() 8 | reason = fields.Str() 9 | timestamp = fields.DateTime() 10 | 11 | -------------------------------------------------------------------------------- /commandment/app.py: -------------------------------------------------------------------------------- 1 | from commandment import create_app 2 | 3 | app = create_app(None) 4 | -------------------------------------------------------------------------------- /commandment/apps/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ManagedAppStatus(Enum): 5 | """A list of possible Managed Application statuses returned by the `ManagedApplicationList` command.""" 6 | NeedsRedemption = 'NeedsRedemption' 7 | Redeeming = 'Redeeming' 8 | Prompting = 'Prompting' 9 | PromptingForLogin = 'PromptingForLogin' 10 | Installing = 'Installing' 11 | ValidatingPurchase = 'ValidatingPurchase' 12 | Managed = 'Managed' 13 | ManagedButUninstalled = 'ManagedButUninstalled' 14 | PromptingForUpdate = 'PromptingForUpdate' 15 | PromptingForUpdateLogin = 'PromptingForUpdateLogin' 16 | PromptingForManagement = 'PromptingForManagement' 17 | Updating = 'Updating' 18 | ValidatingUpdate = 'ValidatingUpdate' 19 | Unknown = 'Unknown' 20 | 21 | # Transient 22 | UserInstalledApp = 'UserInstalledApp' 23 | UserRejected = 'UserRejected' 24 | UpdateRejected = 'UpdateRejected' 25 | ManagementRejected = 'ManagementRejected' 26 | Failed = 'Failed' 27 | 28 | # Commandment ONLY - To indicate that the command for IA is queued but not yet acked 29 | Queued = 'Queued' 30 | -------------------------------------------------------------------------------- /commandment/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/commandment/auth/__init__.py -------------------------------------------------------------------------------- /commandment/auth/models.py: -------------------------------------------------------------------------------- 1 | from commandment.models import db 2 | from authlib.flask.oauth2.sqla import OAuth2ClientMixin, OAuth2TokenMixin 3 | 4 | 5 | class User(db.Model): 6 | __tablename__ = 'users' 7 | 8 | id = db.Column(db.Integer, primary_key=True) 9 | name = db.Column(db.String) 10 | fullname = db.Column(db.String) 11 | password = db.Column(db.String) 12 | 13 | def get_user_id(self): 14 | """This method is implemented as part of the Resource Owner interface for Authlib.""" 15 | return self.id 16 | 17 | 18 | class OAuth2Client(db.Model, OAuth2ClientMixin): 19 | """OAuth 2 Client""" 20 | __tablename__ = 'oauth2_clients' 21 | 22 | id = db.Column(db.Integer, primary_key=True) 23 | user_id = db.Column( 24 | db.Integer, db.ForeignKey('users.id', ondelete='CASCADE') 25 | ) 26 | user = db.relationship('User') 27 | 28 | 29 | class OAuth2Token(db.Model, OAuth2TokenMixin): 30 | """Bearer Token""" 31 | __tablename__ = 'oauth2_tokens' 32 | 33 | id = db.Column(db.Integer, primary_key=True) 34 | user_id = db.Column( 35 | db.Integer, db.ForeignKey('users.id', ondelete='CASCADE') 36 | ) 37 | user = db.relationship('User') 38 | 39 | -------------------------------------------------------------------------------- /commandment/dep/errors.py: -------------------------------------------------------------------------------- 1 | from requests import Response, HTTPError 2 | 3 | 4 | class DEPServiceError(HTTPError): 5 | """DEPServiceError inherits from request's HTTPError to provide the response and request as part of the exception. 6 | 7 | Additionally, the error tracks information about the body content as this can sometimes be the only way to 8 | distinguish an error. 9 | 10 | Attributes: 11 | text (str): The reserved string that was returned in the error body. 12 | """ 13 | def __init__(self, *args, **kwargs): 14 | super(DEPServiceError, self).__init__(*args, **kwargs) 15 | if 'response' in kwargs: 16 | # Quote characters (") must be stripped, because the body may contain the reason inside double quotes. 17 | self.text = kwargs.get('response').content.decode('utf8').strip("\"\n\r") 18 | else: 19 | self.text = "NO_REASON_GIVEN" 20 | 21 | def __str__(self): 22 | return '{}: {}'.format(self.response.status_code, self.text) 23 | 24 | 25 | class DEPClientError(Exception): 26 | """DEPClientError describes errors that happen on the client side, often as a result of failed validations.""" 27 | pass 28 | -------------------------------------------------------------------------------- /commandment/enroll/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class DeviceAttributes(Enum): 5 | """This enumeration describes all of the device attributes available to OTA profile enrolment. 6 | """ 7 | UDID = 'UDID' 8 | VERSION = 'VERSION' 9 | PRODUCT = 'PRODUCT' 10 | DEVICE_NAME = 'DEVICE_NAME' 11 | SERIAL = 'SERIAL' 12 | MODEL = 'MODEL' 13 | MAC_ADDRESS_EN0 = 'MAC_ADDRESS_EN0' 14 | MEID = 'MEID' 15 | IMEI = 'IMEI' 16 | ICCID = 'ICCID' 17 | COMPROMISED = 'COMPROMISED' 18 | DeviceID = 'DeviceID' 19 | # SPIROM = 'SPIROM' 20 | # MLB = 'MLB' 21 | 22 | 23 | AllDeviceAttributes = { 24 | DeviceAttributes.UDID.value, 25 | DeviceAttributes.VERSION.value, 26 | DeviceAttributes.PRODUCT.value, 27 | DeviceAttributes.DEVICE_NAME.value, 28 | DeviceAttributes.SERIAL.value, 29 | DeviceAttributes.MODEL.value, 30 | # DeviceAttributes.MAC_ADDRESS_EN0.value, 31 | DeviceAttributes.MEID.value, 32 | DeviceAttributes.IMEI.value, 33 | DeviceAttributes.ICCID.value, 34 | DeviceAttributes.COMPROMISED.value, 35 | DeviceAttributes.DeviceID.value, 36 | # DeviceAttributes.SPIROM.value, 37 | # DeviceAttributes.MLB.value, 38 | } 39 | 40 | -------------------------------------------------------------------------------- /commandment/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict 2 | from flask import jsonify 3 | 4 | 5 | class JSONAPIError(Exception): 6 | 7 | def __init__(self, title: str, status: int = 500, code: Optional[str] = None, detail: Optional[str] = None, 8 | source: Optional[Dict[str, str]] = None, meta=None, id=None): 9 | self.title = title 10 | self.status = status 11 | self.code = code 12 | self.detail = detail 13 | self.source = source 14 | self.meta = meta 15 | self.id = id 16 | 17 | def to_dict(self) -> Dict[str, any]: 18 | res = {'errors': []} 19 | error = {'title': self.title, 'status': self.status} 20 | if self.code is not None: 21 | error['code'] = self.code 22 | 23 | if self.detail is not None: 24 | error['detail'] = self.detail 25 | 26 | res['errors'].append(error) 27 | return res 28 | -------------------------------------------------------------------------------- /commandment/mdm/api.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from commandment.mdm.resources import CommandsList, CommandDetail 3 | from commandment.api.app_jsonapi import api 4 | 5 | api_app = Blueprint('inventory_api_app', __name__) 6 | 7 | # Commands 8 | api.route(CommandsList, 'commands_list', '/v1/commands', '/v1/devices//commands') 9 | api.route(CommandDetail, 'command_detail', '/v1/commands/') 10 | -------------------------------------------------------------------------------- /commandment/mdm/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | 4 | def handle_error_status(func): 5 | """This decorator looks at the request for an Error status, then handles the error accordingly: 6 | 7 | """ 8 | @wraps(func) 9 | def handler(*args, **kwargs): 10 | return func(*args, **kwargs) 11 | return handler 12 | 13 | 14 | -------------------------------------------------------------------------------- /commandment/mdm/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/commandment/mdm/models.py -------------------------------------------------------------------------------- /commandment/mdm/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow_jsonapi import fields 2 | from marshmallow_jsonapi.flask import Relationship, Schema 3 | 4 | 5 | class CommandSchema(Schema): 6 | class Meta: 7 | type_ = 'commands' 8 | self_view = 'api_app.command_detail' 9 | self_view_kwargs = {'command_id': ''} 10 | self_view_many = 'api_app.commands_list' 11 | strict = True 12 | 13 | id = fields.Int(dump_only=True) 14 | uuid = fields.Str(dump_only=True) 15 | request_type = fields.Str() 16 | status = fields.Str() 17 | queued_at = fields.DateTime() 18 | sent_at = fields.DateTime() 19 | acknowledged_at = fields.DateTime() 20 | after = fields.DateTime() 21 | ttl = fields.Int() 22 | 23 | device = Relationship( 24 | related_view='api_app.device_detail', 25 | related_view_kwargs={'device_id': ''}, 26 | type_='devices' 27 | ) 28 | 29 | -------------------------------------------------------------------------------- /commandment/omdm/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, current_app 2 | from uuid import uuid4 3 | import plistlib 4 | 5 | omdm_app = Blueprint('omdm_app', __name__) 6 | 7 | 8 | @omdm_app.route('/') 9 | def omdm(): 10 | faux_command = { 11 | 'CommandUUID': str(uuid4()), 12 | 'RequestType': 'OMAlert', 13 | 'Message': 'Hello World!' 14 | } 15 | 16 | return plistlib.dumps(faux_command), {'Content-Type': 'text/xml'} -------------------------------------------------------------------------------- /commandment/omdm/models.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | 5 | from sqlalchemy import Integer, String, ForeignKey, Table, Text, Boolean, DateTime, Enum as DBEnum, text, \ 6 | BigInteger, and_, or_, LargeBinary, Float 7 | 8 | -------------------------------------------------------------------------------- /commandment/pkg/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ManifestAssetKind(Enum): 5 | SoftwarePackage = 'software-package' 6 | FullSizeImage = 'full-size-image' 7 | DisplayImage = 'display-image' 8 | 9 | -------------------------------------------------------------------------------- /commandment/pkg/manifest.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | import hashlib 3 | import io 4 | 5 | # Required for InstallApplication to work. 6 | DEFAULT_MD5_CHUNK_SIZE = 10485760 7 | 8 | 9 | def chunked_hash(stream: Union[io.RawIOBase, io.BufferedIOBase], chunk_size: int = DEFAULT_MD5_CHUNK_SIZE) -> List[bytes]: 10 | """Create a list of hashes of chunk_size size in bytes. 11 | 12 | Args: 13 | stream (Union[io.RawIOBase, io.BufferedIOBase]): The steam containing the bytes to be hashed. 14 | chunk_size (int): The md5 chunk size. Default is 10485760 (which is required for InstallApplication). 15 | 16 | Returns: 17 | List[str]: A list of md5 hashes calculated for each chunk 18 | """ 19 | chunk = stream.read(chunk_size) 20 | hashes = [] 21 | 22 | while chunk is not None: 23 | h = hashlib.md5() 24 | h.update(chunk) 25 | md5 = h.digest() 26 | hashes.append(md5) 27 | chunk = stream.read(chunk_size) 28 | 29 | return hashes 30 | -------------------------------------------------------------------------------- /commandment/pkg/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class Asset(Schema): 5 | kind = fields.String(default='software-package') 6 | md5_size = fields.Integer(default=10485760) 7 | md5s = fields.List(fields.String()) 8 | url = fields.URL() 9 | needs_shine = fields.Boolean() 10 | 11 | 12 | class BundleItem(Schema): 13 | bundle_identifier = fields.String() 14 | bundle_version = fields.String() 15 | 16 | 17 | class Metadata(Schema): 18 | bundle_identifier = fields.String() 19 | bundle_version = fields.String() 20 | items = fields.Nested(BundleItem, many=True) 21 | kind = fields.String() 22 | sizeInBytes = fields.String() 23 | subtitle = fields.String() 24 | title = fields.String() 25 | 26 | 27 | class ManifestItem(Schema): 28 | assets = fields.Nested(Asset, many=True) 29 | metadata = fields.Nested(Metadata) 30 | 31 | 32 | class Manifest(Schema): 33 | items = fields.Nested(ManifestItem, many=True) 34 | 35 | -------------------------------------------------------------------------------- /commandment/pki/ca.py: -------------------------------------------------------------------------------- 1 | from flask import g, current_app 2 | import sqlalchemy.orm.exc 3 | 4 | from .models import CertificateAuthority 5 | from commandment.models import db, Device 6 | from commandment.pki.models import CertificateType, Certificate 7 | 8 | 9 | def get_ca() -> CertificateAuthority: 10 | if 'ca' not in g: 11 | try: 12 | ca = db.session.query(CertificateAuthority).filter_by(common_name='COMMANDMENT-CA').one() 13 | except sqlalchemy.orm.exc.NoResultFound: 14 | ca = CertificateAuthority.create() 15 | 16 | g.ca = ca 17 | 18 | return g.ca 19 | 20 | # 21 | # @current_app.teardown_appcontext 22 | # def teardown_ca(): 23 | # ca = g.pop('ca', None) 24 | 25 | -------------------------------------------------------------------------------- /commandment/pki/openssl.py: -------------------------------------------------------------------------------- 1 | # Regrettably, some functionality must come from PyOpenSSL 2 | from typing import Optional 3 | from cryptography.hazmat.primitives.asymmetric import rsa 4 | from cryptography import x509 5 | from cryptography.hazmat.primitives import serialization 6 | import OpenSSL 7 | 8 | 9 | def create_pkcs12( 10 | private_key: rsa.RSAPrivateKeyWithSerialization, 11 | certificate: x509.Certificate, 12 | passphrase: Optional[str] = None) -> Optional[bytes]: 13 | """Create a PKCS#12 container from the given RSA key and Certificate.""" 14 | 15 | p12 = OpenSSL.crypto.PKCS12() 16 | pkey = OpenSSL.crypto.PKey.from_cryptography_key(private_key) 17 | p12.set_privatekey(pkey) 18 | cert = OpenSSL.crypto.X509.from_cryptography(certificate) 19 | p12.set_certificate(cert) 20 | 21 | return p12.export(passphrase) 22 | 23 | -------------------------------------------------------------------------------- /commandment/pki/ormutils.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from asn1crypto.cms import ContentInfo, EnvelopedData, KeyTransRecipientInfo, RecipientIdentifier 4 | 5 | from commandment.pki.models import Certificate 6 | 7 | 8 | def find_recipient(cms_data: bytes) -> Optional[Certificate]: 9 | """Find the Certificate + Private Key of a recipient indicated by encoded CMS/PKCS#7 data from the database and 10 | return the database model that matches (if any). 11 | 12 | Requires that the indicated recipient is present in the `certificates` table, and has a matching private key in the 13 | `rsa_private_keys` table. 14 | """ 15 | content_info = ContentInfo.load(cms_data) 16 | 17 | assert content_info['content_type'].native == 'enveloped_data' 18 | content: EnvelopedData = content_info['content'] 19 | 20 | for recipient_info in content['recipient_infos']: 21 | if recipient_info.name == 'ktri': # KeyTransRecipientInfo 22 | recipient: KeyTransRecipientInfo = recipient_info.chosen 23 | recipient_id: RecipientIdentifier = recipient['rid'] 24 | assert recipient_id.name == 'issuer_and_serial_number' 25 | 26 | else: 27 | pass # Unsupported recipient type 28 | 29 | return None 30 | 31 | -------------------------------------------------------------------------------- /commandment/plistutil/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/commandment/plistutil/__init__.py -------------------------------------------------------------------------------- /commandment/profiles/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2015 Jesse Peterson, 2017 Mosen 3 | Licensed under the MIT license. See the included LICENSE.txt file for details. 4 | """ 5 | from enum import Enum 6 | 7 | PROFILE_CONTENT_TYPE = 'application/x-apple-aspen-config' 8 | 9 | 10 | class PayloadScope(Enum): 11 | User = 'User' 12 | System = 'System' 13 | 14 | -------------------------------------------------------------------------------- /commandment/profiles/ad.py: -------------------------------------------------------------------------------- 1 | from typing import Set 2 | from enum import Enum, Flag, auto 3 | 4 | 5 | class ADMountStyle(Enum): 6 | AFP = 'afp' 7 | SMB = 'smb' 8 | 9 | 10 | class ADNamespace(Enum): 11 | Domain = 'domain' 12 | Forest = 'forest' 13 | 14 | 15 | class ADOption(Flag): 16 | CreateMobileAccountAtLogin = auto() 17 | WarnUserBeforeCreatingMobileAccount = auto() 18 | ForceHomeLocal = auto() 19 | UseWindowsUNCPath = auto() 20 | AllowMultiDomainAuth = auto() 21 | 22 | ADOptions = Set[ADOption] 23 | 24 | 25 | class ADPacketSignPolicy(Enum): 26 | Allow = 'allow' 27 | Disable = 'disable' 28 | Require = 'require' 29 | 30 | 31 | class ADPacketEncryptPolicy(Enum): 32 | Allow = 'allow' 33 | Disable = 'disable' 34 | Require = 'require' 35 | SSL = 'ssl' 36 | 37 | 38 | class ADCertificateAcquisitionMechanism(Enum): 39 | RPC = 'RPC' 40 | HTTP = 'HTTP' 41 | -------------------------------------------------------------------------------- /commandment/profiles/api.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask_rest_jsonapi import Api 3 | from commandment.profiles.resources import ProfilesList, ProfileDetail, ProfileRelationship 4 | 5 | profiles_api_app = Blueprint('profiles_api', __name__) 6 | api = Api(blueprint=profiles_api_app) 7 | 8 | # Profiles (Different to profiles returned by inventory) 9 | api.route(ProfilesList, 'profiles_list', '/v1/profiles') 10 | api.route(ProfileDetail, 'profile_detail', '/v1/profiles/') 11 | api.route(ProfileRelationship, 'profile_tags', '/v1/profiles//relationships/tags') 12 | # api.route(PayloadsList, 'payloads_list', '/v1/payloads') 13 | # api.route(PayloadDetail, 'payload_detail', '/v1/payloads/') 14 | -------------------------------------------------------------------------------- /commandment/profiles/certificates.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2015 Jesse Peterson, 2017 Mosen 3 | Licensed under the MIT license. See the included LICENSE.txt file for details. 4 | """ 5 | 6 | from enum import IntFlag 7 | from marshmallow import Schema, fields, post_load, post_dump 8 | 9 | 10 | class KeyUsage(IntFlag): 11 | """Intended key usage flag. Used in SCEP payload.""" 12 | Signing = 1 13 | Encryption = 4 14 | All = Signing | Encryption 15 | -------------------------------------------------------------------------------- /commandment/profiles/eap.py: -------------------------------------------------------------------------------- 1 | from typing import Set 2 | from enum import Enum, IntEnum 3 | 4 | 5 | class EAPTypes(IntEnum): 6 | """EAP Types accepted by the EAPClient. 7 | 8 | See Also: 9 | EAP8021X, EAP.h:51 10 | """ 11 | Invalid = 0 12 | Identity = 1 13 | Notification = 2 14 | Nak = 3 15 | MD5Challenge = 4 16 | OneTimePassword = 5 17 | GenericTokenCard = 6 18 | TLS = 13 19 | CiscoLEAP = 17 20 | EAP_SIM = 18 21 | SRP_SHA1 = 19 22 | TTLS = 21 23 | EAP_AKA = 23 24 | PEAP = 25 25 | MSCHAPv2 = 26 26 | Extensions = 33 27 | EAP_FAST = 43 28 | EAP_AKA_Prime = 50 29 | 30 | AcceptEAPTypes = Set[EAPTypes] 31 | 32 | 33 | class TTLSInnerAuthentication(Enum): 34 | PAP = 'PAP' 35 | CHAP = 'CHAP' 36 | MSCHAP = 'MSCHAP' 37 | MSCHAPv2 = 'MSCHAPv2' 38 | EAP = 'EAP' 39 | -------------------------------------------------------------------------------- /commandment/profiles/email.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class EmailAccountType(Enum): 4 | POP = 'EmailTypePOP' 5 | IMAP = 'EmailTypeIMAP' 6 | 7 | 8 | class EmailAuthenticationType(Enum): 9 | Password = 'EmailAuthPassword' 10 | CRAM_MD5 = 'EmailAuthCRAMMD5' 11 | NTLM = 'EmailAuthNTLM' 12 | HTTP_MD5 = 'EmailAuthHTTPMD5' 13 | ENone = 'EmailAuthNone' 14 | -------------------------------------------------------------------------------- /commandment/profiles/energy.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntFlag, auto 2 | 3 | 4 | class ScheduledPowerEventType(Enum): 5 | wake = 'wake' 6 | wakepoweron = 'wakepoweron' 7 | sleep = 'sleep' 8 | shutdown = 'shutdown' 9 | restart = 'restart' 10 | 11 | 12 | class ScheduledPowerEventWeekdays(IntFlag): 13 | def _generate_next_value_(name, start, count, last_values): 14 | return 2 ** count 15 | 16 | Monday = auto() 17 | Tuesday = auto() 18 | Wednesday = auto() 19 | Thursday = auto() 20 | Friday = auto() 21 | Saturday = auto() 22 | Sunday = auto() 23 | 24 | All = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday 25 | -------------------------------------------------------------------------------- /commandment/profiles/resources.py: -------------------------------------------------------------------------------- 1 | from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship 2 | from commandment.models import db 3 | from commandment.profiles.models import Profile 4 | from commandment.profiles.schema import ProfileSchema 5 | 6 | 7 | class ProfilesList(ResourceList): 8 | schema = ProfileSchema 9 | data_layer = { 10 | 'session': db.session, 11 | 'model': Profile 12 | } 13 | 14 | 15 | class ProfileDetail(ResourceDetail): 16 | schema = ProfileSchema 17 | data_layer = { 18 | 'session': db.session, 19 | 'model': Profile, 20 | 'url_field': 'profile_id' 21 | } 22 | 23 | 24 | class ProfileRelationship(ResourceRelationship): 25 | schema = ProfileSchema 26 | data_layer = { 27 | 'session': db.session, 28 | 'model': Profile, 29 | 'url_field': 'profile_id' 30 | } 31 | -------------------------------------------------------------------------------- /commandment/profiles/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow_jsonapi import fields 2 | from marshmallow_jsonapi.flask import Relationship, Schema 3 | from marshmallow import Schema as FlatSchema, post_load 4 | 5 | 6 | class ProfileSchema(Schema): 7 | class Meta: 8 | type_ = 'profiles' 9 | self_view = 'profiles_api.profile_detail' 10 | self_view_kwargs = {'profile_id': ''} 11 | self_view_many = 'profiles_api.profiles_list' 12 | 13 | id = fields.Int(dump_only=True) 14 | data = fields.String() 15 | 16 | description = fields.Str() 17 | display_name = fields.Str() 18 | expiration_date = fields.DateTime() 19 | identifier = fields.Str() 20 | organization = fields.Str() 21 | uuid = fields.UUID() 22 | removal_disallowed = fields.Boolean() 23 | version = fields.Int() 24 | scope = fields.Str() 25 | removal_date = fields.DateTime() 26 | duration_until_removal = fields.Int() 27 | consent_en = fields.Str() 28 | 29 | tags = Relationship( 30 | related_view='api_app.tag_detail', 31 | related_view_kwargs={'tag_id': ''}, 32 | many=True, 33 | schema='TagSchema', 34 | type_='tags' 35 | ) 36 | 37 | -------------------------------------------------------------------------------- /commandment/profiles/vpn.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class VPNType(Enum): 4 | L2TP = 'L2TP' 5 | PPTP = 'PPTP' 6 | IPSec = 'IPSec' 7 | IKEv2 = 'IKEv2' 8 | AlwaysOn = 'AlwaysOn' 9 | VPN = 'VPN' 10 | -------------------------------------------------------------------------------- /commandment/profiles/wifi.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class WIFIEncryptionType(Enum): 5 | ENone = 'None' 6 | Any = 'Any' 7 | WPA2 = 'WPA2' 8 | WPA = 'WPA' 9 | WEP = 'WEP' 10 | 11 | 12 | class WIFIProxyType(Enum): 13 | ENone = 'None' 14 | Manual = 'Manual' 15 | Auto = 'Auto' 16 | 17 | -------------------------------------------------------------------------------- /commandment/signals.py: -------------------------------------------------------------------------------- 1 | from blinker import Namespace 2 | signals = Namespace() 3 | 4 | # Sent when a device enrolls for the first time, or re-enrols after checking out 5 | device_enrolled = signals.signal('device-enrolled') 6 | 7 | # Sent when a device voluntarily checks out 8 | device_unenrolled = signals.signal('device-unenrolled') 9 | 10 | # Sent when a device checks in, including: Authenticate, TokenUpdate, Acknowledge, NotNow 11 | device_checkin = signals.signal('device-checkin') 12 | 13 | # If APNS tells us that a device token expired 14 | device_token_expired = signals.signal('device-token-expired') 15 | -------------------------------------------------------------------------------- /commandment/static/.gitignore: -------------------------------------------------------------------------------- 1 | app.js 2 | *.map 3 | fonts/* 4 | css/* 5 | images/* 6 | 7 | -------------------------------------------------------------------------------- /commandment/static/index.dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | commandment 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /commandment/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | commandment 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /commandment/storage/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /commandment/templates/index.html: -------------------------------------------------------------------------------- 1 | {# The URL that assets will be loaded from if running from webpack-dev-server #} 2 | {% set webpack_dev_url = 'https://localhost:4000' %} 3 | 4 | 5 | 6 | 7 | 8 | {% block head %} 9 | 10 | {% if config['DEBUG'] == False %} 11 | 12 | {% endif %} 13 | commandment 14 | {% endblock %} 15 | 16 | 17 | 18 | loading {{ config['DEBUG'] }} 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /commandment/threads/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/commandment/threads/__init__.py -------------------------------------------------------------------------------- /commandment/threads/vpp_thread.py: -------------------------------------------------------------------------------- 1 | """ 2 | This thread should synchronise available licenses 3 | """ -------------------------------------------------------------------------------- /commandment/utils.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | import plistlib 3 | 4 | 5 | def plistify(*args, **kwargs): 6 | """Similar to jsonify, which ships with Flask, this function wraps plistlib.dumps and sets up the correct 7 | mime type for the response.""" 8 | if args and kwargs: 9 | raise TypeError('plistify() behavior undefined when passed both args and kwargs') 10 | elif len(args) == 1: # single args are passed directly to dumps() 11 | data = args[0] 12 | else: 13 | data = args or kwargs 14 | 15 | mimetype = kwargs.get('mimetype', current_app.config['PLISTIFY_MIMETYPE']) 16 | 17 | return current_app.response_class( 18 | (plistlib.dumps(data), '\n'), 19 | mimetype=mimetype 20 | ) 21 | -------------------------------------------------------------------------------- /commandment/vpp/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import g, current_app 2 | 3 | from commandment.vpp.errors import VPPError 4 | from commandment.vpp.vpp import VPP 5 | 6 | 7 | def get_vpp() -> VPP: 8 | vpp = getattr(g, '_vpp', None) 9 | 10 | if vpp is None: 11 | if 'VPP_STOKEN' not in current_app.config: 12 | raise VPPError('VPP stoken not configured') 13 | 14 | g._vpp = VPP(current_app.config['VPP_STOKEN']) 15 | 16 | return vpp 17 | -------------------------------------------------------------------------------- /commandment/vpp/cli.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/commandment/vpp/cli.py -------------------------------------------------------------------------------- /commandment/vpp/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from commandment.vpp.errors import VPPAPIError 4 | 5 | 6 | def raise_error_replies(f): 7 | """Decorator which wraps a function that returns the dict representing a direct response body from the VPP service. 8 | 9 | The reply is checked for VPP errors and, if there are any errors, the error is raised as a VPPAPIError exception. 10 | """ 11 | @functools.wraps(f) 12 | def wrapper(*args, **kwargs): 13 | reply = f(*args, **kwargs) 14 | if reply['status'] == -1: # VPP Error occurred 15 | raise VPPAPIError(reply['errorNumber'], reply['errorMessage']) 16 | return reply 17 | 18 | return wrapper 19 | -------------------------------------------------------------------------------- /commandment/vpp/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class VPPAccountSchema(Schema): 5 | exp_date = fields.DateTime() 6 | org_name = fields.String() 7 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /doc/_static/config/uwsgi-commandment.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | base = /usr/local/commandment 3 | 4 | pythonpath = %(base) 5 | module = commandment:create_app() 6 | 7 | home = /usr/local/commandment/virtualenv 8 | # This might be different if you used pipenv to install the dependencies eg. 9 | # home = /Users//.local/share/virtualenvs/commandment- 10 | plugins = python3 11 | 12 | env = COMMANDMENT_SETTINGS=/usr/local/commandment/settings.cfg 13 | 14 | # This is necessary to make multi-threading / multi-processing not fail on High Sierra with 15 | # `+[__NSPlaceholderDate initialize] may have been in progress in another thread when fork() was called.` 16 | env = OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES 17 | master = true 18 | processes = 4 19 | enable-threads = true 20 | 21 | socket = /usr/local/var/run/uwsgi-commandment.sock 22 | chmod-socket = 660 23 | 24 | die-on-term = true 25 | 26 | logto = /usr/local/commandment/log/uwsgi-commandment.log 27 | -------------------------------------------------------------------------------- /doc/_static/images/asm/upload-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/doc/_static/images/asm/upload-key.png -------------------------------------------------------------------------------- /doc/_static/uml/checkin.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | actor Device 3 | boundary MDM 4 | entity DeviceModel 5 | 6 | Device -> MDM: Authenticate message 7 | MDM -> EnrollPolicy: Check whitelist 8 | EnrollPolicy -> MDM: Device passed 9 | MDM -> DeviceModel: Update Attributes 10 | MDM -> DeviceModel: Clear Token 11 | MDM -> Device: 200 "OK" 12 | 13 | Device -> MDM: TokenUpdate message 14 | 15 | 16 | @enduml -------------------------------------------------------------------------------- /doc/_static/uml/commandqueue.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | start 3 | :device has commands; 4 | :at least one command is "Queued" status; 5 | :command does not have "after" date; 6 | 7 | end 8 | @enduml -------------------------------------------------------------------------------- /doc/_static/uml/models/Certificate.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam defaultFontName Courier 4 | 5 | Class certificates { 6 | INTEGER ★ id 7 | INTEGER ☆ rsa_private_key_id 8 | VARCHAR[20] ⚪ discriminator 9 | VARCHAR[64] ⚪ fingerprint 10 | DATETIME ⚪ not_after 11 | DATETIME ⚪ not_before 12 | TEXT ⚪ pem_data 13 | VARCHAR ⚪ push_topic 14 | VARCHAR[2] ⚪ x509_c 15 | VARCHAR[64] ⚪ x509_cn 16 | VARCHAR[64] ⚪ x509_o 17 | VARCHAR[32] ⚪ x509_ou 18 | VARCHAR[128] ⚪ x509_st 19 | INDEX[fingerprint] » ix_certificates_fingerprint 20 | } 21 | 22 | right footer generated by sadisplay v0.4.8 23 | 24 | @enduml -------------------------------------------------------------------------------- /doc/_static/uml/models/Command.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam defaultFontName Courier 4 | 5 | Class commands { 6 | INTEGER ★ id 7 | INTEGER ☆ device_id 8 | DATETIME ⚪ acknowledged_at 9 | DATETIME ⚪ after 10 | TEXT ⚪ parameters 11 | DATETIME ⚪ queued_at 12 | VARCHAR ⚪ request_type 13 | DATETIME ⚪ sent_at 14 | VARCHAR[1] ⚪ status 15 | INTEGER ⚪ ttl 16 | CHAR[32] ⚪ uuid 17 | INDEX[status] » ix_commands_status 18 | INDEX[uuid] » ix_commands_uuid 19 | } 20 | 21 | right footer generated by sadisplay v0.4.8 22 | 23 | @enduml -------------------------------------------------------------------------------- /doc/_static/uml/models/InstalledApplication.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam defaultFontName Courier 4 | 5 | Class installed_applications { 6 | INTEGER ★ id 7 | INTEGER ☆ device_id 8 | VARCHAR ⚪ bundle_identifier 9 | BIGINT ⚪ bundle_size 10 | CHAR[32] ⚪ device_udid 11 | BIGINT ⚪ dynamic_size 12 | BOOLEAN ⚪ is_validated 13 | VARCHAR ⚪ name 14 | VARCHAR ⚪ short_version 15 | VARCHAR ⚪ version 16 | INDEX[bundle_identifier] » ix_installed_applications_bundle_identifier 17 | INDEX[device_udid] » ix_installed_applications_device_udid 18 | INDEX[version] » ix_installed_applications_version 19 | } 20 | 21 | right footer generated by sadisplay v0.4.8 22 | 23 | @enduml -------------------------------------------------------------------------------- /doc/_static/uml/models/InstalledCertificate.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam defaultFontName Courier 4 | 5 | Class installed_certificates { 6 | INTEGER ★ id 7 | INTEGER ☆ device_id 8 | BLOB ⚪ der_data 9 | CHAR[32] ⚪ device_udid 10 | VARCHAR[64] ⚪ fingerprint_sha256 11 | BOOLEAN ⚪ is_identity 12 | VARCHAR ⚪ x509_cn 13 | INDEX[device_udid] » ix_installed_certificates_device_udid 14 | INDEX[fingerprint_sha256] » ix_installed_certificates_fingerprint_sha256 15 | } 16 | 17 | right footer generated by sadisplay v0.4.8 18 | 19 | @enduml -------------------------------------------------------------------------------- /doc/_static/uml/models/InstalledProfile.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam defaultFontName Courier 4 | 5 | Class installed_profiles { 6 | INTEGER ★ id 7 | INTEGER ☆ device_id 8 | CHAR[32] ⚪ device_udid 9 | BOOLEAN ⚪ has_removal_password 10 | BOOLEAN ⚪ is_encrypted 11 | VARCHAR ⚪ payload_description 12 | VARCHAR ⚪ payload_display_name 13 | VARCHAR ⚪ payload_identifier 14 | VARCHAR ⚪ payload_organization 15 | BOOLEAN ⚪ payload_removal_disallowed 16 | CHAR[32] ⚪ payload_uuid 17 | INDEX[device_udid] » ix_installed_profiles_device_udid 18 | INDEX[payload_uuid] » ix_installed_profiles_payload_uuid 19 | } 20 | 21 | right footer generated by sadisplay v0.4.8 22 | 23 | @enduml -------------------------------------------------------------------------------- /doc/api/commands.rst: -------------------------------------------------------------------------------- 1 | Commands 2 | ======== 3 | 4 | Summary 5 | ------- 6 | 7 | 8 | 9 | Detail 10 | ------ 11 | 12 | .. autoflask:: commandment:create_app() 13 | :blueprints: api_app 14 | 15 | .. http:get:: /api/v1/commands 16 | 17 | Get all commands 18 | 19 | :reqheader Accept: application/vnd.api+json 20 | :resheader Content-Type: application/vnd.api+json 21 | 22 | .. http:post:: /api/v1/commands 23 | 24 | Create a command 25 | 26 | .. http:patch:: /api/v1/commands/(int:command_id) 27 | 28 | Update a command 29 | 30 | .. http:delete:: /api/v1/commands/(int:command_id) 31 | 32 | Delete a command 33 | 34 | .. http:get:: /api/v1/devices/(int:device_id)/commands 35 | 36 | Get MDM commands associated with the device specified by **device_id** 37 | 38 | .. http:all:: /api/v1/devices/(int:device_id)/relationships/commands 39 | 40 | Attach/Detach command relationships to specific devices 41 | 42 | -------------------------------------------------------------------------------- /doc/api/dep.rst: -------------------------------------------------------------------------------- 1 | Device Enrollment Programmes 2 | ============================ 3 | 4 | Summary 5 | ------- 6 | 7 | .. autoflask:: commandment:create_app() 8 | :blueprints: dep_app 9 | 10 | 11 | -------------------------------------------------------------------------------- /doc/api/devices.rst: -------------------------------------------------------------------------------- 1 | Devices 2 | ======= 3 | 4 | .. autoflask:: commandment:create_app() 5 | :blueprints: api_app 6 | 7 | .. http:get:: /api/v1/devices 8 | 9 | Get a list of devices 10 | 11 | :reqheader Accept: application/vnd.api+json 12 | :resheader Content-Type: application/vnd.api+json 13 | 14 | .. http:get:: /api/v1/devices/(int:device_id) 15 | 16 | Get information about a specific device. 17 | 18 | .. http:post:: /api/v1/devices 19 | 20 | Create a new enrolled device 21 | 22 | .. http:patch:: /api/v1/devices/(int:device_id) 23 | 24 | Update an enrolled device 25 | 26 | .. http:delete:: /api/v1/devices/(int:device_id) 27 | 28 | Delete an enrolled device 29 | 30 | .. http:get:: /api/v1/devices/(int:device_id)/commands 31 | 32 | Get MDM commands associated with this device. 33 | 34 | .. http:get:: /api/v1/devices/(int:device_id)/tags 35 | 36 | Get tags associated with this device. 37 | 38 | -------------------------------------------------------------------------------- /doc/api/index.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | Almost all responses and requests are expected to follow the `JSON-API `_ standard, 5 | except in cases where binary or encoded data needs to be uploaded or downloaded, 6 | *OR* the endpoint is a one-off RPC style action eg. "Erase Device". 7 | 8 | All of the API is generated via the `flask-rest-jsonapi `_ library. 9 | 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | certificates 15 | commands 16 | dep 17 | devices 18 | organization 19 | -------------------------------------------------------------------------------- /doc/api/organization.rst: -------------------------------------------------------------------------------- 1 | Organization 2 | ============ 3 | 4 | :SQLAlchemy: :ref:`model-organization` 5 | 6 | .. autoflask:: commandment:create_app() 7 | :blueprints: configuration_app 8 | -------------------------------------------------------------------------------- /doc/dev/MUSINGS.rst: -------------------------------------------------------------------------------- 1 | # Musings # 2 | 3 | Dynamic device groups by attribute. 4 | Problem: too slow to resolve group membership 5 | 6 | Possible solutions: update group membership on change? 7 | 8 | --- 9 | 10 | Problem: storage of dynamic group predicates 11 | 12 | 13 | 14 | 15 | --- 16 | 17 | Group predicate attributes 18 | 19 | model 20 | os_version 21 | enrolled / not enrolled 22 | check in date/delta 23 | device capacity <> 24 | 25 | how about IN or NOT IN 26 | 27 | has installed application(s) => 28 | has installed profile(s) => identifier in 29 | 30 | 31 | 32 | Profile Install via Tag 33 | ======================= 34 | 35 | - Device and profile share tag: Profile should be installed. 36 | - Queue profile when tag changes or when device checks in? 37 | - If tag is subsequently removed, we have to manage the queue too. 38 | - VS: generate install while device checks in 39 | - What if multiple tags are assigned to the same profile and the device is too? 40 | - Checking the queue gets more complex. 41 | -------------------------------------------------------------------------------- /doc/developer/guide/building.rst: -------------------------------------------------------------------------------- 1 | Building 2 | ======== 3 | 4 | Backend 5 | ------- 6 | 7 | None of the Python backend is compiled. So there is no build step. 8 | 9 | Since we are using type annotations, you may perform "linting" of sorts with `mypy `_. 10 | 11 | Frontend 12 | -------- 13 | 14 | From the **ui** directory inside the repository, there are several **npm run** scripts/commands that you can use in 15 | each stage of development. 16 | 17 | If you are working on live changes and want to see the results immediately, you can run:: 18 | 19 | $ npm start 20 | 21 | This starts a `webpack-dev-server `_, listening on ``localhost:4000`` by default. 22 | 23 | When flask is run with the setting ``DEBUG=True``, the javascript code is loaded from this webpack dev server on localhost. 24 | 25 | Documentation 26 | ------------- 27 | 28 | The documentation is built using `Sphinx `_. 29 | 30 | Sphinx, it's extensions, and the documentation theme are included in the **pipenv** developer dependencies. 31 | 32 | If you have installed the dependencies using **pipenv** you may run:: 33 | 34 | $ pipenv run make html 35 | 36 | from the :file:`doc/` directory in order to build the documentation. 37 | -------------------------------------------------------------------------------- /doc/developer/guide/index.rst: -------------------------------------------------------------------------------- 1 | Developer Guide 2 | =============== 3 | 4 | 5 | This guide explains how to get commandment set up for development. 6 | 7 | 8 | .. warning:: The guide only covers macOS at this point in time. 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | dependencies 14 | building 15 | architecture 16 | running 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /doc/developer/index.rst: -------------------------------------------------------------------------------- 1 | ####################### 2 | Developer Documentation 3 | ####################### 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | install 9 | guide/index 10 | 11 | 12 | -------------------------------------------------------------------------------- /doc/guides/scep.rst: -------------------------------------------------------------------------------- 1 | Verifying CMS Replies:: 2 | 3 | /usr/local/Cellar/openssl/1.0.2k/bin/openssl cms -verify -in /tmp/reply.bin -inform DER -noverify 4 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. commandment documentation master file, created by 2 | sphinx-quickstart on Sat Mar 25 14:50:50 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to commandment's documentation! 7 | ======================================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | about-mdm 15 | installing/macos 16 | user/configuration 17 | user/dep 18 | developer/index 19 | api/index 20 | internal/index 21 | 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | 31 | -------------------------------------------------------------------------------- /doc/installing/index.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Installing 3 | ========== 4 | 5 | -------------------------------------------------------------------------------- /doc/internal/api/api.rst: -------------------------------------------------------------------------------- 1 | Non-Standardised API 2 | ==================== 3 | 4 | .. qrefflask:: commandment:create_app() 5 | :blueprints: flat_api 6 | :endpoints: 7 | 8 | .. autoflask:: commandment:create_app() 9 | :blueprints: flat_api 10 | :endpoints: 11 | -------------------------------------------------------------------------------- /doc/internal/api/index.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | The API is currently split into two categories. Most of the API adheres to the 5 | `JSON-API Specification `_. Some things such as RPC style calls or singleton objects 6 | wouldn't make sense in this context, so they're placed into a flat_api blueprint. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | api.rst 12 | json-api.rst 13 | 14 | -------------------------------------------------------------------------------- /doc/internal/api/json-api.rst: -------------------------------------------------------------------------------- 1 | JSON-API v1 2 | =========== 3 | 4 | .. qrefflask:: commandment:create_app() 5 | :blueprints: api_app 6 | :endpoints: 7 | 8 | .. autoflask:: commandment:create_app() 9 | :blueprints: api_app 10 | :endpoints: 11 | -------------------------------------------------------------------------------- /doc/internal/cms/decorators.rst: -------------------------------------------------------------------------------- 1 | Decorators 2 | ========== 3 | 4 | .. automodule:: commandment.cms.decorators 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /doc/internal/cms/index.rst: -------------------------------------------------------------------------------- 1 | CMS - Cryptographic Message Syntax / PKCS#7 2 | =========================================== 3 | 4 | Details of the **commandment.cms** package. 5 | 6 | This package implements most of the CMS / PKCS#7 functionality with the aid of *asn1crypto* and *cryptography*. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | decorators 12 | 13 | -------------------------------------------------------------------------------- /doc/internal/core/index.rst: -------------------------------------------------------------------------------- 1 | Commandment Core 2 | ================ 3 | 4 | Details of the **commandment** core package 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | models/index 10 | signals 11 | 12 | -------------------------------------------------------------------------------- /doc/internal/core/models/certificate.rst: -------------------------------------------------------------------------------- 1 | .. _model-certificate: 2 | 3 | Certificate 4 | =========== 5 | 6 | .. uml:: /_static/uml/models/Certificate.plantuml 7 | 8 | .. py:currentmodule:: commandment.models 9 | .. autoclass:: Certificate 10 | :members: 11 | 12 | 13 | -------------------------------------------------------------------------------- /doc/internal/core/models/certificate_request.rst: -------------------------------------------------------------------------------- 1 | CertificateRequest 2 | ================== 3 | 4 | .. py:currentmodule:: commandment.models 5 | .. autoclass:: CertificateSigningRequest 6 | :members: 7 | :show-inheritance: 8 | 9 | 10 | -------------------------------------------------------------------------------- /doc/internal/core/models/command.rst: -------------------------------------------------------------------------------- 1 | .. _model-command: 2 | 3 | Command 4 | ======= 5 | 6 | .. uml:: /_static/uml/models/Command.plantuml 7 | 8 | .. py:currentmodule:: commandment.models 9 | .. autoclass:: Command 10 | :members: 11 | 12 | .. autoclass:: CommandStatus 13 | :members: 14 | 15 | -------------------------------------------------------------------------------- /doc/internal/core/models/device.rst: -------------------------------------------------------------------------------- 1 | Device 2 | ====== 3 | 4 | .. uml:: /_static/uml/models/Device.plantuml 5 | 6 | .. py:currentmodule:: commandment.models 7 | .. autoclass:: Device 8 | :members: 9 | 10 | -------------------------------------------------------------------------------- /doc/internal/core/models/index.rst: -------------------------------------------------------------------------------- 1 | ORM (SQLAlchemy) Models 2 | ======================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | certificate.rst 8 | certificate_request.rst 9 | command.rst 10 | device.rst 11 | organization.rst 12 | installed_profile 13 | installed_certificate 14 | installed_application 15 | profile.rst 16 | rsa_private_key.rst 17 | -------------------------------------------------------------------------------- /doc/internal/core/models/installed_application.rst: -------------------------------------------------------------------------------- 1 | InstalledApplication 2 | ==================== 3 | 4 | .. uml:: /_static/uml/models/InstalledApplication.plantuml 5 | 6 | .. py:currentmodule:: commandment.models 7 | .. autoclass:: InstalledApplication 8 | :members: -------------------------------------------------------------------------------- /doc/internal/core/models/installed_certificate.rst: -------------------------------------------------------------------------------- 1 | InstalledCertificate 2 | ==================== 3 | 4 | .. uml:: /_static/uml/models/InstalledCertificate.plantuml 5 | 6 | .. py:currentmodule:: commandment.models 7 | .. autoclass:: InstalledCertificate 8 | :members: -------------------------------------------------------------------------------- /doc/internal/core/models/installed_profile.rst: -------------------------------------------------------------------------------- 1 | InstalledProfile 2 | ================ 3 | 4 | .. uml:: /_static/uml/models/InstalledProfile.plantuml 5 | 6 | .. py:currentmodule:: commandment.models 7 | .. autoclass:: InstalledProfile 8 | :members: 9 | -------------------------------------------------------------------------------- /doc/internal/core/models/organization.rst: -------------------------------------------------------------------------------- 1 | .. _model-organization: 2 | 3 | Organization 4 | ============ 5 | 6 | .. py:currentmodule:: commandment.models 7 | .. autoclass:: Organization 8 | :members: 9 | -------------------------------------------------------------------------------- /doc/internal/core/models/profile.rst: -------------------------------------------------------------------------------- 1 | Profile 2 | ======= 3 | 4 | .. py:currentmodule:: commandment.models 5 | .. autoclass:: Profile 6 | :members: 7 | 8 | -------------------------------------------------------------------------------- /doc/internal/core/models/rsa_private_key.rst: -------------------------------------------------------------------------------- 1 | PrivateKey 2 | ========== 3 | 4 | .. py:currentmodule:: commandment.models 5 | .. autoclass:: RSAPrivateKey 6 | :members: 7 | 8 | 9 | -------------------------------------------------------------------------------- /doc/internal/core/signals.rst: -------------------------------------------------------------------------------- 1 | Signals 2 | ======= 3 | 4 | .. automodule:: commandment.signals 5 | :members: 6 | -------------------------------------------------------------------------------- /doc/internal/decorators.rst: -------------------------------------------------------------------------------- 1 | Decorators 2 | ========== 3 | 4 | 5 | commandment.mdm.parse_plist_input_data 6 | 7 | device_cert_check 8 | -------------------------------------------------------------------------------- /doc/internal/dep/dep.rst: -------------------------------------------------------------------------------- 1 | DEP Client 2 | ========== 3 | 4 | The main DEP API wrapper class 5 | 6 | .. autoclass:: commandment.dep.dep.DEP 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /doc/internal/dep/index.rst: -------------------------------------------------------------------------------- 1 | DEP - Device Enrollment Programme 2 | ================================= 3 | 4 | Details of the **commandment.dep** package 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | dep 10 | types 11 | models 12 | -------------------------------------------------------------------------------- /doc/internal/dep/models.rst: -------------------------------------------------------------------------------- 1 | SQLAlchemy Models 2 | ================= 3 | 4 | .. autoclass:: commandment.dep.models.DEPAnchorCertificate 5 | :members: 6 | 7 | .. autoclass:: commandment.dep.models.DEPSupervisionCertificate 8 | :members: 9 | 10 | .. autoclass:: commandment.dep.models.DEPServerTokenCertificate 11 | :members: 12 | 13 | .. autoclass:: commandment.dep.models.DEPConfiguration 14 | :members: 15 | 16 | .. autoclass:: commandment.dep.models.DEPProfile 17 | :members: 18 | -------------------------------------------------------------------------------- /doc/internal/dep/types.rst: -------------------------------------------------------------------------------- 1 | DEP Types 2 | ========= 3 | 4 | .. automodule:: commandment.dep 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /doc/internal/enroll/app.rst: -------------------------------------------------------------------------------- 1 | Enrollment Blueprint 2 | ==================== 3 | 4 | .. qrefflask:: commandment:create_app() 5 | :blueprints: enroll_app 6 | :endpoints: 7 | 8 | .. autoflask:: commandment:create_app() 9 | :blueprints: enroll_app 10 | :endpoints: 11 | -------------------------------------------------------------------------------- /doc/internal/enroll/index.rst: -------------------------------------------------------------------------------- 1 | Enrollment 2 | ========== 3 | 4 | Details of the **commandment.enroll** package. 5 | 6 | This package implements all of the non-dep enrollment logic. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | app 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/internal/flask/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration Blueprint 2 | ======================= 3 | 4 | .. autoflask:: commandment:create_app() 5 | :blueprints: configuration_app 6 | :endpoints: 7 | 8 | 9 | -------------------------------------------------------------------------------- /doc/internal/flask/index.rst: -------------------------------------------------------------------------------- 1 | Flask Endpoints 2 | =============== 3 | 4 | This should contain only endpoints that are not REST endpoints. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | configuration 10 | enroll 11 | mdm_app 12 | 13 | -------------------------------------------------------------------------------- /doc/internal/index.rst: -------------------------------------------------------------------------------- 1 | ################### 2 | Internals Reference 3 | ################### 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | core/index 9 | 10 | api/index 11 | cms/index 12 | dep/index 13 | enroll/index 14 | mdm/index 15 | vpp/index 16 | 17 | workers/index 18 | 19 | push.rst 20 | 21 | 22 | -------------------------------------------------------------------------------- /doc/internal/mdm/app.rst: -------------------------------------------------------------------------------- 1 | MDM Blueprint 2 | ============= 3 | 4 | .. autoflask:: commandment:create_app() 5 | :blueprints: mdm_app 6 | :endpoints: 7 | 8 | 9 | -------------------------------------------------------------------------------- /doc/internal/mdm/handlers.rst: -------------------------------------------------------------------------------- 1 | MDM Command Response Handlers 2 | ============================= 3 | 4 | .. automodule:: commandment.mdm.handlers 5 | :members: 6 | -------------------------------------------------------------------------------- /doc/internal/mdm/index.rst: -------------------------------------------------------------------------------- 1 | MDM 2 | === 3 | 4 | Details of the **commandment.mdm** package. 5 | 6 | This package implements the Apple MDM Protocol. 7 | Responses and requests from devices may be generated or handled by other modules also. 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | app 13 | handlers 14 | types 15 | 16 | 17 | -------------------------------------------------------------------------------- /doc/internal/mdm/types.rst: -------------------------------------------------------------------------------- 1 | MDM Types 2 | ========= 3 | 4 | .. automodule:: commandment.mdm 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /doc/internal/push.rst: -------------------------------------------------------------------------------- 1 | push 2 | ==== 3 | 4 | .. automodule:: commandment.push 5 | -------------------------------------------------------------------------------- /doc/internal/vpp/decorators.rst: -------------------------------------------------------------------------------- 1 | Decorators 2 | ========== 3 | 4 | .. automodule:: commandment.vpp.decorators 5 | :members: 6 | -------------------------------------------------------------------------------- /doc/internal/vpp/enum.rst: -------------------------------------------------------------------------------- 1 | VPP Types 2 | ========= 3 | 4 | .. automodule:: commandment.vpp.enum 5 | :members: 6 | -------------------------------------------------------------------------------- /doc/internal/vpp/errors.rst: -------------------------------------------------------------------------------- 1 | VPP Errors 2 | ========== 3 | 4 | .. automodule:: commandment.vpp.errors 5 | :members: 6 | -------------------------------------------------------------------------------- /doc/internal/vpp/index.rst: -------------------------------------------------------------------------------- 1 | VPP - Volume Purchasing Programme 2 | ================================= 3 | 4 | Details of the **commandment.vpp** package. 5 | 6 | This package implements all of the functionality related to Apple's **Volume Purchase Programme**. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | decorators 12 | enum 13 | errors 14 | operations 15 | vpp 16 | -------------------------------------------------------------------------------- /doc/internal/vpp/operations.rst: -------------------------------------------------------------------------------- 1 | VPP License Operations 2 | ====================== 3 | 4 | .. autoclass:: commandment.vpp.vpp.VPPLicenseOperation 5 | :members: 6 | 7 | .. autoclass:: commandment.vpp.vpp.VPPUserLicenseOperation 8 | :members: 9 | 10 | .. autoclass:: commandment.vpp.vpp.VPPDeviceLicenseOperation 11 | :members: 12 | 13 | -------------------------------------------------------------------------------- /doc/internal/vpp/vpp.rst: -------------------------------------------------------------------------------- 1 | VPP Client 2 | ========== 3 | 4 | The main VPP API wrapper class 5 | 6 | .. autoclass:: commandment.vpp.vpp.VPP 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /doc/internal/workers/index.rst: -------------------------------------------------------------------------------- 1 | Worker Threads 2 | ============== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | runner -------------------------------------------------------------------------------- /doc/internal/workers/runner.rst: -------------------------------------------------------------------------------- 1 | runner 2 | ====== 3 | 4 | .. automodule:: commandment.runner 5 | -------------------------------------------------------------------------------- /doc/sadisplay/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | import codecs 3 | import sadisplay 4 | from flask import Flask 5 | 6 | from commandment.models import db, Device, Command, InstalledApplication, InstalledCertificate, \ 7 | InstalledProfile 8 | from commandment.pki.models import Certificate 9 | 10 | dummyapp = Flask(__name__) 11 | db.init_app(dummyapp) 12 | 13 | UML_PATH = os.path.realpath(os.path.dirname(__file__) + '/../_static/uml/models') 14 | 15 | classes = [Certificate, Command, InstalledApplication, InstalledApplication, InstalledCertificate, InstalledProfile] 16 | 17 | with dummyapp.app_context(): 18 | for cls in classes: 19 | desc = sadisplay.describe( 20 | [getattr(cls, attr) for attr in dir(cls)], 21 | show_methods=True, 22 | show_properties=True, 23 | show_indexes=True, 24 | ) 25 | 26 | with codecs.open(os.path.join(UML_PATH, '{}.plantuml'.format(cls.__name__)), 'w', encoding='utf-8') as f: 27 | f.write(sadisplay.plantuml(desc)) 28 | -------------------------------------------------------------------------------- /doc/user/index.rst: -------------------------------------------------------------------------------- 1 | ################## 2 | User Documentation 3 | ################## 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | preface 9 | install 10 | configuration 11 | dep 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | commandment: 4 | build: 5 | context: . 6 | dockerfile: .docker/Dockerfile 7 | image: cmdmnt/commandment:latest 8 | # volumes: 9 | # - "./.docker/settings.cfg.docker:/settings.cfg" 10 | # - "./server.crt:/etc/nginx/ssl.crt" 11 | # - "./server.key:/etc/nginx/ssl.key" 12 | ports: 13 | - "8445:443" 14 | environment: 15 | - SSL_HOSTNAME=commandment.dev 16 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports=True 3 | 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | markers = 4 | depsim: mark a test requiring depsim 5 | vppsim: mark a test requiring vppsim 6 | dep: mark a test requiring a live DEP account 7 | vpp: mark a test requiring a live VPP account 8 | 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [tool:pytest] 5 | python_files=tests/*.py -------------------------------------------------------------------------------- /simulators/depsim/.gitignore: -------------------------------------------------------------------------------- 1 | depsim 2 | -------------------------------------------------------------------------------- /simulators/depsim/Dockerfile: -------------------------------------------------------------------------------- 1 | # depsim container 2 | FROM ubuntu:14.04 3 | ADD depsim /usr/bin 4 | RUN mkdir /etc/depsim 5 | 6 | EXPOSE 8080 7 | VOLUME ["/etc/depsim"] 8 | ENTRYPOINT ["depsim"] 9 | CMD ["start"] 10 | -------------------------------------------------------------------------------- /simulators/depsim/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_insertions": { 3 | "devices_per_event": 500 4 | } 5 | } -------------------------------------------------------------------------------- /simulators/depsim/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | depsim: 4 | image: depsim 5 | build: 6 | context: . 7 | ports: 8 | - 8080:8080 9 | volumes: 10 | - "./config.json:/etc/depsim/config.json:ro" 11 | command: ["start", "/etc/depsim/config.json"] -------------------------------------------------------------------------------- /simulators/vppsim/.gitignore: -------------------------------------------------------------------------------- 1 | vppsim -------------------------------------------------------------------------------- /simulators/vppsim/Dockerfile: -------------------------------------------------------------------------------- 1 | # vppsim container 2 | FROM ubuntu:14.04 3 | ADD vppsim /usr/bin 4 | RUN mkdir /etc/vppsim 5 | 6 | EXPOSE 8080 7 | VOLUME ["/etc/vppsim"] 8 | ENTRYPOINT ["vppsim"] 9 | CMD ["start"] 10 | -------------------------------------------------------------------------------- /simulators/vppsim/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "license_insertions": [{ 3 | "license": { 4 | "adamId": 525463029 5 | }, 6 | "licenses_per_event": 2000, 7 | "interval_in_seconds": 1, 8 | "max_event_count": 1 9 | }], 10 | "associate_users": { 11 | "users_per_event": 2000, 12 | "interval_in_seconds": 1, 13 | "max_event_count": 1 14 | } 15 | } -------------------------------------------------------------------------------- /simulators/vppsim/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | vppsim: 4 | image: vppsim 5 | build: 6 | context: . 7 | ports: 8 | - 8080:8080 9 | volumes: 10 | - "./config.json:/etc/vppsim/config.json:ro" 11 | command: ["start", "/etc/vppsim/config.json"] -------------------------------------------------------------------------------- /testdata/Authenticate/10.11.x.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildVersion 6 | 15G1004 7 | Challenge 8 | 9 | YXBwbGU= 10 | 11 | DeviceName 12 | micromdm-test 13 | MessageType 14 | Authenticate 15 | Model 16 | iMac15,1 17 | ModelName 18 | iMac 19 | OSVersion 20 | 10.11.6 21 | ProductName 22 | iMac15,1 23 | SerialNumber 24 | C00000000004 25 | Topic 26 | com.apple.mgmt.test.00000000-1111-2222-3333-444455556666 27 | UDID 28 | 00000000-1111-2222-3333-444455556666 29 | 30 | -------------------------------------------------------------------------------- /testdata/Authenticate/10.12.2.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | BuildVersion 6 | 16C67 7 | Challenge 8 | YXBwbGU= 9 | DeviceName 10 | commandment 11 | MessageType 12 | Authenticate 13 | Model 14 | iMac17,1 15 | ModelName 16 | iMac 17 | OSVersion 18 | 10.12.2 19 | ProductName 20 | iMac17,1 21 | SerialNumber 22 | 000000000000 23 | Topic 24 | com.apple.mgmt.commandment.dev 25 | UDID 26 | E3568F17-92ED-450A-8904-C3BF4CB7E9A5 27 | 28 | -------------------------------------------------------------------------------- /testdata/Authenticate/IOS-11.3.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildVersion 6 | 15E302 7 | MessageType 8 | Authenticate 9 | OSVersion 10 | 11.3.1 11 | ProductName 12 | iPad4,1 13 | SerialNumber 14 | XXXXXXXXXXXX 15 | Topic 16 | com.apple.mgmt.XServer.1c111c11-1c11-1c11-1c11-1c111c111c11 17 | UDID 18 | 1c111c111c111c111c111c111c111c111c111c11 19 | 20 | -------------------------------------------------------------------------------- /testdata/Authenticate/IOS-9.x.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildVersion 6 | 13F69 7 | MessageType 8 | Authenticate 9 | OSVersion 10 | 9.3.2 11 | ProductName 12 | iPad4,1 13 | SerialNumber 14 | XXXXXXXXXXXX 15 | Topic 16 | io.micromdm.topic.00000000-1111-2222-3333-444455556666 17 | UDID 18 | 1111111111111111111111111111111111111111 19 | 20 | 21 | -------------------------------------------------------------------------------- /testdata/Authenticate/iOS-11.3.1-cell.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | BuildVersion 6 | 15E302 7 | IMEI 8 | 11 111111 111111 1 9 | MEID 10 | 1111111111111 11 | MessageType 12 | Authenticate 13 | OSVersion 14 | 11.3.1 15 | ProductName 16 | iPhone7,2 17 | SerialNumber 18 | XXXXXXXXXXXX 19 | Topic 20 | com.apple.mgmt.XServer.1c111c11-1c11-1c11-1c11-1c111c111c11 21 | UDID 22 | 1111111111111111111111111111111111111111 23 | 24 | -------------------------------------------------------------------------------- /testdata/AvailableOSUpdates/iOS-11.3.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AvailableOSUpdates 6 | 7 | 8 | AllowsInstallLater 9 | 10 | Build 11 | 15F79 12 | DownloadSize 13 | 225236247 14 | HumanReadableName 15 | iOS 11.4 16 | InstallSize 17 | 537395200 18 | IsCritical 19 | 20 | ProductKey 21 | iOSUpdate15F79 22 | ProductName 23 | iOS 24 | RestartRequired 25 | 26 | Version 27 | 11.4 28 | 29 | 30 | CommandUUID 31 | 8fb53ef6-5fcd-46c2-bf05-ee502406f240 32 | Status 33 | Acknowledged 34 | UDID 35 | 1c111c111c111c111c111c111c111c111c111c11 36 | 37 | 38 | -------------------------------------------------------------------------------- /testdata/CertificateList/iOS-11.3.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CertificateList 6 | 7 | 8 | CommonName 9 | COMMON-NAME 10 | Data 11 | Base64= 12 | 13 | IsIdentity 14 | 15 | 16 | 17 | CommonName 18 | device-identity 19 | Data 20 | Base64= 21 | 22 | IsIdentity 23 | 24 | 25 | 26 | CommandUUID 27 | 506315e2-386a-44eb-9f46-e402afce7e80 28 | Status 29 | Acknowledged 30 | UDID 31 | 1c111c111c111c111c111c111c111c111c111c11 32 | 33 | 34 | -------------------------------------------------------------------------------- /testdata/CheckOut/10.11.x.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MessageType 6 | CheckOut 7 | Topic 8 | com.apple.mgmt.test.00000000-1111-2222-3333-444455556666 9 | UDID 10 | 00000000-1111-2222-3333-444455556666 11 | 12 | -------------------------------------------------------------------------------- /testdata/CheckOut/iOS-11.3.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MessageType 6 | CheckOut 7 | Topic 8 | com.apple.mgmt.XServer.1c111c11-1c11-1c11-1c11-1c111c111c11 9 | UDID 10 | 1c111c111c111c111c111c111c111c111c111c11 11 | 12 | -------------------------------------------------------------------------------- /testdata/DeviceLock/iOS-11.3.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | b6c8627f-bf4a-4ef4-8f40-d8673cd568c9 7 | MessageResult 8 | NoPasscodeSet 9 | Status 10 | Acknowledged 11 | UDID 12 | 1c111c111c111c111c111c111c111c111c111c11 13 | 14 | 15 | -------------------------------------------------------------------------------- /testdata/Errors/10.12.5-invalid-command.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 93a81e2a-8fbe-4e4b-b6e5-ee9148893f33 7 | ErrorChain 8 | 9 | 10 | ErrorCode 11 | 97 12 | ErrorDomain 13 | MDMClientError 14 | LocalizedDescription 15 | No \'Identifier\' in \'RemoveProfile\' command <MDMClientError:97> 16 | 17 | 18 | RequestType 19 | RemoveProfile 20 | Status 21 | Error 22 | UDID 23 | 00000000-1111-2222-3333-444455556666 24 | 25 | 26 | -------------------------------------------------------------------------------- /testdata/Errors/10.13.6-invalid-command.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | bac6348e-3291-4523-a354-037ec379b738 7 | ErrorChain 8 | 9 | 10 | ErrorCode 11 | 12021 12 | ErrorDomain 13 | MCMDMErrorDomain 14 | LocalizedDescription 15 | Unknown command: ShutdownDevice <MDMClientError:91> 16 | 17 | 18 | Status 19 | Error 20 | UDID 21 | 2F6DE437-7C14-5735-85B4-DC6B365BCAF1 22 | 23 | -------------------------------------------------------------------------------- /testdata/Errors/error_invalid_request_type.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 00000000-1111-2222-3333-444455556666 7 | ErrorChain 8 | 9 | 10 | ErrorCode 11 | 12021 12 | ErrorDomain 13 | MCMDMErrorDomain 14 | LocalizedDescription 15 | “OSUpdateStatus” is not a valid request type. 16 | USEnglishDescription 17 | “OSUpdateStatus” is not a valid request type. 18 | 19 | 20 | Status 21 | Error 22 | UDID 23 | 00000000-1111-2222-3333-444455556666 24 | 25 | -------------------------------------------------------------------------------- /testdata/Errors/iOS-11.3.1-AvailableOSUpdatesFailure.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 93289d88-c8da-4732-9a7e-33dd51851b6e 7 | ErrorChain 8 | 9 | 10 | ErrorCode 11 | 2213 12 | ErrorDomain 13 | DeviceManagement.error 14 | LocalizedDescription 15 | No update available. 16 | 17 | 18 | ErrorCode 19 | 3 20 | ErrorDomain 21 | com.apple.softwareupdateservices.errors 22 | LocalizedDescription 23 | The operation couldn\xe2\x80\x99t be completed. (com.apple.softwareupdateservices.errors error 3.) 24 | 25 | 26 | Status 27 | Error 28 | UDID 29 | 1c111c111c111c111c111c111c111c111c111c11 30 | 31 | 32 | -------------------------------------------------------------------------------- /testdata/Errors/iOS-11.3.1-CommandFormatError.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 52aab5d2-6a61-4fe8-b685-44f8b56972d0 7 | Status 8 | CommandFormatError 9 | UDID 10 | 1c111c111c111c111c111c111c111c111c111c11 11 | 12 | 13 | -------------------------------------------------------------------------------- /testdata/Errors/iOS-11.3.1-RemoveProfile-Unmanaged.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | b2da2591-a2e5-45e5-8dd8-0ffb050d8407 7 | ErrorChain 8 | 9 | 10 | ErrorCode 11 | 12013 12 | ErrorDomain 13 | MCMDMErrorDomain 14 | LocalizedDescription 15 | The profile \xe2\x80\x9corg.github.cmdmnt.commandment.trust\xe2\x80\x9d is not managed by MDM. 16 | USEnglishDescription 17 | The profile \xe2\x80\x9corg.github.cmdmnt.commandment.trust\xe2\x80\x9d is not managed by MDM. 18 | 19 | 20 | Status 21 | Error 22 | UDID 23 | 1c111c111c111c111c111c111c111c111c111c11 24 | 25 | 26 | -------------------------------------------------------------------------------- /testdata/InstallApplication/iOS-11.3.1-alreadyprompting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 94ee37e2-3e03-4bdf-ad9c-a81ccc9d78e9 7 | ErrorChain 8 | 9 | 10 | ErrorCode 11 | 1407 12 | ErrorDomain 13 | DeviceManagement.error 14 | LocalizedDescription 15 | The user is already being prompted. 16 | 17 | 18 | Status 19 | Error 20 | UDID 21 | 1c111c111c111c111c111c111c111c111c111c11 22 | 23 | 24 | -------------------------------------------------------------------------------- /testdata/InstallApplication/iOS-12.1-prompting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 7a9df3db-d661-4c1c-aa0e-56d69c8718a5 7 | Identifier 8 | com.tinyspeck.slackmacgap 9 | State 10 | Prompting 11 | Status 12 | Acknowledged 13 | UDID 14 | 1c111c111c111c111c111c111c111c111c111c11 15 | 16 | 17 | -------------------------------------------------------------------------------- /testdata/InstalledApplicationList/10.11.x.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 00000000-1111-2222-3333-444455556666 7 | InstalledApplicationList 8 | 9 | 10 | BundleSize 11 | 5855484 12 | Identifier 13 | com.apple.systempreferences 14 | Name 15 | System Preferences 16 | ShortVersion 17 | 14.0 18 | Version 19 | 14.0 20 | 21 | 22 | BundleSize 23 | 65092 24 | Name 25 | Set Info 26 | 27 | 28 | BundleSize 29 | 0 30 | Name 31 | Install OS X Yosemite 32 | 33 | 34 | RequestType 35 | InstalledApplicationList 36 | Status 37 | Acknowledged 38 | UDID 39 | 00000000-1111-2222-3333-444455556666 40 | 41 | -------------------------------------------------------------------------------- /testdata/ManagedApplicationList/iOS-11.3.1-Failed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | b3f87776-66a4-40f4-a194-8b451a2eacb1 7 | ManagedApplicationList 8 | 9 | com.apple.iWork.Keynote 10 | 11 | HasConfiguration 12 | 13 | HasFeedback 14 | 15 | IsValidated 16 | 17 | ManagementFlags 18 | 0 19 | Status 20 | Failed 21 | 22 | com.tinyspeck.slackmacgap 23 | 24 | HasConfiguration 25 | 26 | HasFeedback 27 | 28 | IsValidated 29 | 30 | ManagementFlags 31 | 0 32 | Status 33 | Failed 34 | 35 | 36 | Status 37 | Acknowledged 38 | UDID 39 | 1c111c111c111c111c111c111c111c111c111c11 40 | 41 | 42 | -------------------------------------------------------------------------------- /testdata/ManagedApplicationList/iOS-12.1-Installing.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | cfaa6f63-6ea9-493c-ab2e-a534937c5eda 7 | ManagedApplicationList 8 | 9 | com.apple.Numbers 10 | 11 | ExternalVersionIdentifier 12 | 829165942 13 | HasConfiguration 14 | 15 | HasFeedback 16 | 17 | IsValidated 18 | 19 | ManagementFlags 20 | 0 21 | Status 22 | Installing 23 | 24 | 25 | Status 26 | Acknowledged 27 | UDID 28 | 1c111c111c111c111c111c111c111c111c111c11 29 | 30 | 31 | -------------------------------------------------------------------------------- /testdata/ManagedApplicationList/iOS-12.1-Managed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 0a40830d-1cf9-4a00-a153-d8294e576c3f 7 | ManagedApplicationList 8 | 9 | com.apple.Numbers 10 | 11 | ExternalVersionIdentifier 12 | 829165942 13 | HasConfiguration 14 | 15 | HasFeedback 16 | 17 | IsValidated 18 | 19 | ManagementFlags 20 | 0 21 | Status 22 | Managed 23 | 24 | 25 | Status 26 | Acknowledged 27 | UDID 28 | 1c111c111c111c111c111c111c111c111c111c11 29 | 30 | 31 | -------------------------------------------------------------------------------- /testdata/ManagedApplicationList/iOS-12.1-RejectedPrompting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 2dce0324-3a9c-493d-bd7a-2d2a2985a67e 7 | ManagedApplicationList 8 | 9 | com.apple.iWork.Keynote 10 | 11 | HasConfiguration 12 | 13 | HasFeedback 14 | 15 | IsValidated 16 | 17 | ManagementFlags 18 | 0 19 | Status 20 | UserRejected 21 | 22 | com.tinyspeck.slackmacgap 23 | 24 | HasConfiguration 25 | 26 | HasFeedback 27 | 28 | IsValidated 29 | 30 | ManagementFlags 31 | 0 32 | Status 33 | UserRejected 34 | 35 | 36 | Status 37 | Acknowledged 38 | UDID 39 | 1c111c111c111c111c111c111c111c111c111c11 40 | 41 | 42 | -------------------------------------------------------------------------------- /testdata/NotNow/iOS-11.3.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | bb5d7813-e7c3-4279-b954-4b678925de5f 7 | Status 8 | NotNow 9 | UDID 10 | 1c111c111c111c111c111c111c111c111c111c11 11 | 12 | 13 | -------------------------------------------------------------------------------- /testdata/README.rst: -------------------------------------------------------------------------------- 1 | Test Data 2 | ========= 3 | 4 | This directory contains the test fixtures. 5 | 6 | You can also place test certificates here but they will be ignored by VCS. 7 | 8 | -------------------------------------------------------------------------------- /testdata/SecurityInfo/10.11.x.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 00000000-1111-2222-3333-444455556666 7 | RequestType 8 | SecurityInfo 9 | SecurityInfo 10 | 11 | FDE_Enabled 12 | 13 | 14 | Status 15 | Acknowledged 16 | UDID 17 | 00000000-1111-2222-3333-444455556666 18 | 19 | -------------------------------------------------------------------------------- /testdata/SecurityInfo/IOS-9.x.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | 00000000-1111-2222-3333-444455556666 7 | SecurityInfo 8 | 9 | HardwareEncryptionCaps 10 | 3 11 | PasscodeCompliant 12 | 13 | PasscodeCompliantWithProfiles 14 | 15 | PasscodeLockGracePeriod 16 | 0 17 | PasscodeLockGracePeriodEnforced 18 | 0 19 | PasscodePresent 20 | 21 | 22 | Status 23 | Acknowledged 24 | UDID 25 | 1111111111111111111111111111111111111111 26 | 27 | -------------------------------------------------------------------------------- /testdata/SecurityInfo/iOS-11.3.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | f1048316-6628-4b32-b2f2-708d7f4d7105 7 | SecurityInfo 8 | 9 | HardwareEncryptionCaps 10 | 3 11 | PasscodeCompliant 12 | 13 | PasscodeCompliantWithProfiles 14 | 15 | PasscodeLockGracePeriod 16 | 0 17 | PasscodeLockGracePeriodEnforced 18 | 0 19 | PasscodePresent 20 | 21 | 22 | Status 23 | Acknowledged 24 | UDID 25 | 1c111c111c111c111c111c111c111c111c111c11 26 | 27 | 28 | -------------------------------------------------------------------------------- /testdata/SecurityInfo/macOS-10.13.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CommandUUID 6 | bf88d054-bd86-480d-b406-a1bc74a403c0 7 | SecurityInfo 8 | 9 | FDE_Enabled 10 | 11 | FirewallSettings 12 | 13 | Applications 14 | 15 | 16 | Allowed 17 | 18 | Name 19 | pia_openvpn 20 | 21 | 22 | Allowed 23 | 24 | Name 25 | pia_openvpn_client 26 | 27 | 28 | BlockAllIncoming 29 | 30 | FirewallEnabled 31 | 32 | StealthMode 33 | 34 | 35 | FirmwarePasswordStatus 36 | 37 | SystemIntegrityProtectionEnabled 38 | 39 | 40 | Status 41 | Acknowledged 42 | UDID 43 | 00000000-1111-2222-3333-444455556666 44 | 45 | 46 | -------------------------------------------------------------------------------- /testdata/TokenUpdate/10.11.x-user.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MessageType 6 | TokenUpdate 7 | NotOnConsole 8 | 9 | PushMagic 10 | 00000000-1111-2222-3333-444455556666 11 | Token 12 | 13 | AAAA= 14 | 15 | Topic 16 | com.apple.mgmt.test.00000000-1111-2222-3333-444455556666 17 | UDID 18 | 00000000-1111-2222-3333-444455556666 19 | UserID 20 | 00000000-1111-2222-3333-444455556666 21 | UserLongName 22 | Administrator 23 | UserShortName 24 | admin 25 | 26 | -------------------------------------------------------------------------------- /testdata/TokenUpdate/10.11.x.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AwaitingConfiguration 6 | 7 | MessageType 8 | TokenUpdate 9 | PushMagic 10 | 00000000-1111-2222-3333-444455556666 11 | Token 12 | 13 | YXBwbGU= 14 | 15 | Topic 16 | com.apple.mgmt.test.00000000-1111-2222-3333-444455556666 17 | UDID 18 | 00000000-1111-2222-3333-444455556666 19 | 20 | -------------------------------------------------------------------------------- /testdata/TokenUpdate/10.12.2-user.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | MessageType 6 | TokenUpdate 7 | NotOnConsole 8 | 9 | PushMagic 10 | B81B1FEC-09C6-4EC2-871C-E521EC971B38 11 | Token 12 | MDAyMDEwNTItQTJEMC00MDNELUI4NTctNzJGOTEzRjVCQ0NECg== 13 | Topic 14 | com.apple.mgmt.commandment.dev 15 | UDID 16 | E3568F17-92ED-450A-8904-C3BF4CB7E9A5 17 | UserID 18 | A522C2FB-D0BA-487E-BBC6-BE0DB2DC7883 19 | UserLongName 20 | Commando Joe 21 | UserShortName 22 | cjoe 23 | 24 | -------------------------------------------------------------------------------- /testdata/TokenUpdate/10.12.2.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AwaitingConfiguration 6 | 7 | MessageType 8 | TokenUpdate 9 | PushMagic 10 | B81B1FEC-09C6-4EC2-871C-E521EC971B38 11 | Token 12 | MDAyMDEwNTItQTJEMC00MDNELUI4NTctNzJGOTEzRjVCQ0NFCg== 13 | Topic 14 | com.apple.mgmt.commandment.dev 15 | UDID 16 | E3568F17-92ED-450A-8904-C3BF4CB7E9A5 17 | 18 | -------------------------------------------------------------------------------- /testdata/TokenUpdate/iOS-11.3.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AwaitingConfiguration 6 | 7 | MessageType 8 | TokenUpdate 9 | PushMagic 10 | AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA 11 | Token 12 | 13 | Base64= 14 | 15 | Topic 16 | com.apple.mgmt.XServer.1c111c11-1c11-1c11-1c11-1c111c111c11 17 | UDID 18 | 1c111c111c111c111c111c111c111c111c111c11 19 | UnlockToken 20 | 21 | base64== 22 | 23 | 24 | -------------------------------------------------------------------------------- /testdata/decrypt_dep_token.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | openssl smime -decrypt -in "${1}" -recip "./dep-public.pem" -inkey "./dep-key.pem" 4 | -------------------------------------------------------------------------------- /testdata/dep/profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LANGUAGE 6 | en-AU 7 | PRODUCT 8 | iPad4,1 9 | SERIAL 10 | BLXLN1111111 11 | UDID 12 | 00000000000000000000000000000000 13 | VERSION 14 | 15A5278f 15 | 16 | 17 | -------------------------------------------------------------------------------- /testdata/mdmclient-PKIOperation.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/testdata/mdmclient-PKIOperation.der -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | JSON_API_HEADERS = { 2 | 'Content-Type': 'application/vnd.api+json', 3 | 'Accept': 'application/vnd.api+json' 4 | } -------------------------------------------------------------------------------- /tests/alembic_test.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = %(here)s/../commandment/alembic 6 | 7 | sqlalchemy.url = sqlite:///:memory: 8 | 9 | 10 | # Logging configuration 11 | [loggers] 12 | keys = root,sqlalchemy,alembic 13 | 14 | [handlers] 15 | keys = console 16 | 17 | [formatters] 18 | keys = generic 19 | 20 | [logger_root] 21 | level = INFO 22 | handlers = console 23 | qualname = 24 | 25 | [logger_sqlalchemy] 26 | level = WARNING 27 | handlers = 28 | qualname = sqlalchemy.engine 29 | 30 | [logger_alembic] 31 | level = WARNING 32 | handlers = 33 | qualname = alembic 34 | 35 | [handler_console] 36 | class = StreamHandler 37 | args = (sys.stderr,) 38 | level = NOTSET 39 | formatter = generic 40 | 41 | [formatter_generic] 42 | format = %(levelname)-5.5s [%(name)s] %(message)s 43 | datefmt = %H:%M:%S 44 | -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/tests/api/__init__.py -------------------------------------------------------------------------------- /tests/api/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | from tests.conftest import * 4 | from commandment.models import Device 5 | from sqlalchemy.orm.session import Session 6 | 7 | TEST_DIR = os.path.realpath(os.path.dirname(__file__)) 8 | TEST_DATA_DIR = os.path.realpath(TEST_DIR + '/../../testdata') 9 | 10 | 11 | @pytest.fixture(scope='function') 12 | def device(session: Session): 13 | """Create a fixture device which is referenced in all of the fake MDM responses by its UDID.""" 14 | d = Device( 15 | udid='00000000-1111-2222-3333-444455556666', 16 | device_name='commandment-mdmclient' 17 | ) 18 | session.add(d) 19 | session.commit() 20 | -------------------------------------------------------------------------------- /tests/client.py: -------------------------------------------------------------------------------- 1 | from flask.testing import FlaskClient 2 | 3 | 4 | class MDMClient(FlaskClient): 5 | """MDMClient is a superset of the flask testing client meant to perform higher level operations similar to the 6 | native mdmclient binary. 7 | 8 | Attributes: 9 | _private_key (rsa.RSAPrivateKey): RSA Private Key for the simulated client. 10 | _certificate (x509.Certificate): X.509 Certificate for the simulated client. 11 | """ 12 | 13 | def __init__(self, *args, **kwargs): 14 | self._private_key = kwargs.get('private_key', None) 15 | self._certificate = kwargs.get('certificate', None) 16 | super(MDMClient, self).__init__(*args, **kwargs) 17 | # self.environ_base['HTTP_MDM_SIGNATURE'] = b'Tk9UUkVBTA==' 18 | 19 | -------------------------------------------------------------------------------- /tests/dep/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/tests/dep/__init__.py -------------------------------------------------------------------------------- /tests/dep/test_dep.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from commandment.dep.dep import DEP 3 | 4 | 5 | @pytest.mark.depsim 6 | class TestDEP: 7 | def test_account(self, dep: DEP): 8 | dep.fetch_token() 9 | account = dep.account() 10 | assert account is not None 11 | 12 | def test_fetch_devices(self, dep: DEP): 13 | dep.fetch_token() 14 | devices = dep.fetch_devices() 15 | assert len(devices) == 500 16 | 17 | # def test_device_details(self, dep: DEP): 18 | # dep.fetch_token() 19 | # device_details = dep.device_detail() 20 | 21 | def test_fetch_cursor(self, dep: DEP): 22 | dep.fetch_token() 23 | for page in dep.devices(): 24 | print(len(page)) 25 | for d in page: 26 | print(d) 27 | 28 | -------------------------------------------------------------------------------- /tests/dep/test_dep_app.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | import json 4 | import sqlalchemy 5 | from flask import Response 6 | 7 | from commandment.dep.models import DEPProfile 8 | from commandment.models import Device 9 | from tests.client import MDMClient 10 | 11 | 12 | @pytest.mark.dep 13 | @pytest.mark.usefixtures("device", "dep_profile_committed") 14 | class TestDEPAPI: 15 | 16 | def test_post_dep_profile_relationship(self, client: MDMClient, session): 17 | """Test assignment of DEP Profile to device via relationship URL: 18 | /api/v1/devices//relationships/dep_profiles""" 19 | request_json = json.dumps({ 20 | "data": { 21 | "type": "dep_profiles", 22 | "id": "1", 23 | }, 24 | "jsonapi": { 25 | "version": "1.0" 26 | } 27 | }) 28 | 29 | response: Response = client.patch("/api/v1/devices/1/relationships/dep_profile", data=request_json, 30 | content_type="application/vnd.api+json") 31 | print(response.data) 32 | assert response.status_code == 200 33 | 34 | d: Device = session.query(Device).filter(Device.id == 1).one() 35 | assert d.dep_profile_id is not None 36 | -------------------------------------------------------------------------------- /tests/dep/test_dep_failures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from commandment.dep.dep import DEP 3 | from commandment.dep.errors import DEPServiceError 4 | 5 | @pytest.mark.depsim 6 | class TestDEPFailures: 7 | # NOTE: ensure that this is in exactly the same order as your DEPsim config. 8 | @pytest.mark.parametrize("expected_status,expected_text", [ 9 | (400, ""), 10 | (403, "ACCESS_DENIED"), 11 | (403, "T_C_NOT_SIGNED"), 12 | (405, ""), 13 | (401, "UNAUTHORIZED"), 14 | (429, "TOO_MANY_REQUESTS"), 15 | ]) 16 | def test_token_failure(self, dep: DEP, expected_status: int, expected_text: str): 17 | try: 18 | dep.fetch_token() 19 | except DEPServiceError as e: 20 | assert e.response.status_code == expected_status 21 | assert e.text == expected_text 22 | 23 | @pytest.mark.parametrize("expected_status,expected_text", [ 24 | (403, "ACCESS_DENIED"), 25 | (401, "UNAUTHORIZED"), 26 | ]) 27 | def test_account_failure(self, dep: DEP, expected_status: int, expected_text: str): 28 | try: 29 | dep.fetch_token() 30 | dep.account() 31 | except DEPServiceError as e: 32 | assert e.response.status_code == expected_status 33 | assert e.text == expected_text 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/dep/test_smime.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os.path 3 | import os 4 | from cryptography.hazmat.backends import default_backend 5 | from cryptography.hazmat.primitives.asymmetric import rsa 6 | from cryptography.hazmat.primitives import serialization, hashes 7 | from commandment.dep import smime 8 | 9 | DEP_TOKEN_SMIME_PATH = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata', 'dep_smime.p7m') 10 | DEP_TOKEN_KEY_PATH = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata', 'dep_key.pem') 11 | 12 | 13 | class TestDepSmime: 14 | def test_decrypt(self): 15 | with open(DEP_TOKEN_SMIME_PATH, 'rb') as fd: 16 | message = fd.read() 17 | 18 | with open(DEP_TOKEN_KEY_PATH, 'rb') as fd: 19 | pem_key = fd.read() 20 | 21 | pk = serialization.load_pem_private_key( 22 | pem_key, 23 | backend=default_backend(), 24 | password=None, 25 | ) 26 | 27 | result = smime.decrypt(message, pk) 28 | print(result) 29 | -------------------------------------------------------------------------------- /tests/mdm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/tests/mdm/__init__.py -------------------------------------------------------------------------------- /tests/mdm/test_certificate_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | from flask import Response 4 | from tests.client import MDMClient 5 | from commandment.mdm import CommandStatus 6 | from commandment.models import Command, Device 7 | 8 | TEST_DIR = os.path.realpath(os.path.dirname(__file__)) 9 | 10 | 11 | @pytest.fixture() 12 | def certificate_list_response(): 13 | with open(os.path.join(TEST_DIR, '../../testdata/CertificateList/10.11.x.xml'), 'r') as fd: 14 | plist_data = fd.read() 15 | 16 | return plist_data 17 | 18 | 19 | @pytest.fixture(scope='function') 20 | def certificate_list_command(session): 21 | c = Command( 22 | uuid='00000000-1111-2222-3333-444455556666', 23 | request_type='CertificateList', 24 | status=CommandStatus.Sent.value, 25 | parameters={}, 26 | ) 27 | session.add(c) 28 | session.commit() 29 | 30 | 31 | @pytest.mark.usefixtures("device", "certificate_list_command") 32 | class TestCertificateList: 33 | 34 | def test_certificate_list_response(self, client: MDMClient, certificate_list_response: str, session): 35 | response: Response = client.put('/mdm', data=certificate_list_response, content_type='text/xml') 36 | assert response.status_code != 410 37 | assert response.status_code == 200 38 | 39 | d = session.query(Device).filter(Device.udid == '00000000-1111-2222-3333-444455556666').one() 40 | ic = d.installed_certificates 41 | assert len(ic) == 2 42 | 43 | -------------------------------------------------------------------------------- /tests/mdm/test_device_information.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | from flask import Response 4 | from tests.client import MDMClient 5 | 6 | TEST_DIR = os.path.realpath(os.path.dirname(__file__)) 7 | 8 | 9 | @pytest.fixture() 10 | def device_information_response(): 11 | with open(os.path.join(TEST_DIR, '../../testdata/DeviceInformation/10.11.x.xml'), 'r') as fd: 12 | plist_data = fd.read() 13 | 14 | return plist_data 15 | 16 | 17 | @pytest.mark.usefixtures("device") 18 | class TestDeviceInformation: 19 | 20 | def test_device_information_response(self, client: MDMClient, device_information_response: str): 21 | response: Response = client.put('/mdm', data=device_information_response, content_type='text/xml') 22 | assert response.status_code != 410 23 | assert response.status_code == 200 24 | -------------------------------------------------------------------------------- /tests/mdm/test_profile_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | from flask import Response 4 | from tests.client import MDMClient 5 | 6 | 7 | TEST_DIR = os.path.realpath(os.path.dirname(__file__)) 8 | 9 | 10 | @pytest.fixture() 11 | def profile_list_response() -> str: 12 | with open(os.path.join(TEST_DIR, '../../testdata/ProfileList/10.11.x.xml'), 'r') as fd: 13 | plist_data = fd.read() 14 | 15 | return plist_data 16 | 17 | 18 | @pytest.mark.usefixtures("device") 19 | class TestProfileList: 20 | 21 | def test_profile_list_response(self, client: MDMClient, profile_list_response: str): 22 | response: Response = client.put('/mdm', data=profile_list_response, content_type='text/xml') 23 | assert response.status_code != 410 24 | assert response.status_code == 200 25 | -------------------------------------------------------------------------------- /tests/pkg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/tests/pkg/__init__.py -------------------------------------------------------------------------------- /tests/pki/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/tests/pki/__init__.py -------------------------------------------------------------------------------- /tests/pki/test_ca.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/tests/pki/test_ca.py -------------------------------------------------------------------------------- /tests/pki/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os.path 3 | import logging 4 | from cryptography import x509 5 | from cryptography.hazmat.primitives.asymmetric import rsa 6 | from commandment.pki.models import RSAPrivateKey, CACertificate 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class TestModels: 12 | 13 | def test_rsa_privatekey_from_crypto(self, private_key: rsa.RSAPrivateKeyWithSerialization, session): 14 | m = RSAPrivateKey.from_crypto(private_key) 15 | session.add(m) 16 | session.commit() 17 | 18 | assert m.id is not None 19 | assert m.pem_data is not None 20 | 21 | def test_ca_certificate_from_crypto(self, ca_certificate: x509.Certificate, session): 22 | m = CACertificate.from_crypto(ca_certificate) 23 | session.add(m) 24 | session.commit() 25 | 26 | assert m.id is not None 27 | assert m.pem_data is not None 28 | assert m.fingerprint is not None 29 | assert m.x509_cn is not None 30 | 31 | -------------------------------------------------------------------------------- /tests/pki/test_openssl.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os.path 3 | import logging 4 | from cryptography import x509 5 | from cryptography.hazmat.primitives.asymmetric import rsa 6 | from commandment.pki import openssl 7 | import oscrypto 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class TestOpenssl: 13 | 14 | def test_pkcs12_from_crypto(self, private_key: rsa.RSAPrivateKeyWithSerialization, certificate: x509.Certificate): 15 | pkcs12_data = openssl.create_pkcs12(private_key, certificate) 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/pki/test_ormutils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os.path 3 | import logging 4 | from cryptography import x509 5 | from cryptography.hazmat.primitives.asymmetric import rsa 6 | from commandment.pki.models import RSAPrivateKey, CACertificate 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class TestORMUtils: 13 | 14 | def test_find_recipient(self, certificate): 15 | pass 16 | -------------------------------------------------------------------------------- /tests/threads/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/tests/threads/__init__.py -------------------------------------------------------------------------------- /tests/threads/test_startup_thread.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from commandment.threads import startup_thread 3 | from commandment.pki.models import CACertificate 4 | 5 | 6 | class TestStartupThread: 7 | 8 | def test_startup_thread_ca(self, session): 9 | """Assert that the startup thread actually creates self-signed certificates.""" 10 | startup_thread.startup_callback() 11 | certificate = session.query(CACertificate).one() 12 | assert certificate.x509_cn == 'COMMANDMENT-CA' 13 | assert certificate.pem_data is not None 14 | assert certificate.fingerprint is not None 15 | -------------------------------------------------------------------------------- /tests/vpp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/tests/vpp/__init__.py -------------------------------------------------------------------------------- /ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parser": "@typescript-eslint/parser", 12 | parserOptions: { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | "plugins": [ 20 | "react", 21 | "@typescript-eslint" 22 | ], 23 | "rules": {} 24 | }; -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # Yarn Integrity file 54 | .yarn-integrity 55 | 56 | # dotenv environment variables file 57 | .env 58 | 59 | 60 | -------------------------------------------------------------------------------- /ui/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | const req = require.context('../src/stories', true, /.stories.tsx$/); 4 | 5 | function loadStories() { 6 | require('../src/stories/index.ts'); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /ui/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const {CheckerPlugin} = require('awesome-typescript-loader'); 4 | 5 | module.exports = (baseConfig, env, defaultConfig) => { 6 | defaultConfig.module.rules.push({ 7 | test: /\.tsx?$/, 8 | include: path.resolve(__dirname, '../src'), 9 | loader: require.resolve('awesome-typescript-loader') 10 | }); 11 | defaultConfig.plugins.push(new CheckerPlugin()); 12 | defaultConfig.resolve.extensions.push('.ts', '.tsx'); 13 | 14 | // defaultConfig.module.rules.push({ 15 | // test: /\.jsx?$/, 16 | // include: [ 17 | // path.resolve(__dirname, "node_modules/semantic-ui-react"), 18 | // path.resolve(__dirname, "node_modules/byte-size") 19 | // ], 20 | // loader: "babel-loader" 21 | // }); 22 | 23 | return defaultConfig; 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /ui/_deprecated/assistant/FinalStep.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface FinalStepProps { 4 | 5 | } 6 | 7 | export class FinalStep extends React.Component { 8 | 9 | render() { 10 | return ( 11 | 12 | Success 13 | 14 | 15 | 16 | Congratulations, your commandment server is set up! 17 | 18 | If your devices are not DEP provisioned, 19 | use the link below to download an enrollment profile. 20 | 21 | Enroll 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /ui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | 4 | return { 5 | plugins: [ 6 | "@babel/plugin-proposal-export-default-from", 7 | "@babel/plugin-syntax-jsx", 8 | "@babel/plugin-transform-react-jsx", 9 | "@babel/plugin-transform-react-display-name", 10 | "@babel/plugin-proposal-class-properties", 11 | "@babel/plugin-proposal-export-namespace-from", 12 | "react-hot-loader/babel" 13 | ], 14 | presets: [ 15 | 16 | ], 17 | }; 18 | }; -------------------------------------------------------------------------------- /ui/sass/_dropzone.scss: -------------------------------------------------------------------------------- 1 | .dropzone { 2 | margin: 1rem 0; 3 | height: 5rem; 4 | 5 | border: 1px solid rgba(34,36,38,.1); 6 | background-color: #F9FAFB; 7 | text-align: center; 8 | cursor: pointer; 9 | 10 | .ui.header { 11 | line-height: 5rem; 12 | } 13 | } -------------------------------------------------------------------------------- /ui/sass/_helper.scss: -------------------------------------------------------------------------------- 1 | @import 'settings'; 2 | 3 | .error { 4 | font-weight: bold; 5 | color: $highlight-color; 6 | } -------------------------------------------------------------------------------- /ui/sass/_nav.scss: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: inherit; 3 | } 4 | 5 | //.navigation { 6 | // box-sizing: border-box; 7 | // display: flex; 8 | // align-items: center; 9 | // width: 100%; 10 | // background: #eee; 11 | // padding: 20px; 12 | //} 13 | 14 | nav { 15 | background: #eee; 16 | } 17 | 18 | // top-level menu 19 | nav ul { 20 | margin: 0; 21 | list-style: none; 22 | position: relative; 23 | display: flex; 24 | //padding: 1rem 0; // same as milligram body padding 25 | 26 | li { 27 | float: left; 28 | margin: 0; // cancel out the milligram default margin 29 | 30 | a { 31 | padding: 1rem 2rem; 32 | display: block; 33 | } 34 | 35 | span { // if not using a link 36 | display: inline-block; 37 | padding: 1rem 2rem; 38 | } 39 | } 40 | 41 | li:hover { 42 | // hover style 43 | } 44 | 45 | // make submenu active 46 | li:hover > ul { 47 | display: block; 48 | } 49 | 50 | li:active > ul { 51 | display: block; 52 | } 53 | } 54 | 55 | nav ul:after { 56 | content: ""; clear: both; display: block; 57 | } 58 | 59 | // submenu 60 | nav ul ul { 61 | z-index: 99; 62 | background-color: #9b4dca; 63 | position: absolute; 64 | top: 100%; 65 | 66 | display: none; 67 | padding: 0; 68 | 69 | li { 70 | float: none; 71 | position: relative; 72 | 73 | a { 74 | padding: 1rem 2rem; 75 | color: #fff; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ui/sass/_settings.scss: -------------------------------------------------------------------------------- 1 | $highlight-color: #d241c7; 2 | -------------------------------------------------------------------------------- /ui/sass/_upload.scss: -------------------------------------------------------------------------------- 1 | .ap-upload-input { 2 | background-color: #eee; 3 | border: 1px solid #aaa; 4 | border-radius: 0.4rem; 5 | } -------------------------------------------------------------------------------- /ui/src/@types/byte-size/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "byte-size" { 2 | 3 | export = byteSize 4 | 5 | function byteSize(bytes: number, options?: byteSize.Options): byteSize.ByteSize; 6 | namespace byteSize { 7 | export interface Options { 8 | precision: number; 9 | units: "metric" | "iec" | "metric_octet" | "iec_octet" 10 | } 11 | 12 | export interface ByteSize { 13 | value: string; 14 | unit: string; 15 | toString(): string; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/components/ActionMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Dropdown} from "semantic-ui-react"; 3 | 4 | export enum UIActionTypes { 5 | BLANK_PUSH = "BLANK_PUSH", 6 | CLEAR_PASSCODE = "CLEAR_PASSCODE", 7 | FULL_INVENTORY = "FULL_INVENTORY", 8 | } 9 | 10 | export interface IActionMenu { 11 | enabledActions: UIActionTypes[]; 12 | } 13 | 14 | export const ActionMenu: React.FunctionComponent = (props: IActionMenu) => ( 15 | 20 | ); 21 | -------------------------------------------------------------------------------- /ui/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {hot} from "react-hot-loader"; 3 | 4 | /** 5 | * AppLayout is the top level root display component. 6 | * 7 | * It is recommended to keep this as a class and not a stateless component, due to earlier issues with react-router not 8 | * updating children. 9 | * 10 | * It is also recommended to keep this as an unconnected component for the same reason. 11 | * 12 | * @see https://github.com/ReactTraining/react-router/issues/4975 13 | */ 14 | class AppCool extends React.Component<{}, {}> { 15 | public render() { 16 | const {children} = this.props; 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | } 25 | 26 | export const App = hot(module)(AppCool); 27 | -------------------------------------------------------------------------------- /ui/src/components/BareLayout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import {Grid} from "semantic-ui-react"; 4 | import {Route, RouteProps} from "react-router"; 5 | import {ComponentClass, FunctionComponent} from "react"; 6 | 7 | interface INavigationLayout { 8 | component: ComponentClass; 9 | } 10 | 11 | export const BareLayout: FunctionComponent = ({ Component: component, ...rest }) => ( 12 | ( 13 | 14 | 15 | 16 | )} /> 17 | ); 18 | -------------------------------------------------------------------------------- /ui/src/components/CertificateTypeIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Icon} from "semantic-ui-react"; 3 | 4 | interface CertificateTypeIconProps { 5 | value: number; 6 | title: string; 7 | } 8 | 9 | export const CertificateTypeIcon: React.StatelessComponent = (props: CertificateTypeIconProps): JSX.Element => { 10 | return ; 11 | }; 12 | -------------------------------------------------------------------------------- /ui/src/components/CheckListItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {List} from "semantic-ui-react"; 3 | 4 | interface ICheckListItemProps { 5 | title: string; 6 | description?: string; 7 | value: any; // will be interpreted as boolean 8 | children?: JSX.Element[] | JSX.Element; 9 | } 10 | 11 | export const CheckListItem: React.FunctionComponent = ({ title, value, description, children }: ICheckListItemProps) => ( 12 | 13 | {value ? : } 14 | 15 | {title} 16 | {children && 17 | 18 | {children} 19 | 20 | } 21 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /ui/src/components/DeviceActions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface DeviceActionsProps { 4 | 5 | } 6 | 7 | export const DeviceActions: React.StatelessComponent = (props: DeviceActionsProps) => ( 8 | 9 | ); -------------------------------------------------------------------------------- /ui/src/components/Navigation.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/ui/src/components/Navigation.scss -------------------------------------------------------------------------------- /ui/src/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Menu} from "semantic-ui-react"; 3 | 4 | import {MenuItemLink} from "../components/semantic-ui/MenuItemLink"; 5 | import "./Navigation.scss"; 6 | 7 | export interface INavigationProps { 8 | 9 | } 10 | 11 | export const Navigation: React.StatelessComponent = (props: INavigationProps) => ( 12 | 13 | CMDMNT 14 | Devices 15 | Profiles 16 | Applications 17 | Settings 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /ui/src/components/NavigationLayout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import {Grid} from "semantic-ui-react"; 4 | import {NavigationVertical} from "./NavigationVertical"; 5 | import {RouteComponentProps} from "react-router"; 6 | import {ComponentProps, FunctionComponent} from "react"; 7 | 8 | export const NavigationLayout: FunctionComponent = (props: RouteComponentProps & ComponentProps) => ( 9 | 10 | 11 | 12 | 13 | 14 | {props.children} 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /ui/src/components/NavigationVertical.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import {MenuItemLink} from "../components/semantic-ui/MenuItemLink"; 4 | import "./Navigation.scss"; 5 | import {RouteComponentProps} from "react-router"; 6 | import {Divider, Sidebar, Menu} from "semantic-ui-react"; 7 | 8 | interface IRouteProps { 9 | } 10 | 11 | export interface INavigationVerticalProps extends RouteComponentProps { 12 | 13 | } 14 | 15 | export const NavigationVertical: React.FC = (props: INavigationVerticalProps) => ( 16 | 17 | CMDMNT 18 | Devices 19 | Profiles 20 | Applications 21 | Settings 22 | 23 | Logout 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /ui/src/components/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {connect} from "react-redux"; 3 | import {FunctionComponent, Component} from "react"; 4 | import {Redirect, Route} from "react-router"; 5 | import {RootState} from "../reducers"; 6 | 7 | export interface IProtectedRoute { 8 | component: Component; 9 | access_token: string; 10 | } 11 | 12 | const UnconnectedProtectedRoute: FunctionComponent = 13 | ({component: Component, access_token, ...rest}: Partial) => ( 14 | 15 | access_token ? ( 18 | 19 | ) : ( 20 | 24 | )} 25 | /> 26 | ); 27 | 28 | export const ProtectedRoute = connect((state: RootState) => { 29 | return { 30 | access_token: state.auth.access_token, 31 | expires_in: state.auth.expires_in, 32 | } 33 | }, null)(UnconnectedProtectedRoute); 34 | -------------------------------------------------------------------------------- /ui/src/components/RSAAApiErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {ApiError} from "redux-api-middleware"; 3 | import {Message} from "semantic-ui-react"; 4 | import {JSONAPIErrorObject, JSONAPIErrorResponse} from "../store/json-api"; 5 | 6 | export interface IRSAAApiErrorMessageProps { 7 | error: ApiError; 8 | } 9 | 10 | export const RSAAApiErrorMessage: React.FunctionComponent = 11 | (props: IRSAAApiErrorMessageProps) => ( 12 | `${err.detail}`), 18 | ]} 19 | /> 20 | ); 21 | -------------------------------------------------------------------------------- /ui/src/components/devices/MacOSDeviceDetail.scss: -------------------------------------------------------------------------------- 1 | .MacOSDeviceDetail { 2 | i.icon { 3 | width: 1.5em; 4 | } 5 | } 6 | 7 | .ui.item { 8 | padding: 1em 0; 9 | } -------------------------------------------------------------------------------- /ui/src/components/devices/ModelIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {SemanticICONS} from "semantic-ui-react"; 3 | import {Icon} from "semantic-ui-react"; 4 | 5 | interface IModelIconProps { 6 | value: string; 7 | title: string; 8 | } 9 | 10 | export const ModelIcon = (props: IModelIconProps): JSX.Element => { 11 | const icons: { [propName: string]: SemanticICONS; } = { 12 | "Mac Pro": "computer", 13 | "MacBook Air": "laptop", 14 | "MacBook Pro": "laptop", 15 | "iMac": "desktop", 16 | "iPad": "tablet", 17 | "iPhone": "mobile", 18 | }; 19 | 20 | let className: SemanticICONS = "apple"; 21 | if (icons.hasOwnProperty(props.value)) { 22 | className = icons[props.value]; 23 | } 24 | 25 | return ; 26 | }; 27 | -------------------------------------------------------------------------------- /ui/src/components/errors/ApiError.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {ApiError} from "redux-api-middleware"; 3 | import {Message} from "semantic-ui-react"; 4 | 5 | export interface IApiErrorProps { 6 | error: ApiError; 7 | } 8 | 9 | export const ApiError: React.FC = ({ error }: IApiErrorProps) => ( 10 | 11 | Unhandled API Error. This might be a bug 12 | { error.response.code } 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /ui/src/components/formik/FormikCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import {Field, FieldConfig, FieldProps} from "formik"; 2 | import * as React from "react"; 3 | import {Form, FormProps, Checkbox, CheckboxProps} from "semantic-ui-react"; 4 | 5 | export type IFormikCheckbox = FieldConfig & CheckboxProps; 6 | 7 | export const FormikCheckbox: React.SFC = ({ 8 | id, name, label, toggle, 9 | }) => ( 10 | { 13 | const error = form.touched[name] && form.errors[name]; 14 | return ( 15 | 16 | 23 | {error ? ( 24 | {form.errors[name]} 25 | ) : null} 26 | 27 | ); 28 | }} 29 | /> 30 | ); 31 | -------------------------------------------------------------------------------- /ui/src/components/forms/DEPAccountForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Form} from "semantic-ui-react"; 3 | 4 | export class DEPAccountForm extends React.Component { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | } -------------------------------------------------------------------------------- /ui/src/components/itunes/MASResult.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Image, List, Grid, Button } from "semantic-ui-react"; 3 | import {ArtworkIconSize, IiTunesSoftwareSearchResult} from "../../store/applications/itunes"; 4 | 5 | 6 | export interface IMASResultProps { 7 | icon?: ArtworkIconSize; 8 | data: IiTunesSoftwareSearchResult; 9 | isAdded: boolean; 10 | onClickAdd: (result: IiTunesSoftwareSearchResult) => void; 11 | } 12 | 13 | export const MASResult: React.FunctionComponent = ({ data, onClickAdd, isAdded = false, icon }: IMASResultProps) => ( 14 | 15 | 16 | 17 | {data.trackName} 18 | 19 | 20 | {data.artistName} 21 | Version {data.version} 22 | 23 | 24 | 25 | 26 | 27 | (onClickAdd(data))}>{isAdded ? "Added" : "Add"} 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /ui/src/components/react-table/AppName.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Link} from "react-router-dom"; 3 | import {CellInfo} from "react-table"; 4 | 5 | export const AppName: React.FunctionComponent = ({ value, original }: CellInfo) => ( 6 | 7 | {value ? value : original.attributes.display_name} 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /ui/src/components/react-table/ApplicationType.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {CellInfo} from "react-table"; 3 | import {Icon} from "semantic-ui-react"; 4 | import {FunctionComponent, ReactElement, ReactNode} from "react"; 5 | 6 | const icons: { [status: string]: ReactNode } = { 7 | "appstore_mac": , 8 | "appstore_ios": , 9 | }; 10 | 11 | export const ApplicationType: FunctionComponent = ({ value }: CellInfo) => ( 12 | {icons.hasOwnProperty(value) ? icons[value] : null} 13 | ); 14 | -------------------------------------------------------------------------------- /ui/src/components/react-table/ByteSize.tsx: -------------------------------------------------------------------------------- 1 | import * as byteSize from "byte-size"; 2 | import * as React from "react"; 3 | import {CellInfo} from "react-table"; 4 | 5 | export const ByteSize: React.FunctionComponent = ({ value, original }) => ( 6 | {value ? byteSize(value).value + " " + byteSize(value).unit : ""} 7 | ); 8 | -------------------------------------------------------------------------------- /ui/src/components/react-table/CommandStatus.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {CellInfo} from "react-table"; 3 | import {Icon, IconProps} from "semantic-ui-react"; 4 | import {FunctionComponent, ReactElement} from "react"; 5 | 6 | const icons: { [status: string]: JSX.Element } = { 7 | "CommandStatus.Acknowledged": , 8 | "CommandStatus.Error": , 9 | "CommandStatus.NotNow": , 10 | "CommandStatus.Queued": , 11 | "CommandStatus.Sent": , 12 | }; 13 | 14 | export const CommandStatus: FunctionComponent = ({ value }: CellInfo) => ( 15 | {icons.hasOwnProperty(value) ? icons[value] : null} 16 | ); 17 | -------------------------------------------------------------------------------- /ui/src/components/react-table/DEPAccountServerName.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Link} from "react-router-dom"; 3 | import {CellInfo} from "react-table"; 4 | 5 | export const DEPAccountServerName: React.FunctionComponent = ({ value, original }) => ( 6 | 7 | {value ? value : original.attributes.server_name} 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /ui/src/components/react-table/DEPProfileName.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Link} from "react-router-dom"; 3 | import {CellInfo} from "react-table"; 4 | 5 | export const DEPProfileName: React.FunctionComponent = ({ value, original }) => { 6 | return ( 7 | 8 | {value ? value : original.attributes.profile_name} 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /ui/src/components/react-table/DeviceName.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Link} from "react-router-dom"; 3 | import {CellInfo} from "react-table"; 4 | 5 | export const DeviceName: React.FunctionComponent = ({ value, original }) => ( 6 | 7 | {value ? value : original.attributes.description} 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /ui/src/components/react-table/ObjectLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {CellInfo} from "react-table"; 3 | import {Link} from "react-router-dom"; 4 | 5 | export const ObjectLink: React.FunctionComponent = ({ value, original }: CellInfo) => ( 6 | 7 | {value ? value : original.attributes.display_name} 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /ui/src/components/react-table/ProfileName.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Link} from "react-router-dom"; 3 | import {CellInfo} from "react-table"; 4 | 5 | export const ProfileName: React.FunctionComponent = ({ value, original }: CellInfo) => ( 6 | 7 | {value ? value : original.attributes.display_name} 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /ui/src/components/react-table/RelativeToNow.tsx: -------------------------------------------------------------------------------- 1 | import {distanceInWordsToNow, parse} from "date-fns"; 2 | import * as React from "react"; 3 | import {CellInfo} from "react-table"; 4 | 5 | export const RelativeToNow: React.FunctionComponent = ({ value, original }) => ( 6 | {value ? distanceInWordsToNow(parse(value), {addSuffix: true}) : ""} 7 | ); 8 | -------------------------------------------------------------------------------- /ui/src/components/react-tables/AppDeployStatusTable.tsx: -------------------------------------------------------------------------------- 1 | import {distanceInWordsToNow} from "date-fns"; 2 | import * as React from "react"; 3 | import ReactTable, {CellInfo, TableProps} from "react-table"; 4 | import {JSONAPIDataObject} from "../../store/json-api"; 5 | import {ManagedApplication} from "../../store/applications/types"; 6 | 7 | export interface IAppDeployStatusTableProps { 8 | loading: boolean; 9 | data: Array>; 10 | onFetchData: (state: any, instance: any) => void; 11 | } 12 | 13 | const columns = [ 14 | { 15 | 16 | }, 17 | { 18 | Header: "Bundle ID", 19 | accessor: "attributes.bundle_id", 20 | id: "bundle_id", 21 | }, 22 | { 23 | Header: "Status", 24 | accessor: "attributes.status", 25 | id: "status", 26 | }, 27 | ]; 28 | 29 | export const AppDeployStatusTable = ({ data, ...props }: IAppDeployStatusTableProps & Partial) => ( 30 | 37 | ); 38 | -------------------------------------------------------------------------------- /ui/src/components/react-tables/DEPAccountsTable.tsx: -------------------------------------------------------------------------------- 1 | import {distanceInWordsToNow} from "date-fns"; 2 | import * as React from "react"; 3 | import ReactTable, {CellInfo, TableProps, Column} from "react-table"; 4 | import selectTableHoc from "react-table/lib/hoc/selectTable"; 5 | import {JSONAPIDataObject} from "../../store/json-api"; 6 | import {DEPAccount} from "../../store/dep/types"; 7 | import {DEPAccountServerName} from "../react-table/DEPAccountServerName"; 8 | // import "react-table/react-table.css"; 9 | 10 | export interface IDEPAccountsTableProps { 11 | loading: boolean; 12 | data: Array>; 13 | onToggleSelection: () => void; 14 | onToggleAll: () => void; 15 | } 16 | 17 | const columns: Column[] = [ 18 | { 19 | Cell: DEPAccountServerName, 20 | Header: "Server Name", 21 | accessor: "attributes.server_name", 22 | id: "server_name", 23 | }, 24 | { 25 | Header: "Organization", 26 | accessor: "attributes.org_name", 27 | id: "org_name", 28 | }, 29 | ]; 30 | 31 | const ReactSelectTable = selectTableHoc(ReactTable); 32 | 33 | export const DEPAccountsTable = ({ data, ...props }: IDEPAccountsTableProps & Partial) => ( 34 | 42 | ); 43 | -------------------------------------------------------------------------------- /ui/src/components/react-tables/DEPProfilesTable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactTable, {TableProps, Column} from "react-table"; 3 | import selectTableHoc from "react-table/lib/hoc/selectTable"; 4 | import {DEPAccount, DEPProfile} from "../../store/dep/types"; 5 | import {DEPProfileName} from "../react-table/DEPProfileName"; 6 | import {JSONAPIDataObject} from "../../store/json-api"; 7 | // import "react-table/react-table.css"; 8 | 9 | export interface IDEPProfilesTableProps { 10 | loading: boolean; 11 | data: Array>; 12 | onToggleSelection: () => void; 13 | onToggleAll: () => void; 14 | } 15 | 16 | const columns: Column[] = [ 17 | { 18 | Cell: DEPProfileName, 19 | Header: "Name", 20 | accessor: "attributes.profile_name", 21 | id: "profile_name", 22 | }, 23 | { 24 | Header: "UUID", 25 | accessor: "attributes.uuid", 26 | id: "uuid", 27 | }, 28 | ]; 29 | 30 | const ReactSelectTable = selectTableHoc(ReactTable); 31 | 32 | export const DEPProfilesTable = ({ data, ...props }: IDEPProfilesTableProps & Partial) => ( 33 | 40 | ); 41 | -------------------------------------------------------------------------------- /ui/src/components/react-tables/DeviceApplicationsTable.tsx: -------------------------------------------------------------------------------- 1 | import {distanceInWordsToNow} from "date-fns"; 2 | import * as React from "react"; 3 | import ReactTable, {CellInfo, TableProps} from "react-table"; 4 | import {InstalledApplication} from "../../store/device/types"; 5 | import {JSONAPIDataObject} from "../../store/json-api"; 6 | import {ByteSize} from "../react-table/ByteSize"; 7 | import {DEPAccount} from "../../store/dep/types"; 8 | 9 | export interface IDeviceApplicationsTableProps { 10 | loading: boolean; 11 | data: Array>; 12 | onFetchData: (state: any, instance: any) => void; 13 | } 14 | 15 | const columns = [ 16 | { 17 | Header: "Name", 18 | accessor: "attributes.name", 19 | id: "name", 20 | }, 21 | { 22 | Header: "Version", 23 | accessor: "attributes.short_version", 24 | id: "short_version", 25 | maxWidth: 140, 26 | }, 27 | { 28 | Cell: ByteSize, 29 | Header: "Size", 30 | accessor: "attributes.bundle_size", 31 | id: "bundle_size", 32 | maxWidth: 100, 33 | }, 34 | ]; 35 | 36 | export const DeviceApplicationsTable = ({ data, ...props }: IDeviceApplicationsTableProps & Partial) => ( 37 | 45 | ); 46 | -------------------------------------------------------------------------------- /ui/src/components/react-tables/DeviceCertificatesTable.tsx: -------------------------------------------------------------------------------- 1 | import {distanceInWordsToNow} from "date-fns"; 2 | import * as React from "react"; 3 | import ReactTable, {CellInfo, TableProps} from "react-table"; 4 | import {JSONAPIDataObject} from "../../store/json-api"; 5 | import {InstalledCertificate} from "../../store/device/types"; 6 | import {DEPAccount} from "../../store/dep/types"; 7 | 8 | export interface IDeviceCertificateTableProps { 9 | loading: boolean; 10 | data: Array>; 11 | onFetchData: (state: any, instance: any) => void; 12 | } 13 | 14 | const columns = [ 15 | { 16 | Header: "Common Name", 17 | accessor: (certificate: JSONAPIDataObject) => certificate.attributes.x509_cn, 18 | id: "x509_cn", 19 | }, 20 | ]; 21 | 22 | export const DeviceCertificatesTable = ({ data, ...props }: IDeviceCertificateTableProps & Partial) => ( 23 | 30 | ); 31 | -------------------------------------------------------------------------------- /ui/src/components/react-tables/DeviceCommandsTable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactTable, {TableProps} from "react-table"; 3 | import {Command} from "../../store/device/types"; 4 | import {RelativeToNow} from "../react-table/RelativeToNow"; 5 | import {CommandStatus} from "../react-table/CommandStatus"; 6 | import {JSONAPIDataObject} from "../../store/json-api"; 7 | import {DEPAccount} from "../../store/dep/types"; 8 | 9 | export interface IDeviceCommandsTableProps { 10 | loading: boolean; 11 | data: Array>; 12 | onFetchData: (state: any, instance: any) => void; 13 | } 14 | 15 | const columns = [ 16 | { 17 | Cell: CommandStatus, 18 | Header: "Status", 19 | accessor: "attributes.status", 20 | id: "status", 21 | maxWidth: 50, 22 | style: { textAlign: "center" }, 23 | }, 24 | { 25 | Header: "Type", 26 | accessor: "attributes.request_type", 27 | id: "request_type", 28 | }, 29 | { 30 | Cell: RelativeToNow, 31 | Header: "Sent", 32 | accessor: "attributes.sent_at", 33 | id: "sent_at", 34 | }, 35 | ]; 36 | 37 | export const DeviceCommandsTable = ({ data, ...props }: IDeviceCommandsTableProps & Partial) => ( 38 | 45 | ); 46 | -------------------------------------------------------------------------------- /ui/src/components/react-tables/DeviceProfilesTable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactTable, {TableProps} from "react-table"; 3 | import {InstalledProfile} from "../../store/device/types"; 4 | import {JSONAPIDataObject} from "../../store/json-api"; 5 | import {DEPAccount} from "../../store/dep/types"; 6 | 7 | export interface IDeviceProfilesTableProps { 8 | loading: boolean; 9 | data: Array>; 10 | onFetchData: (state: any, instance: any) => void; 11 | } 12 | 13 | const columns = [ 14 | { 15 | Header: "Display Name", 16 | accessor: "attributes.payload_display_name", 17 | id: "payload_display_name", 18 | }, 19 | { 20 | Header: "Identifier", 21 | accessor: "attributes.payload_identifier", 22 | id: "payload_identifier", 23 | }, 24 | ]; 25 | 26 | export const DeviceProfilesTable = ({ data, ...props }: IDeviceProfilesTableProps & Partial) => ( 27 | 34 | ); 35 | -------------------------------------------------------------------------------- /ui/src/components/react-tables/DeviceUpdatesTable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactTable, {TableProps} from "react-table"; 3 | import {AvailableOSUpdate} from "../../store/device/types"; 4 | import {JSONAPIDataObject} from "../../store/json-api"; 5 | 6 | export interface IDeviceUpdatesTableProps extends Partial { 7 | loading: boolean; 8 | data: Array>; 9 | onFetchData: (state: any, instance: any) => void; 10 | } 11 | 12 | const columns = [ 13 | { 14 | Header: "Product ID", 15 | accessor: "attributes.product_key", 16 | id: "product_key", 17 | maxWidth: 140, 18 | }, 19 | { 20 | Header: "Name", 21 | accessor: "attributes.human_readable_name", 22 | id: "human_readable_name", 23 | }, 24 | { 25 | Header: "Version", 26 | accessor: "attributes.version", 27 | id: "version", 28 | maxWidth: 100, 29 | }, 30 | ]; 31 | 32 | export const DeviceUpdatesTable = ({ data, ...props }: IDeviceUpdatesTableProps) => ( 33 | 40 | ); 41 | -------------------------------------------------------------------------------- /ui/src/components/semantic-ui/ButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Link, Route} from "react-router-dom"; 3 | import {Button, ButtonProps } from "semantic-ui-react"; 4 | 5 | interface IButtonLinkProps extends ButtonProps { 6 | to: string; 7 | } 8 | 9 | /** 10 | * The ButtonLink component mixes the visual style and behaviour of a semantic-ui-react button with a react-router Link. 11 | * @param {IButtonLinkProps} props 12 | * @returns {any} 13 | * @constructor 14 | */ 15 | export const ButtonLink = (props: IButtonLinkProps) => ( 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /ui/src/components/semantic-ui/MenuItemLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Link, Route} from "react-router-dom"; 3 | import {Menu} from "semantic-ui-react"; 4 | 5 | interface IMenuItemLinkProps { 6 | to: string; 7 | activeOnlyWhenExact?: boolean; 8 | header?: boolean; 9 | children: any; 10 | } 11 | 12 | export const MenuItemLink = ({ to, children, activeOnlyWhenExact = false, header = false }: IMenuItemLinkProps) => ( 13 | ( 14 | {children} 15 | )}/> 16 | ); 17 | -------------------------------------------------------------------------------- /ui/src/components/vpp/VPPAccountDetail.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { 4 | Button, 5 | Header, 6 | Icon, 7 | Segment, 8 | } from "semantic-ui-react"; 9 | 10 | import {format} from "date-fns"; 11 | import {VPPAccount} from "../../store/configuration/types"; 12 | 13 | export interface IVPPAccountDetailProps extends VPPAccount { 14 | } 15 | 16 | export const VPPAccountDetail: React.StatelessComponent = (props: IVPPAccountDetailProps) => ( 17 | 18 | 19 | 20 | 21 | VPP Token ({props.org_name}) 22 | 23 | 24 | Expires {format(props.exp_date)} 25 | 26 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /ui/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const CERTIFICATE_PURPOSE: {[propName: string]: string} = { 2 | "mdm.pushcert": "APNS MDM Push Certificate", 3 | "mdm.webcrt": "MDM Web Server Certificate", 4 | "mdm.cacert": "MDM SCEP CA Certificate", 5 | }; 6 | -------------------------------------------------------------------------------- /ui/src/containers/DashboardPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Container} from "semantic-ui-react"; 3 | import {RouteComponentProps} from "react-router"; 4 | 5 | export const DashboardPage: React.FunctionComponent> = () => ( 6 | 7 | 8 | Enroll (Direct) 9 | Enroll (OTA) 10 | Download Trust Profile 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /ui/src/containers/DeviceRename.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {connect} from "react-redux"; 3 | import {bindActionCreators, Dispatch} from "redux"; 4 | import {DeviceRenameModal} from "../components/modals/DeviceRenameModal"; 5 | import {RootState} from "../reducers"; 6 | import {upload, UploadActionRequest} from "../store/profiles/actions"; 7 | 8 | export interface IReduxStateProps { 9 | 10 | } 11 | 12 | export interface IReduxDispatchProps { 13 | 14 | } 15 | 16 | export const DeviceRename = connect( 17 | (state: RootState) => { 18 | return state.device; 19 | }, 20 | (dispatch: Dispatch, ownProps: any) => bindActionCreators({ 21 | upload, 22 | }, dispatch), 23 | )(DeviceRenameModal); 24 | -------------------------------------------------------------------------------- /ui/src/containers/ProfileUpload.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {connect} from "react-redux"; 3 | import {bindActionCreators, Dispatch} from "redux"; 4 | import {ProfileUploadModal} from "../components/modals/ProfileUploadModal"; 5 | import {RootState} from "../reducers"; 6 | import {upload, UploadActionRequest} from "../store/profiles/actions"; 7 | 8 | export interface IReduxDispatchProps { 9 | upload: UploadActionRequest; 10 | } 11 | 12 | export const ProfileUpload = connect( 13 | (state: RootState) => { 14 | return state.profiles; 15 | }, 16 | (dispatch: Dispatch, ownProps: any) => bindActionCreators({ 17 | upload, 18 | }, dispatch), 19 | )(ProfileUploadModal); 20 | -------------------------------------------------------------------------------- /ui/src/forms/DeviceGroupForm.tsx: -------------------------------------------------------------------------------- 1 | // import * as React from "react"; 2 | // import {Field, FormProps, reduxForm} from "redux-form"; 3 | // import Form, {FormComponent, FormProps} from "semantic-ui-react/src/collections/Form"; 4 | // import Button from "semantic-ui-react/src/elements/Button"; 5 | // 6 | // import {SemanticInput} from "./fields/SemanticInput"; 7 | // 8 | // export interface FormData { 9 | // name: string; 10 | // } 11 | // 12 | // interface DeviceGroupFormProps extends FormProps { 13 | // 14 | // } 15 | // 16 | // class UnconnectedDeviceGroupForm extends React.Component { 17 | // public render() { 18 | // const { 19 | // handleSubmit, 20 | // } = this.props; 21 | // 22 | // return ( 23 | // 24 | // 25 | // Save 26 | // 27 | // ); 28 | // } 29 | // } 30 | // 31 | // export const DeviceGroupForm = reduxForm({ 32 | // form: "device_group", 33 | // })(UnconnectedDeviceGroupForm); 34 | -------------------------------------------------------------------------------- /ui/src/guards.ts: -------------------------------------------------------------------------------- 1 | // NOTE: Does not work with frames (but we don't have any) 2 | import {ApiError} from "redux-api-middleware"; 3 | 4 | export const isArray = (v: any): v is Array => v instanceof Array; 5 | export const isApiError = (v: any): v is ApiError => v instanceof ApiError; 6 | 7 | //// Type Guards 8 | // import {ApiError, ErrorNames} from "redux-api-middleware"; 9 | // 10 | // export function isApiError(payload: any): payload is ApiError { 11 | // return payload.name && payload.name === ErrorNames.ApiError; 12 | // } 13 | -------------------------------------------------------------------------------- /ui/src/hooks/useForm.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const useForm = (callback) => { 4 | 5 | const [values, setValues] = useState({}); 6 | 7 | const handleSubmit = (event) => { 8 | if (event) event.preventDefault(); 9 | callback(); 10 | }; 11 | 12 | const handleChange = (event) => { 13 | event.persist(); 14 | setValues((values) => ({ ...values, [event.target.name]: event.target.value })); 15 | }; 16 | 17 | return { 18 | handleChange, 19 | handleSubmit, 20 | values, 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /ui/src/models.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface InstalledPayload { 4 | id?: number; 5 | description: string; 6 | display_name: string; 7 | identifier: string; 8 | organization: string; 9 | payload_type: string; 10 | uuid: string; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /ui/src/reducers/interfaces.ts: -------------------------------------------------------------------------------- 1 | import {ApiError} from "redux-api-middleware"; 2 | 3 | /** 4 | * This interface declares a common interface for reducers that contain an array of results and metadata about those 5 | * results. 6 | */ 7 | export interface IResults { 8 | items: TResultArray; 9 | loading: boolean; 10 | error?: ApiError | any; 11 | lastReceived?: Date; 12 | currentPage: number; 13 | pageSize: number; 14 | pages: number; 15 | recordCount?: number; 16 | } 17 | 18 | export const ResultsDefaultState: IResults = { 19 | currentPage: 1, 20 | error: null, 21 | items: [], 22 | lastReceived: null, 23 | loading: false, 24 | pageSize: 20, 25 | pages: 0, 26 | recordCount: 0, 27 | }; 28 | -------------------------------------------------------------------------------- /ui/src/selectors/device.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, Selector as Reselector} from "reselect"; 2 | import {RootState} from "../reducers/index"; 3 | 4 | export const getDeviceAvailableCapacity = (state: RootState) => state.device.device ? state.device.device.attributes.available_device_capacity : null; 5 | export const getDeviceCapacity = (state: RootState) => state.device.device ? state.device.device.attributes.device_capacity : null; 6 | 7 | export const getPercentCapacityUsed: Reselector = createSelector( 8 | [getDeviceCapacity, getDeviceAvailableCapacity], 9 | (deviceCapacity: number = 0, availableCapacity: number = 0) => (deviceCapacity - availableCapacity) 10 | ); 11 | 12 | -------------------------------------------------------------------------------- /ui/src/store/assistant/actions.ts: -------------------------------------------------------------------------------- 1 | import {ThunkAction} from 'redux-thunk'; 2 | 3 | export type NEXT_STEP = 'assistant/NEXT_STEP'; 4 | export const NEXT_STEP: NEXT_STEP = 'assistant/NEXT_STEP'; 5 | 6 | export type PREV_STEP = 'assistant/PREV_STEP'; 7 | export const PREV_STEP: PREV_STEP = 'assistant/PREV_STEP'; 8 | 9 | export interface NextStepAction { 10 | type: NEXT_STEP; 11 | } 12 | 13 | export interface PrevStepAction { 14 | type: PREV_STEP; 15 | } 16 | 17 | export const nextStep = (): NextStepAction => { 18 | return { 19 | type: NEXT_STEP 20 | }; 21 | }; 22 | 23 | export const prevStep = (): PrevStepAction => { 24 | return { 25 | type: PREV_STEP 26 | }; 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /ui/src/store/assistant/reducer.ts: -------------------------------------------------------------------------------- 1 | import * as actions from "./actions"; 2 | 3 | export interface IAssistantState { 4 | currentStep: number; 5 | totalSteps: number; 6 | } 7 | 8 | const initialState: IAssistantState = { 9 | currentStep: 0, 10 | totalSteps: 0, 11 | }; 12 | 13 | export type AssistantAction = actions.NextStepAction | actions.PrevStepAction; 14 | 15 | export function assistant(state: IAssistantState = initialState, action: AssistantAction): IAssistantState { 16 | switch (action.type) { 17 | case actions.NEXT_STEP: 18 | return { 19 | ...state, 20 | currentStep: (state.currentStep + 1), 21 | }; 22 | case actions.PREV_STEP: 23 | return { 24 | ...state, 25 | currentStep: (state.currentStep - 1), 26 | }; 27 | default: 28 | return state 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/store/auth/types.ts: -------------------------------------------------------------------------------- 1 | export interface IOAuth2TokenSuccessResponse { 2 | access_token: string; 3 | expires_in: number; 4 | token_type: "Bearer"; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/store/certificates/ca_reducer.ts: -------------------------------------------------------------------------------- 1 | import {JSONAPIDataObject, JSONAPIListResponse} from "../json-api"; 2 | import * as actions from "./ca_actions"; 3 | import {Certificate} from "./types"; 4 | 5 | export interface CAState { 6 | items?: JSONAPIListResponse>; 7 | loading: boolean; 8 | error: boolean; 9 | errorDetail?: any 10 | lastReceived?: Date; 11 | } 12 | 13 | const initialState: CAState = { 14 | loading: false, 15 | error: false, 16 | errorDetail: null, 17 | lastReceived: null, 18 | }; 19 | 20 | export type PushAction = actions.FetchCACertificatesActionResponse; 21 | 22 | export function ca(state: CAState = initialState, action: PushAction): CAState { 23 | switch (action.type) { 24 | case actions.CACERT_REQUEST: 25 | return { 26 | ...state, 27 | loading: true, 28 | }; 29 | case actions.CACERT_FAILURE: 30 | return { 31 | ...state, 32 | loading: false, 33 | error: true, 34 | errorDetail: action.payload, 35 | }; 36 | case actions.CACERT_SUCCESS: 37 | return { 38 | ...state, 39 | items: action.payload, 40 | lastReceived: new Date(), 41 | error: false, 42 | errorDetail: null, 43 | }; 44 | default: 45 | return state; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/store/certificates/types.ts: -------------------------------------------------------------------------------- 1 | export interface Certificate { 2 | type: string; 3 | x509_cn: string; 4 | not_before: Date; 5 | not_after: Date; 6 | fingerprint?: string; 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/store/commands/reducer.ts: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import {PostActionResponse} from "./actions"; 3 | 4 | export interface CommandsState { 5 | 6 | } 7 | 8 | const initialState: CommandsState = { 9 | 10 | }; 11 | 12 | type CommandAction = PostActionResponse; 13 | 14 | export function commands (state: CommandsState = initialState, action: CommandAction): CommandsState { 15 | switch (action.type) { 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/store/configuration/reducer.ts: -------------------------------------------------------------------------------- 1 | import {combineReducers} from "redux"; 2 | 3 | import {apns, APNSState} from "./apns_reducer"; 4 | import {scep, SCEPState} from "./scep_reducer"; 5 | import {vpp, VPPState} from "./vpp_reducer"; 6 | 7 | export interface ConfigurationState { 8 | scep: SCEPState; 9 | vpp: VPPState; 10 | apns: APNSState; 11 | } 12 | 13 | const initialState: ConfigurationState = { 14 | apns: null, 15 | scep: null, 16 | vpp: null, 17 | }; 18 | 19 | export function configuration(state: ConfigurationState = initialState, action: any): ConfigurationState { 20 | return combineReducers({ 21 | apns, 22 | scep, 23 | vpp, 24 | })(state, action); 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/store/configuration/types.ts: -------------------------------------------------------------------------------- 1 | export interface SCEPConfiguration { 2 | url: string; 3 | challenge_enabled: boolean; 4 | challenge: string; 5 | ca_fingerprint: string; 6 | subject: string; 7 | key_size: string; // Needs to be string to support redux-form 8 | key_type: "RSA"; 9 | key_usage: string; 10 | subject_alt_name: string; 11 | retries: number; 12 | retry_delay: number; 13 | certificate_renewal_time_interval: number; 14 | } 15 | 16 | export interface VPPAccount { 17 | org_name: string; 18 | exp_date: string; 19 | } 20 | 21 | // Possible Responses from mdmcert.download 22 | export interface IMDMCertResponse { 23 | result: "failure" | "success"; 24 | reason?: string; 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/store/configuration/vpp_reducer.ts: -------------------------------------------------------------------------------- 1 | import {isJSONAPIErrorResponsePayload, JSONAPIDetailResponse} from "../json-api"; 2 | import {VPPAccount} from "./types"; 3 | import {TokenActionResponse, VPPActionTypes} from "./vpp"; 4 | 5 | export interface VPPState { 6 | data?: JSONAPIDetailResponse; 7 | loading: boolean; 8 | submitted: boolean; 9 | error: boolean; 10 | errorDetail?: any; 11 | } 12 | 13 | const initialState: VPPState = { 14 | error: false, 15 | loading: false, 16 | submitted: false, 17 | }; 18 | 19 | type VPPAction = TokenActionResponse; 20 | 21 | export function vpp(state: VPPState = initialState, action: VPPAction): VPPState { 22 | switch (action.type) { 23 | case VPPActionTypes.TOKEN_REQUEST: 24 | return { 25 | ...state, 26 | loading: true, 27 | }; 28 | case VPPActionTypes.TOKEN_SUCCESS: 29 | if (isJSONAPIErrorResponsePayload(action.payload)) { 30 | return state; 31 | } else { 32 | return { 33 | ...state, 34 | data: action.payload, 35 | loading: false, 36 | }; 37 | } 38 | case VPPActionTypes.TOKEN_FAILURE: 39 | return { 40 | ...state, 41 | error: true, 42 | }; 43 | default: 44 | return state; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ui/src/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import {applyMiddleware, compose, createStore, Store} from "redux"; 2 | import {Middleware} from "redux"; 3 | import {apiMiddleware} from "redux-api-middleware"; 4 | import thunk from "redux-thunk"; 5 | import rootReducer from "../reducers"; 6 | import {RootState} from "../reducers"; 7 | import {createBrowserHistory} from "history"; 8 | 9 | const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 10 | 11 | export const history = createBrowserHistory(); 12 | 13 | export const configureStore = (initialState: RootState, ...middlewares: Middleware[] ): Store => { 14 | 15 | const enhancer = composeEnhancers( 16 | applyMiddleware( 17 | thunk, 18 | apiMiddleware, 19 | ...middlewares, 20 | ), 21 | ); 22 | 23 | const store = createStore( 24 | rootReducer(history), 25 | initialState, 26 | enhancer, 27 | ); 28 | 29 | if (module.hot) { 30 | module.hot.accept("../reducers", () => { 31 | const nextRootReducer = require("../reducers").default; 32 | store.replaceReducer(nextRootReducer) 33 | }); 34 | } 35 | 36 | return store; 37 | }; 38 | 39 | export default configureStore; 40 | -------------------------------------------------------------------------------- /ui/src/store/constants.ts: -------------------------------------------------------------------------------- 1 | export const JSONAPI_HEADERS = { 2 | "Accept": "application/vnd.api+json", 3 | "Content-Type": "application/vnd.api+json", 4 | }; 5 | 6 | export const JSON_HEADERS = { 7 | "Accept": "application/json", 8 | "Content-Type": "application/json", 9 | }; 10 | 11 | // TODO: This is for resource owner password grant but we should use something much more secure. 12 | export const OAUTH2_CLIENT_ID = "F8955645-A21D-44AE-9387-42B0800ADF15"; 13 | export const OAUTH2_CLIENT_SECRET = "A"; 14 | 15 | // Flask-REST-JSONAPI Filter and Sort definitions 16 | 17 | export interface OtherAction { 18 | type: string; 19 | payload?: any; 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/store/dep/reducer.ts: -------------------------------------------------------------------------------- 1 | import {combineReducers, Reducer} from 'redux'; 2 | import {account, IDEPAccountState} from "./account_reducer"; 3 | import {accounts, IDEPAccountsState} from "./accounts_reducer"; 4 | import {profiles, IDEPProfilesState} from "./profiles_reducer"; 5 | import {IDEPProfileState, profile} from "./profile_reducer"; 6 | 7 | export const dep = combineReducers({ 8 | account, 9 | accounts, 10 | profile, 11 | profiles, 12 | }); 13 | 14 | export interface IDEPState { 15 | account?: IDEPAccountState; 16 | accounts?: IDEPAccountsState; 17 | profiles?: IDEPProfilesState; 18 | profile?: IDEPProfileState; 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/store/device/available_os_updates_reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATES_SUCCESS, 3 | AvailableOSUpdatesActionResponse 4 | } from "./updates"; 5 | import {JSONAPIDataObject, isJSONAPIErrorResponsePayload} from "../json-api"; 6 | import {InstalledProfile} from "./types"; 7 | import {OtherAction} from "../constants"; 8 | 9 | 10 | export interface AvailableOSUpdatesState { 11 | items?: Array>; 12 | loading: boolean; 13 | pageSize: number; 14 | pages: number; 15 | recordCount: number; 16 | } 17 | 18 | const initialState: AvailableOSUpdatesState = { 19 | items: [], 20 | loading: false, 21 | pageSize: 20, 22 | pages: 0, 23 | recordCount: 0, 24 | }; 25 | 26 | type AvailableOSUpdatesAction = AvailableOSUpdatesActionResponse | OtherAction; 27 | 28 | export function available_os_updates_reducer( 29 | state: AvailableOSUpdatesState = initialState, 30 | action: AvailableOSUpdatesAction): AvailableOSUpdatesState { 31 | switch (action.type) { 32 | case UPDATES_SUCCESS: 33 | if (isJSONAPIErrorResponsePayload(action.payload)) { 34 | return state; 35 | } else { 36 | return { 37 | ...state, 38 | items: action.payload.data, 39 | recordCount: action.payload.meta.count, 40 | }; 41 | } 42 | default: 43 | return state; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ui/src/store/device/commands_reducer.ts: -------------------------------------------------------------------------------- 1 | import {CommandsActionResponse, DevicesActionTypes} from "./actions"; 2 | import {JSONAPIDataObject, isJSONAPIErrorResponsePayload} from "../json-api"; 3 | import {Command} from "./types"; 4 | import {OtherAction} from "../constants"; 5 | 6 | export interface DeviceCommandsState { 7 | items?: Array>; 8 | loading: boolean; 9 | pageSize: number; 10 | pages: number; 11 | recordCount: number; 12 | } 13 | 14 | const initialState: DeviceCommandsState = { 15 | items: [], 16 | loading: false, 17 | pageSize: 20, 18 | pages: 0, 19 | recordCount: 0, 20 | }; 21 | 22 | type DeviceCommandsAction = CommandsActionResponse | OtherAction; 23 | 24 | export function commands_reducer(state: DeviceCommandsState = initialState, action: DeviceCommandsAction): DeviceCommandsState { 25 | switch (action.type) { 26 | case DevicesActionTypes.COMMANDS_SUCCESS: 27 | if (isJSONAPIErrorResponsePayload(action.payload)) { 28 | return state; 29 | } else { 30 | return { 31 | ...state, 32 | items: action.payload.data, 33 | pages: Math.floor(action.payload.meta.count / state.pageSize), 34 | recordCount: action.payload.meta.count, 35 | }; 36 | } 37 | default: 38 | return state; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ui/src/store/device/installed_profiles_reducer.ts: -------------------------------------------------------------------------------- 1 | import {OtherAction} from "../constants"; 2 | import {isJSONAPIErrorResponsePayload, JSONAPIDataObject} from "../json-api"; 3 | import { 4 | InstalledProfilesActionResponse, 5 | PROFILES_SUCCESS, 6 | } from "./profiles"; 7 | import {InstalledProfile} from "./types"; 8 | 9 | export interface InstalledProfilesState { 10 | items?: Array>; 11 | loading: boolean; 12 | pageSize: number; 13 | pages: number; 14 | recordCount: number; 15 | } 16 | 17 | const initialState: InstalledProfilesState = { 18 | items: [], 19 | loading: false, 20 | pageSize: 20, 21 | pages: 0, 22 | recordCount: 0, 23 | }; 24 | 25 | type InstalledProfilesAction = InstalledProfilesActionResponse | OtherAction; 26 | 27 | export function installed_profiles_reducer(state: InstalledProfilesState = initialState, action: InstalledProfilesAction): InstalledProfilesState { 28 | switch (action.type) { 29 | case PROFILES_SUCCESS: 30 | if (isJSONAPIErrorResponsePayload(action.payload)) { 31 | return state; 32 | } else { 33 | return { 34 | ...state, 35 | items: action.payload.data, 36 | recordCount: action.payload.meta.count, 37 | }; 38 | } 39 | default: 40 | return state; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/store/device_groups/types.ts: -------------------------------------------------------------------------------- 1 | export interface DeviceGroup { 2 | id?: string; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/store/devices/actions.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdmnt/commandment/17c1dbe3f5301eab0f950f82608c231c15a3ff43/ui/src/store/devices/actions.ts -------------------------------------------------------------------------------- /ui/src/store/organization/types.ts: -------------------------------------------------------------------------------- 1 | export interface Organization { 2 | id?: string; 3 | name: string; 4 | payload_prefix: string; 5 | x509_ou: string; 6 | x509_o: string; 7 | x509_st: string; 8 | x509_c: string; 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/store/pki/actions.ts: -------------------------------------------------------------------------------- 1 | import { RSAA, RSAAction } from "redux-api-middleware"; 2 | import { JSONAPI_HEADERS } from "../constants" 3 | import {CertificatePurpose} from "./types"; 4 | import {RootState} from "../../reducers"; 5 | 6 | export type NEW_REQUEST = "signing_requests/NEW_REQUEST"; 7 | export const NEW_REQUEST: NEW_REQUEST = "signing_requests/NEW_REQUEST"; 8 | export type NEW_SUCCESS = "signing_requests/NEW_SUCCESS"; 9 | export const NEW_SUCCESS: NEW_SUCCESS = "signing_requests/NEW_SUCCESS"; 10 | export type NEW_FAILURE = "signing_requests/NEW_FAILURE"; 11 | export const NEW_FAILURE: NEW_FAILURE = "signing_requests/NEW_FAILURE"; 12 | 13 | export const newCertificateSigningRequest = (purpose: CertificatePurpose): RSAAction => { 14 | return { 15 | [RSAA]: { 16 | body: JSON.stringify({ purpose }), 17 | endpoint: "/api/v1/certificate_signing_requests/new", 18 | headers: (state: RootState) => ({ 19 | ...JSONAPI_HEADERS, 20 | Authorization: `Bearer ${state.auth.access_token}`, 21 | }), 22 | method: "GET", 23 | types: [ 24 | NEW_REQUEST, 25 | NEW_SUCCESS, 26 | NEW_FAILURE, 27 | ], 28 | }, 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /ui/src/store/pki/types.ts: -------------------------------------------------------------------------------- 1 | export type CertificatePurpose = "apns" | "ssl"; 2 | -------------------------------------------------------------------------------- /ui/src/store/profiles/types.ts: -------------------------------------------------------------------------------- 1 | export interface Profile { 2 | id?: number; 3 | description?: string; 4 | display_name?: string; 5 | expiration_date?: Date; 6 | identifier: string; 7 | organization?: string; 8 | uuid: string; 9 | removal_disallowed?: boolean; 10 | version: number; 11 | scope?: string; 12 | removal_date?: Date; 13 | duration_until_removal?: number; 14 | consent_en?: string; 15 | } 16 | 17 | export type ProfileRelationship = "tags"; 18 | -------------------------------------------------------------------------------- /ui/src/store/table/actions.ts: -------------------------------------------------------------------------------- 1 | import {ActionCreator} from "redux"; 2 | 3 | export enum TableActionTypes { 4 | TOGGLE_SELECTION = "@react_table/TOGGLE_SELECTION", 5 | TOGGLE_ALL = "@react_table/TOGGLE_ALL", 6 | } 7 | 8 | export type ToggleSelectionActionCreator = (key: string, shiftKeyPressed: boolean, row: any) => IToggleSelectionAction; 9 | export interface IToggleSelectionAction { 10 | key: string; 11 | shiftKeyPressed: boolean; 12 | row: any; 13 | type: TableActionTypes; 14 | } 15 | 16 | export const toggleSelection: ActionCreator = 17 | (key: string, shiftKeyPressed: boolean, row: any) => { 18 | return { 19 | key, 20 | row, 21 | shiftKeyPressed, 22 | type: TableActionTypes.TOGGLE_SELECTION, 23 | }; 24 | }; 25 | 26 | export interface IToggleAllAction { 27 | type: TableActionTypes; 28 | } 29 | 30 | export const toggleAll: ActionCreator = () => { 31 | return { 32 | type: TableActionTypes.TOGGLE_ALL, 33 | }; 34 | }; 35 | 36 | export type TableActions = IToggleSelectionAction & IToggleAllAction; 37 | -------------------------------------------------------------------------------- /ui/src/store/table/reducer.ts: -------------------------------------------------------------------------------- 1 | import {Reducer} from "redux"; 2 | import {TableActions, TableActionTypes} from "./actions"; 3 | 4 | export interface ITableState { 5 | pageSize: number; 6 | pages: number; 7 | selection: string[]; 8 | } 9 | 10 | const initialState: ITableState = { 11 | pageSize: 20, 12 | pages: 0, 13 | selection: [], 14 | }; 15 | 16 | export const table: Reducer = (state = initialState, action) => { 17 | switch (action.type) { 18 | case TableActionTypes.TOGGLE_ALL: 19 | return state; 20 | case TableActionTypes.TOGGLE_SELECTION: 21 | let selection = [...state.selection]; 22 | const keyIndex = state.selection.indexOf(action.key); 23 | if (keyIndex !== -1) { 24 | selection = [ 25 | ...selection.slice(0, keyIndex), 26 | ...selection.slice(keyIndex + 1), 27 | ]; 28 | } else { 29 | selection.push(action.key); 30 | } 31 | 32 | return { 33 | ...state, 34 | selection, 35 | }; 36 | default: 37 | return state; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /ui/src/store/table/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IReactTableState { 3 | page: number; 4 | pageSize: number; 5 | filtered: Array<{ id: string; value: any; }>; 6 | sorted: Array<{ id: string; desc: boolean; }>; 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/store/tags/types.ts: -------------------------------------------------------------------------------- 1 | export interface Tag { 2 | id?: string; 3 | name: string; 4 | color: string; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/stories/DEPProfileForm.tsx: -------------------------------------------------------------------------------- 1 | import { action } from "@storybook/addon-actions"; 2 | import { storiesOf } from "@storybook/react"; 3 | import * as React from "react"; 4 | import {Container} from "semantic-ui-react"; 5 | import {DEPProfileForm} from "../components/forms/DEPProfileForm"; 6 | 7 | storiesOf("DEPProfileForm", module) 8 | .add("default", () => ( 9 | 10 | 16 | 17 | )); 18 | -------------------------------------------------------------------------------- /ui/src/stories/index.ts: -------------------------------------------------------------------------------- 1 | import "./redux"; 2 | 3 | export default {} 4 | -------------------------------------------------------------------------------- /ui/src/stories/redux.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Provider} from 'react-redux'; 3 | import {combineReducers, compose, createStore} from 'redux'; 4 | import {addDecorator, Story, StoryDecorator} from '@storybook/react'; 5 | 6 | const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 7 | 8 | const store = createStore((state, action) => state, composeEnhancers()); 9 | 10 | const StoreDecorator: StoryDecorator = (story) => ( 11 | 12 | { story() } 13 | 14 | ); 15 | 16 | addDecorator(StoreDecorator); 17 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | "noImplicitAny": true, 6 | "jsx": "react", 7 | "allowJs": false, 8 | "noEmitOnError": false, 9 | "pretty": true, 10 | "removeComments": true, 11 | "target": "ES5" 12 | }, 13 | "include": [ 14 | "./src/**/*" 15 | ], 16 | "exclude": [ 17 | "node_modules", 18 | "_deprecated" 19 | ], 20 | "paths": { 21 | "lodash/*": [ 22 | "node_modules/@types/lodash-es/*" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "semicolon": false 9 | }, 10 | "rulesDirectory": [] 11 | } -------------------------------------------------------------------------------- /ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === "production") { 2 | console.log('using config from ./webpack.config.prod'); 3 | module.exports = require('./webpack.config.prod'); 4 | } else { 5 | console.log('using config from ./webpack.config.hmr'); 6 | module.exports = require('./webpack.config.hmr'); 7 | } 8 | --------------------------------------------------------------------------------
Congratulations, your commandment server is set up!
If your devices are not DEP provisioned, 19 | use the link below to download an enrollment profile.
{ error.response.code }